Application roles
An application role is a Lua module that implements specific functions or logic. You can turn on or off a particular role for certain instances in a configuration without restarting these instances. A role is run when a configuration is loaded or reloaded.
Roles can be divided into the following groups:
- Tarantool's built-in roles. For example, the
config.storagerole can be used to make a Tarantool replica set act as a configuration storage. - Roles provided by third-party Lua modules. For example, the
CRUD module provides the
roles.crud-storageandroles.crud-routerroles that enable CRUD operations in a sharded cluster. - Custom roles that are developed as a part of a cluster application. For example, you can create a custom role to define a stored procedure or implement a supplementary service, such as an email notifier or a replicator.
This section describes how to develop custom roles. To learn how to enable and configure roles, see configuration_application_roles.
A custom role can be configured in the same way as roles provided by Tarantool or third-party Lua modules. You can learn more from configuration_application_roles.
This example shows how to enable and configure the greeter role, which
is implemented in the next section:
instance001:
The role configuration provided in roles_cfg can be accessed when
validating and
applying this configuration.
Tarantool includes the
experimental.config.utils.schema built-in
module that provides tools for managing user-defined configurations of
applications (app.cfg) and roles (roles_cfg). The examples below
show its basic usage.
Given that a role is a Lua module, a role name is
passed to require() to obtain the module. When
developing an application, you can
place a file with the role code next to the cluster configuration file.
A custom application role is an object which implements custom functions or logic adding to Tarantool's built-in roles and roles provided by third-party Lua modules. For example, a logging role can be created to add logging functionality on top of the built-in one.
Creating a custom role includes the following steps:
- (Optional) Define the role configuration schema.
- Define a function that validates a role configuration.
- Define a function that applies a validated configuration.
- Define a function that stops a role.
- (Optional) Define roles from which this custom role depends on.
- (Optional) Define the
on_eventcallback function.
As a result, a role module should return an object that has corresponding functions and fields specified:
return {validate = function() -- ... -- end,apply = function() -- ... -- end,stop = function() -- ... -- end,dependencies = { -- ... -- },on_event = function(config, key, value)local log = require('log')log.info('roles_cfg.my_role.foo: ' .. config.foo)log.info('on_event is triggered by ' .. key)log.info('is_ro: ' .. value.is_ro)end,}
The examples in this article show how to do this.
You can omit the optional steps and get a simple role as in the example below.
return {validate = function() -- ... -- end,apply = function() -- ... -- end,stop = function() -- ... -- end,}
You can modify a role, for example, by adding dependencies or specifying
the on_event callback. If you modify a role, you need to restart the
Tarantool instance with the role in order to apply the changes.
The experimental.config.utils.schema built-in module provides the config-utils-schema_object class. An object of this class defines a custom configuration scheme of a role or an application.
This example shows how to define a schema that reflects the role configuration shown above:
local greeter_schema = schema.new('greeter', schema.record({greeting = schema.scalar({type = 'string',allowed_values = { 'Hi', 'Hello' }})}))
If you don't use the module, skip this step. In this case, use the
cfg argument of the role's validate() and apply() functions to
refer to its configuration values, for example, cfg.greeting.
To validate a role configuration, you need to define the validate function.
In the example below, the validate() function of the role
configuration schema is used to validate the greeting value:
local function validate(cfg)greeter_schema:validate(cfg)end
If the configuration is not valid, validate() reports an unrecoverable
error by throwing an error object.
To apply the validated configuration, define the
apply([cfg]) function. As the
validate() function, apply() provides access to a role's
configuration using the cfg argument.
In the example below, the apply() function uses the log
module to write a value from the role configuration to the log:
local function apply(cfg)log.info("%s from the 'greeter' role!", greeter_schema:get(cfg, 'greeting'))end
To stop a role, use the stop() function.
In the example below, the stop() function uses the log
module to indicate that a role is stopped:
local function stop()log.info("The 'greeter' role is stopped")end
When you've defined all the role functions, you need to return an object that has corresponding functions specified:
return {
To define a role's dependencies, use the
dependencies field. In this example,
the byeer role has the greeter role as the dependency:
-- byeer.lua --local log = require('log').new("byeer")return {dependencies = { 'greeter' },validate = function() end,apply = function() log.info("Bye from the 'byeer' role!") end,stop = function() end,}
A role cannot be started without its dependencies. This means that all
the dependencies of a role should be defined in the roles
configuration parameter:
instance001:roles: [ greeter, byeer ]
You can find the full example here: application_role_cfg.
Since version 3.3.1,
you can define the on_event callback for custom roles. The on_event
callback is called every time a box.status system event is
broadcasted. If multiple custom roles have the on_event callback
defined, these callbacks are called one after another in the order
defined by roles dependencies.
The on_event callback accepts 3 arguments, when it is called:
-
config, which contains the configuration of the role; -
key, which reflects the trigger event and is set to:config.applyif the callback was triggered by a configuration update;box.statusif it was triggered by thebox.statussystem event.
-
value, which shows the information about the instance status as in the triggerbox.statussystem event. If the callback is triggered by a configuration update, thevalueshows the information of the most recentbox.statussystem event.
The example of the on_event callback is provided in the
spaces creation article below.
You can add initialization code to a role by defining and calling a function with an arbitrary name at the top level of a module, for example:
local function init()-- ... --endinit()
For example, you can create spaces, define indexes, or grant privileges to specific users or roles.
See also: roles_create_space.
To create a space in a role, you need to make sure that the target
instance is in read-write mode (its
box.info.ro is false). You can check an
instance state by subscribing to the box.status event using
box.watch():
box.watch('box.status', function()-- creating a space-- ...end)
Since version 3.3.1,
you can define space creation in a role via the on_event
callback function.
See the example of such definition below:
return {validate = function() end,apply = function() end,stop = function() end,on_event = function(config, key, value)-- Can only create spaces on RW.if value.is_ro thenreturnend-- Assume the role config is a table.if type(config) ~= 'table' thenerror('Config must be a table')endlocal space_name = config.space_name or 'default'box.schema.space.create(space_name, {if_not_exists = true,})end}
A role's life cycle includes the stages described below.
-
Loading roles
On each run, all roles are loaded in the order they are specified in the configuration. This stage takes effect when a role is enabled or an instance with this role is restarted. At this stage, a role executes the initialization code.
A role cannot be started if it has dependencies that are not specified in a configuration.
<!– –>
-
Stopping roles
This stage takes effect during a configuration reload when a role is removed from the configuration for a given instance. Note that all
stop()calls are performed before anyvalidate()orapply()calls. This means that old roles are stopped first, and only then new roles are started. -
Validating a role's configurations
At this stage, a configuration for each role is validated using the corresponding validate() function in the same order in which they are specified in the configuration.
-
Applying a role's configurations
At this stage, a configuration for each role is applied using the corresponding apply() function in the same order in which they are specified in the configuration.
All role's functions report an unrecoverable error by throwing an error
object. If an error is thrown in any phase, applying a configuration is
stopped. If starting or stopping a role throws an error, no roles are
stopped or started afterward. An error is caught and shown in
config:info() in the alerts section.
For roles that depend on each
other, their validate(), apply(), and stop() functions are
executed taking into account the dependencies. Suppose, there are three
independent and two dependent roles:
role1role2role3└─── role4└─── role5
role1,role2, androle5are independent roles.role3depends onrole4,role4depends onrole5.
The roles are enabled in a configuration as follows:
roles: [ role1, role2, role3, role4, role5 ]
In this case, validate() and apply() for these roles are executed in
the following order:
role1 -> role2 -> role5 -> role4 -> role3
Roles removed from a configuration are stopped in the order reversed to
the order they are specified in a configuration, taking into account the
dependencies. Suppose, all roles except role1 are removed from the
configuration above:
roles: [ role1 ]
After reloading a configuration, stop() functions for the removed
roles are executed in the following order:
role3 -> role4 -> role5 -> role2
The example below shows how to enable the custom greeter role for
instance001:
instance001:roles: [ greeter ]
The implementation of this role looks as follows:
-- greeter.lua --return {validate = function() end,apply = function() require('log').info("Hi from the 'greeter' role!") end,stop = function() end,}
Example on GitHub: application_role
The example below shows how to enable the custom greeter role for
instance001 and specify the configuration for this role:
instance001:roles: [ greeter ]roles_cfg:greeter:greeting: 'Hi'
The implementation of this role looks as follows:
local greeter_schema = schema.new('greeter', schema.record({greeting = schema.scalar({type = 'string',allowed_values = { 'Hi', 'Hello' }})}))
local function validate(cfg)greeter_schema:validate(cfg)end
local function apply(cfg)log.info("%s from the 'greeter' role!", greeter_schema:get(cfg, 'greeting'))end
local function stop()log.info("The 'greeter' role is stopped")end
return {
Example on GitHub: application_role_cfg
The example below shows how to enable and configure the http-api
custom role:
instance001:roles: [ http-api ]roles_cfg:http-api:host: '127.0.0.1'port: 8080
The implementation of this role looks as follows:
-- http-api.lua --local httpdlocal json = require('json')local schema = require('experimental.config.utils.schema')local function validate_host(host, w)local host_pattern = "^(%d+)%.(%d+)%.(%d+)%.(%d+)$"if not host:match(host_pattern) thenw.error("'host' should be a string containing a valid IP address, got %q", host)endendlocal function validate_port(port, w)if port <= 1 or port >= 65535 thenw.error("'port' should be between 1 and 65535, got %d", port)endendlocal listen_address_schema = schema.new('listen_address', schema.record({host = schema.scalar({type = 'string',validate = validate_host,default = '127.0.0.1',}),port = schema.scalar({type = 'integer',validate = validate_port,default = 8080,}),}))local function validate(cfg)listen_address_schema:validate(cfg)endlocal function apply(cfg)if httpd thenhttpd:stop()endlocal cfg_with_defaults = listen_address_schema:apply_default(cfg)local host = listen_address_schema:get(cfg_with_defaults, 'host')local port = listen_address_schema:get(cfg_with_defaults, 'port')httpd = require('http.server').new(host, port)local response_headers = { ['content-type'] = 'application/json' }httpd:route({ path = '/band/:id', method = 'GET' }, function(req)local id = req:stash('id')local band_tuple = box.space.bands:get(tonumber(id))if not band_tuple thenreturn { status = 404, body = 'Band not found' }elselocal band = { id = band_tuple['id'],band_name = band_tuple['band_name'],year = band_tuple['year'] }return { status = 200, headers = response_headers, body = json.encode(band) }endend)httpd:route({ path = '/band', method = 'GET' }, function(req)local limit = req:query_param('limit')if not limit thenlimit = 5endlocal band_tuples = box.space.bands:select({}, { limit = tonumber(limit) })local bands = {}for _, tuple in pairs(band_tuples) dolocal band = { id = tuple['id'],band_name = tuple['band_name'],year = tuple['year'] }table.insert(bands, band)endreturn { status = 200, headers = response_headers, body = json.encode(bands) }end)httpd:start()endlocal function stop()httpd:stop()endlocal function init()require('data'):add_sample_data()endinit()return {validate = validate,apply = apply,stop = stop,}
Example on GitHub: application_role_http_api
Members | |
|---|---|
Validate a role's configuration. | |
Apply a role's configuration. | |
Stop a role. |
Validate a role's configuration. This function is called on instance
startup or when the
configuration is reloaded for the
instance with this role. Note that the validate() function is called
regardless of whether the role's configuration or any field in a
cluster's configuration is changed.
validate() should throw an error if the validation fails.
Parameters:
cfg— a role's role configuration to be validated. This parameter provides access to configuration options defined in roles_cfg.<role_name>. To get values of configuration options placed outsideroles_cfg.<role_name>, use config:get().
See also: roles_create_custom_role_validate
Apply a role's configuration. apply() is called after validate() is
executed for all the enabled roles. As the validate() function,
apply() is called on instance startup or when the configuration is
reloaded for the instance with this role.
apply() should throw an error if the specified configuration can't be
applied.
cfg— a role's role configuration to be applied. This parameter provides access to configuration options defined in roles_cfg.. To get values of configuration options placed outsideroles_cfg.<role_name>, use config:get().
See also: roles_create_custom_role_apply
Stop a role. This function is called on configuration reload if the role
is removed from roles for the given instance.
See also: roles_create_custom_role_stop
(Optional) Define a role's dependencies.
Return type
table
See also: roles_create_custom_role_dependencies