Application roles | Tarantool

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 and roles.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.

Note

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 Roles.
  • 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’s configuration provided in roles_cfg can be accessed when validating and applying this configuration.

Given that a role is a Lua module, a role’s name is passed to require() to obtain the module. When developing an application, you can place a file with a role’s code next to a cluster’s configuration file.

Creating a custom role includes the following steps:

  1. Define a function that validates a role’s configuration.
  2. Define a function that applies a validated configuration.
  3. Define a function that stops a role.
  4. (Optional) Define roles from which this custom role depends on.

As a result, a role’s 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.

Note

Code snippets shown in this section are included from the following application: application_role_cfg.

To validate a role’s configuration, you need to define the validate([cfg]) function. The cfg argument provides access to the role’s configuration and check its validity.

In the example below, the validate() function is used to validate the greeting configuration value:

local function validate(cfg)
    if cfg.greeting then
        assert(type(cfg.greeting) == "string", "'greeting' should be a string")
        assert(cfg.greeting == "Hi" or cfg.greeting == "Hello", "'greeting' should be 'Hi' or 'Hello'")
    end
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 role’s configuration value to the log:

local function apply(cfg)
    log.info("%s from the 'greeter' role!", 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’s 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)

Note

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.

  1. 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.

    Note

    Dependencies do not affect the order in which roles are loaded. However, the validate(), apply(), and stop() functions are executed taking dependencies into account. Learn more in Executing functions for dependent roles.

  1. 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 any validate() or apply() calls. This means that old roles are stopped first, and only then new roles are started.

  1. 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.

  1. 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, and role5 are independent roles.
  • role3 depends on role4, role4 depends on role5.

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 function validate(cfg)
    if cfg.greeting then
        assert(type(cfg.greeting) == "string", "'greeting' should be a string")
        assert(cfg.greeting == "Hi" or cfg.greeting == "Hello", "'greeting' should be 'Hi' or 'Hello'")
    end
end

local function apply(cfg)
    log.info("%s from the 'greeter' role!", 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 function validate(cfg)
    if cfg.host then
        assert(type(cfg.host) == "string", "'host' should be a string containing a valid IP address")
    end
    if cfg.port then
        assert(type(cfg.port) == "number", "'port' should be a number")
        assert(cfg.port >= 1 and cfg.port <= 65535, "'port' should be between 1 and 65535")
    end
end

local function apply(cfg)
    if httpd then
        httpd:stop()
    end
    httpd = require('http.server').new(cfg.host, cfg.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.

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 outside roles_cfg.<role_name>, use config:get().

See also: Validating a role configuration

apply([cfg])

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.

Note

Note that apply() is not invoked if an instance switches to read-write mode when replication.failover is set to election or supervised. You can check an instance state by subscribing to the box.status event using box.watch().

Parameters:
  • 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

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

Found what you were looking for?
Feedback