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.storage
role 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-storage
androles.crud-router
roles 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 Enabling and configuring roles.
Примечание
Don’t confuse application roles with other role types:
- A role is a container for privileges that can be granted to users. Learn more in Роли.
- A role of a replica set in regard to sharding. Learn more in Sharding 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 Enabling and configuring roles.
This example shows how to enable and configure the greeter
role, which is implemented in the next section:
instance001:
roles: [ greeter ]
roles_cfg:
greeter:
greeting: 'Hi'
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.
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.
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 = { -- ... -- },
}
The examples below show how to do this.
Примечание
Code snippets shown in this section are included from the following application: application_role_cfg.
The experimental.config.utils.schema built-in module provides the 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([cfg]) 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 {
validate = validate,
apply = apply,
stop = stop,
}
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.
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()
-- ... --
end
init()
For example, you can create spaces, define indexes, or grant privileges to specific users or roles.
See also: Specifics of creating spaces.
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)
Примечание
Given that a role may be enabled when an instance is already in read-write mode, you also need to execute schema initialization code from apply(). To make sure a space is created only once, use the if_not_exists option.
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.
Примечание
Dependencies do not affect the order in which roles are loaded. However, the
validate()
,apply()
, andstop()
functions are executed taking dependencies into account. Learn more in Executing functions for dependent roles.
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:
role1
role2
role3
└─── role4
└─── role5
role1
,role2
, androle5
are independent roles.role3
depends onrole4
,role4
depends 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:
-- greeter.lua --
local log = require('log').new("greeter")
local schema = require('experimental.config.utils.schema')
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 {
validate = validate,
apply = apply,
stop = stop,
}
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 httpd
local 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) then
w.error("'host' should be a string containing a valid IP address, got %q", host)
end
end
local function validate_port(port, w)
if port <= 1 or port >= 65535 then
w.error("'port' should be between 1 and 65535, got %d", port)
end
end
local 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)
end
local function apply(cfg)
if httpd then
httpd:stop()
end
local 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 then
return { status = 404, body = 'Band not found' }
else
local 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) }
end
end)
httpd:route({ path = '/band', method = 'GET' }, function(req)
local limit = req:query_param('limit')
if not limit then
limit = 5
end
local band_tuples = box.space.bands:select({}, { limit = tonumber(limit) })
local bands = {}
for _, tuple in pairs(band_tuples) do
local band = { id = tuple['id'],
band_name = tuple['band_name'],
year = tuple['year'] }
table.insert(bands, band)
end
return { status = 200, headers = response_headers, body = json.encode(bands) }
end)
httpd:start()
end
local function stop()
httpd:stop()
end
local function init()
require('data'):add_sample_data()
end
init()
return {
validate = validate,
apply = apply,
stop = stop,
}
Example on GitHub: application_role_http_api
Members | |
validate([cfg]) | Validate a role’s configuration. |
apply([cfg]) | Apply a role’s configuration. |
stop() | Stop a role. |
dependencies | Define a role’s dependencies. |
-
validate
([cfg])¶ 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.Параметры: - 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 outside
roles_cfg.<role_name>
, use config:get().
See also: Validating a role configuration
- 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 outside
-
apply
([cfg])¶ Apply a role’s configuration.
apply()
is called aftervalidate()
is executed for all the enabled roles. As thevalidate()
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.Примечание
Note that
apply()
is not invoked if an instance switches to read-write mode when replication.failover is set toelection
orsupervised
. You can check an instance state by subscribing to thebox.status
event using box.watch().Параметры: - cfg – a role’s role configuration to be applied.
This parameter provides access to configuration options defined in roles_cfg.<role_name>.
To get values of configuration options placed outside
roles_cfg.<role_name>
, use config:get().
See also: Applying a role configuration
- cfg – a role’s role configuration to be applied.
This parameter provides access to configuration options defined in roles_cfg.<role_name>.
To get values of configuration options placed outside
-
stop
()¶ Stop a role. This function is called on configuration reload if the role is removed from
roles
for the given instance.See also: Stopping a role
-
dependencies
¶ (Optional) Define a role’s dependencies.
Rtype: table See also: Role dependencies