Tarantool это не просто хранилище, а полноценный сервер приложений: тот же процесс, что держит данные в памяти, исполняет ваш код на Lua. Поэтому хранимая процедура в Tarantool это обычная Lua-функция, которая живет в адресном пространстве сервера и работает с данными напрямую, без сетевого round-trip к БД. Вызов такой функции по сети называется RPC поверх протокола iproto.
В этом уроке разберем три вещи, которые часто путают: (1) модуль приложения (Lua-файл с кодом), (2) объект box.func (метаданные функции, зарегистрированной в схеме БД), (3) механику вызова функции локально и удаленно. Понимание границы между ними и есть ключ к правильной организации приложения.
Механика: где живет код и где живет имя функции
Важно с самого начала развести два мира.
Мир Lua-рантайма. Когда вы пишете [c]local m = require('mymodule')[/c], Lua грузит файл [c]mymodule.lua[/c], исполняет его, и результат (обычно таблица с функциями) кешируется в таблице [c]package.loaded['mymodule'][/c]. Повторный [c]require[/c] вернет тот же закешированный объект, файл заново не читается. Пути поиска берутся из [c]package.path[/c] (для .lua) и [c]package.cpath[/c] (для .so). Эти функции существуют только в памяти процесса и нигде не персистятся.
Мир схемы БД. Параллельно есть системный спейс _func, в котором хранятся зарегистрированные функции. Регистрация делается через [c]box.schema.func.create()[/c]. Зарегистрированная функция получает запись в [c]_func[/c] и становится доступна как объект [c]box.func.ИМЯ[/c]. Главное отличие от просто Lua-функции: на функцию из [c]_func[/c] можно выдавать привилегии (grant execute), и ее можно вызвать по сети через iproto.
Эти миры пересекаются двумя способами. Первый: вы регистрируете в схеме имя глобальной Lua-функции (без тела) - тогда [c]_func[/c] хранит только метаданные и права, а сам код берется из Lua-рантайма по этому имени. Второй: вы передаете тело функции в опции body - тогда исходник функции сохраняется прямо в снапшоте БД. Такая функция называется персистентной: после рестарта сервера она восстановится из снапшота, ее не нужно заново определять в коде приложения.

Путь от модуля к box.func и вызову
Что значит "персистентная" под капотом. Строка с телом функции пишется в [c]_func[/c], попадает в снапшот и WAL, реплицируется на реплики. При старте Tarantool компилирует это тело обратно в Lua-функцию. Отсюда два следствия: персистентная функция автоматически появляется на всех репликах, и ее можно сделать sandboxed (детерминированной, без доступа к глобальным переменным) - именно такие функции разрешено использовать в функциональных индексах и ограничениях (constraints).
Ключевые команды и код
1) Модуль приложения (переиспользуемый код).
Код: Выделить всё
-- mymodule.lua
local M = {}
function M.greet(name)
return 'Hello, ' .. name
end
return M
Код: Выделить всё
-- использование
local mymodule = require('mymodule')
print(mymodule.greet('Tarantool'))
-- при необходимости расширить пути поиска ДО require:
-- package.path = 'scripts/?.lua;' .. package.path
Код: Выделить всё
function add_player(name) -- глобальная Lua-функция
return box.space.players:insert({name})
end
box.schema.func.create('add_player') -- запись в _func
box.schema.user.grant('guest', 'execute',
'function', 'add_player') -- право на вызов
Код: Выделить всё
local code = [[function(a, b) return a + b end]]
box.schema.func.create('sum', {body = code})
box.func.sum -- посмотреть метаданные (id, language, body...)
box.func.sum:call({1, 2}) -- -> 3
4) Удаленный вызов через net.box.
Код: Выделить всё
local netbox = require('net.box')
local conn = netbox.connect('127.0.0.1:3301')
conn:call('sum', {2, 3}) -- -> 5, выполнится на сервере
Код: Выделить всё
способ что вызывает по сети нужен _func/grant
----------------------- -------------------- -------- -----------------
m.greet(x) локальную Lua-функ. нет нет
box.func.NAME:call{...} функцию из схемы нет да (создана)
conn:call('NAME',{...}) функцию на сервере да да + grant execute
box.schema.func.create регистрирует имя - создает запись
Частые заблуждения и грабли
Заблуждение: "создал box.schema.func.create - значит код сохранился". Нет. Без опции body в _func попадает только ИМЯ и права. Сам код по-прежнему живет в Lua-рантайме и пропадет после рестарта, если вы не определите функцию заново в стартовом скрипте.
- require кеширует. Поправили .lua-файл - повторный [c]require[/c] вернет старую версию из [c]package.loaded[/c]. Для горячей перезагрузки модуля делают [c]package.loaded['mymodule'] = nil[/c] и затем снова [c]require[/c]. Это и есть основа hot-reload без рестарта.
- :call принимает массив. [c]box.func.sum:call({1, 2})[/c], а не [c]:call(1, 2)[/c]. Частая ошибка новичков.
- Аргументы и результат сериализуются в MsgPack. По сети нельзя передать Lua-функцию, метатаблицу или замыкание - только данные. Для горячего пути есть опция [c]takes_raw_args=true[/c]: аргументы приходят MsgPack-объектом без декодирования в Lua.
- setuid работает только через бинарный порт. В консоли и в Lua-скрипте опция игнорируется - права считаются по текущему пользователю.
- Долгий код в функции блокирует поток. Tarantool кооперативно-многозадачный: функция выполняется в фибере на одном TX-потоке. Тяжелые циклы без yield (или без обращений к БД, которые делают yield сами) подвесят весь инстанс.
- Глобальные функции засоряют namespace. В 3.x чистый способ организации - не плодить глобальные функции, а оформлять логику как роль приложения: Lua-модуль, возвращающий объект с методами [c]validate[/c], [c]apply[/c], [c]stop[/c]. Роль включается декларативно в YAML-конфиге и перезагружается без рестарта инстанса.
Мини-лаба
- Запустите Tarantool, выполните [c]box.cfg{listen=3301}[/c]. Создайте персистентную функцию [c]mul[/c] с телом [c]function(a,b) return a*b end[/c]. Проверьте [c]box.func.mul:call({6,7})[/c]. Затем посмотрите запись в схеме: [c]box.space._func.index.name:select{'mul'}[/c] и убедитесь, что тело функции лежит прямо в кортеже. Бонус: выдайте [c]guest[/c] право execute и вызовите [c]mul[/c] из второй сессии через [c]net.box[/c].
- Чем запись в спейсе _func отличается от просто глобальной Lua-функции в [c]package.loaded[/c]? Что из них переживет рестарт сервера и почему?
- Что именно делает опция body в [c]box.schema.func.create[/c] и где физически оказывается код функции?
- Почему [c]box.func.sum:call({1,2})[/c] пишут с фигурными скобками, и в каком формате аргументы уходят при вызове через net.box?
- Как горячо перезагрузить Lua-модуль без рестарта инстанса и почему обычный повторный [c]require[/c] не подхватит правки в файле?