В классическом Tarantool (1.x/2.x) вся бизнес-логика жила в одном init-скрипте: вы вызывали box.cfg, потом руками создавали спейсы, поднимали HTTP-сервер, регистрировали функции. Перезапуск процесса был единственным способом подхватить изменения. В 3.x появилась декларативная конфигурация (YAML/etcd), и вместе с ней - модульная архитектура для кода: роли (roles) и приложение (app). Идея проста: код тоже становится частью конфигурации. Вы перечисляете, какие куски логики включены на конкретном инстансе, а движок сам прогоняет их через единый жизненный цикл и умеет включать/выключать их на лету, без рестарта.
Роль - это Lua-модуль, реализующий конкретную функцию или логику. Роль можно включить или выключить для нужных инстансов прямо в конфигурации, и она запускается при загрузке или перезагрузке конфига - перезапуск инстанса не требуется. Роли делятся на три группы:
- встроенные роли Tarantool (например, config.storage - превращает репликасет в централизованное хранилище конфигурации);
- роли из сторонних официальных модулей (например, модуль crud даёт roles.crud-storage и roles.crud-router для шардированного кластера);
- кастомные роли - часть вашего кластерного приложения (хранимая процедура, нотификатор, репликатор и т. п.).
Механика: жизненный цикл и контракт роли
Модуль роли возвращает таблицу с заранее оговоренными полями-функциями. Минимальный контракт - три функции:
Код: Выделить всё
return {
validate = function(cfg) --[[ проверка ]] end,
apply = function(cfg) --[[ применение ]] end,
stop = function() --[[ остановка ]] end,
}
Жизненный цикл роли при каждой загрузке/перезагрузке конфига проходит фазы строго в таком порядке:
- 1. Loading (загрузка) - все роли грузятся в порядке, указанном в конфиге; выполняется код верхнего уровня модуля (инициализация). Срабатывает при включении роли или рестарте инстанса. Роль не стартует, если в конфиге нет её зависимостей.
- 2. Stopping (остановка) - при reload вызываются stop() тех ролей, что убраны из конфига. ВСЕ stop() выполняются ДО любых validate() и apply(): сперва глушим старое, потом поднимаем новое.
- 3. Validate (валидация) - для каждой роли вызывается validate(cfg) в порядке из конфига. Любая ошибка останавливает применение конфига целиком.
- 4. Apply (применение) - вызывается apply(cfg) для каждой роли. apply гарантированно идёт после того, как validate отработал для ВСЕХ включённых ролей.

Роли, их жизненный цикл и приложение в Tarantool 3.x
Все функции роли сообщают о неустранимой ошибке через throw (error). Ошибка ловится и показывается в config:info() в секции alerts. Аргумент cfg в validate/apply - это конфигурация роли из roles_cfg.<имя_роли>. Чтобы достать значения вне этой секции, используют config:get().
Зависимости и порядок
Поле dependencies перечисляет роли, без которых данная роль не стартует. Тонкость: зависимости НЕ влияют на порядок загрузки (loading), но влияют на порядок выполнения validate/apply/stop. Для дерева зависимостей сначала отрабатывают листья. Пусть role3 -> role4 -> role5 (стрелка - "зависит от"), а конфиг такой:
Код: Выделить всё
roles: [ role1, role2, role3, role4, role5 ]
Колбэк on_event (с 3.3.1)
on_event(config, key, value) вызывается каждый раз при широковещании системного события box.status. Аргумент key равен config.apply (триггер - обновление конфига) либо box.status (системное событие); value несёт статус инстанса, включая is_ro. Это правильное место для создания спейсов: схему можно менять только на read-write инстансе, а on_event даёт надёжный сигнал смены роли RW/RO. Все on_event с key=config.apply выполняются как часть процесса конфигурации (статусы ready/check_warnings достигаются только после них), и каждый колбэк обёрнут в pcall - ошибка логируется на уровне error, но не рушит остальные.
Код: роль, конфиг и приложение
Простая роль greeter без своего конфига и её включение:
Код: Выделить всё
-- greeter.lua
local log = require('log')
return {
validate = function() end,
apply = function() log.info('Hi from greeter') end,
stop = function() log.info('greeter stopped') end,
}
Код: Выделить всё
# config.yaml
groups:
group001:
replicasets:
replicaset001:
instances:
instance001:
roles: [ greeter ]
Код: Выделить всё
local schema = require('experimental.config.utils.schema')
local greeter_schema = schema.new('greeter', schema.record({
greeting = schema.scalar({ type = 'string' }),
}))
local function validate(cfg) greeter_schema:validate(cfg) end
local function apply(cfg) require('log').info(cfg.greeting) end
local function stop() end
return { validate = validate, apply = apply, stop = stop }
Код: Выделить всё
roles: [ greeter ]
roles_cfg:
greeter:
greeting: 'Hello from config'
Код: Выделить всё
on_event = function(config, key, value)
if value.is_ro then return end
box.schema.space.create(config.space_name or 'events',
{ if_not_exists = true })
end
Код: Выделить всё
app:
file: 'myapp.lua' # ИЛИ module: 'myapp'
cfg:
feature_x: true
Частые заблуждения и грабли
- Роль 3.x - это не роль Cartridge. Контракты разные (нет init/validate_config/apply_config из Cartridge), нет mixin'ов hot-reload Cartridge. Это новый, более простой механизм поверх декларативного конфига.
- Не путать с другими "ролями". Есть роль доступа (контейнер привилегий для users) и sharding-роль репликасета (storage/router в vshard). Application role - третья сущность.
- validate/apply вызываются ВСЕГДА при reload, даже если конфиг роли не менялся. Не закладывайтесь на "вызовут только при изменении" - делайте apply идемпотентным.
- apply не вызывается при смене RO->RW, когда replication.failover = election или supervised. Если логика зависит от RW (создание спейсов), не кладите её только в apply - используйте box.watch('box.status', ...) или on_event.
- Изменили САМ код роли (поля, зависимости) - нужен рестарт инстанса; на лету подхватывается только смена набора ролей и их roles_cfg, но не структура модуля.
- Зависимость должна быть явно в roles. Роль с dependencies не стартует, если зависимостей нет в списке roles - их надо перечислить.
- Создание спейса в apply без if_not_exists = true упадёт при втором reload. Всегда идемпотентность плюс проверка box.info.ro.
Мини-лабаПравило большой кнопки: роль = переключаемый модуль с жизненным циклом и зависимостями; приложение = монолитный entrypoint, грузится один раз после ролей. Тяжёлую переключаемую логику оформляйте ролью, общий старт сервиса - приложением.
Создайте кастомную роль counter, которая на apply пишет в лог, сколько раз её применили за время жизни процесса (используйте upvalue-счётчик на уровне модуля), а на stop логирует финальное значение. Включите её в config.yaml для одного инстанса, запустите tt start, затем выполните config:reload() (или измените roles_cfg и сделайте reload) и убедитесь по логам, что счётчик растёт, а код верхнего уровня модуля (loading) выполнился лишь один раз.
Контрольные вопросы
- 1. В каком порядке движок выполняет stop, validate и apply при перезагрузке конфига и почему все stop() идут первыми?
- 2. Чем порядок loading отличается от порядка validate/apply для дерева зависимостей role3 -> role4 -> role5?
- 3. Почему создание спейса нельзя надёжно делать только в apply() и какие два механизма решают проблему RW-режима?
- 4. Чем приложение (секция app) отличается от роли по моменту загрузки и наличию жизненного цикла?