Closed rosik closed 3 years ago
Первым шагом мы решили реализовать что-то типа require('cartridge.reload').reloade_roles
. Она будет релоадитиь только роли
This is the first iteration of hot-reload support development. At this point we'll make only cartridge roles reloadable. Later we'll come to the full dofile('init.lua')
.
Cartride will provide a new API:
require('cartridge').reload_roles({rollback_on_error = true/false})
What it will do:
Unload all roles. As you may know, cartridge roles are loaded recursively: if your init.lua
calls cartridge.cfg({roles = 'role-a'})
and role-a.lua
specifies dependencies = {'role-b'}
then both role-a
and role-b
will be unloaded. To "unload a role" means package.loaded[role] = nil
.
Require all roles specified in cartridge.cfg
. Acoording to the example above cartridge will require role-a
, and if its dependencies have changed e.g. to role-c
, it'll require role-c
(but not role-b
) anymore.
Cartridge will update service registry that you usually access with service_get/set
functions. For every package name it'll check if the old role_name was initialized and, if so, nullify an old one and set a new one.
Note. Some roles keep state in Tarantool spaces and can't be stopped. For example vshard-storage
. Unregistering them is a bad idea.
collectgarbage()
It may sound complicated and I'm sure it is -- hot-reload is a really difficult task. But if you're looking for simplicity here are 3 simple rules for you:
role_name
;Reloading roles is possible in RolesConfigured
state only. During hot-reload cartridge will enter ReloadingRoles
state temporarily, and return to the RolesConfigured
state upon success. Cartridge won't call any roles callbacks, handling success and errors is up to the application developer.
Upon an error cartridge enters ReloadError
state and there are two options - leave everything as is (doomed), or try rolling back (doomed too). I think the rollback_on_error
option is enough to implement everything. All other actions (like os.exit in case of an error) should be implemented by application developer basing on the apllication needs. We suggest implementing something like
_G.reload = function()
local cartridge = require('cartridge')
local ok, err = cartridge.reload_roles()
if not ok then
require('log').error('Hot-reload failed, exiting')
os.exit()
end
cartridge.apply_config()
return true
end
Note. User can always do something terrible that'll curse the state and cartridge can't even detect it. Below you'll find guidelines on several popular tasks.
The most important thing is to update closures properly. Suppose your role exports some stored procedures in _G
like this:
local function update_balance() end
_G.update_balance = update_balance()
-- bad: polluting global namespace
It's slightly better to group all functions in a single table
local M = {}
function M.update_balance() end
_G.__mystorage = M
Пользователь изменяет код роли на фс и выполняет релоад. Состояние инстанса должно быть максимально похоже на то, как если бы вместо хот релоада перезапустили весь инстанс.
Что может измениться в коде роли:
Добавлены новые хранимки в _G
.
Измененн код хранимок в _G
.
Исчезли старые хранимки в _G
.
Добавлены новые хттп роуты.
Изменен код хттп роутов.
Исчезли старые хттп роуты.
Добавлены новые рекваеры.
Изменен код рекваерищихся модулей.
Исчезли старые рекваеры.
Изменился код, запущенный в файберах.
Файбер "стал ненужным".
_G
local function foo() end
_G.__mymodule_foo = foo
package.reload:register(function() _G.__mymodule_foo = nil end)
return {foo = foo}
local M = {}
function M.foo() end
_G.mymodule = M
return M
Нетбокс вызывает conn:call('mymodule.foo')
. О старых хранимках переживать не приходится. Если название модуля не использует точек, то можно вообще избежать вмешательства в _G
и делать conn:call('package.loaded.mymodule.foo')
.
Процесс сильно зависит от того, как хттп роуты изначально настраиваются. Это могут быть условно перманентные роуты или же роуты, настроенные в конфиге и применяемые на apply_config. Картриджу придётся поддерживать оба варианта сразу потому что уже сейчас есть роли и с тем и с другим подходом. И хранимок в _G
это тоже касается.
Всё как с _G
, только кода больше. Но его можно втащить на борт картриджа.
-- Delete a route
local index = httpd.iroutes[name]
table.remove(httpd.routes, index)
httpd.iroutes[name] = nil
-- Renumber remaining routes
for i = index, #httpd.routes do
local route = httpd.routes[i]
if route.name ~= nil then
httpd.iroutes[route.name] = i
end
end
Со стороны пользователя ничего лишнего делать не нужно, но:
_G
.
В
init.lua
пользователь обычно пишет что-то типаТарантул делает
dofile('init.lua')
и так стартует инстанс.Суть хотрелоада заключается в том, что мы разрешаем пользователю сделать
Что может измениться, и что мы вообще можем разрешить менять:
Я думаю в первую очередь надо сфокусироваться на релоаде ролей. В ролях в свою очередь тоже могут быть серьезные изменения:
Таким образом, список проблем следующий: