关于热更新一个模块功能 #1740
ericzhanchina
started this conversation in
General
关于热更新一个模块功能
#1740
Replies: 2 comments 7 replies
-
|
大概率是 codecache 问题。加载 local cache = require "skynet.codecache"
cache.mode "OFF" -- turn off codecache, because CMD.new may load data fileps: 理解清楚怎么回事再设置吧= =,免得把 codecache 都给关了。 |
Beta Was this translation helpful? Give feedback.
7 replies
-
|
实现了一个开发环境使用的简单版本,自己测试了下,还未作推广测试验证,问题肯定还有很多,欢迎大家提优化改进建议。 --[[
create by lj 2017-8-10
模块
nnhotfix
功能
热更新
支持两种模式:普通热更新(函数原型替换)和补丁代码(额外执行一段代码,通过nnhotfix.HOTFIX_FUN_NAME和nnhotfix.HOTFIX_FUN_VERSION机制实现)
说明
此模块目前仅能用于开发环境,原因除了下面描述的情况外,还有因对lua内部机制原理并未全面了解撑握及未经长期大范围测试验证其有效性的原因
此模块至少不适应于以下情况:
热更新时已经执行过的一次性代码(比如初始化、配置加载等)及其执行产生的效果(包括结果)无法使用本机制正常接口更新
热更新时类似fork类已经开始执行且仍未结束的代码(仅能更新其内部再次调用的子函数,而内嵌局部代码无法更新,这会造成新旧代码混杂产生不可预期的结果)
热更新仅替换函数类型值的原型,对于非函数类型值,除非原对象相应值为nil才会被更新为新值,否则会保持原始值,也即只更新代码,不更新状态(状态更新可以使用本模块已支持的额外补丁模式)
问题
关于热更新,有几个难题:
1、对于模块级更新来说,仅仅替换package.loaded值,只能更新后续新require的代码,对于已经在别的模块中require后作为模块局部变量引用的模块来说,仍然会使用旧代码:代理模式(使用元表)、函数替换模式、
2、对于当前正在执行且未执行完的函数,逻辑上好像无法热更新:理论上完美方案应该无解,但可在一些特殊情况做禁热更新锁标记,等待到仅在未锁时才执行热更新;
3、对于动态修改的函数,比如代码中包含根据配置对函数进行动态附值的情况,又如何实现热更新?:直接替换为新函数
热更失效的情况(热更前置条件):
在模块级别存在对全局对象操作,比如【function gdata.dispatch.req_create_role(sid, uid, msg_id, msg_body, msg_params)】(热更无法检测到对全局对象的修改从而无法使用热更机制)
函数被监控函数hook(无法将新函数和原函数的upvalue对应上)
TODO:
考虑回滚机制
可参考https://github.com/cloudwu/luareload优化完善
修改历史
2025/12/29 参考skynet的hotfix模块开始重新改造 by lj
]]
local nndebug = require("nndebug")
local nninspect = require("nninspect")
---@class nnhotfix
local M = {
HOTFIX_FUN_NAME = "HOTFIX_FUN_NAME", -- 热更新补丁代码函数名,可选
HOTFIX_FUN_VERSION = "HOTFIX_FUN_VERSION", -- 热更新补丁代码版本号(非模块版本号),可选
on_after_hotfix = "on_after_hotfix", -- 模块热更新后回调函数名,可选
}
-- 此部分为从skynet的hotfix中复制并修改的源码------------------------------------------
local function envid(f)
local i = 1
while true do
local name, value = debug.getupvalue(f, i)
if name == nil then
return
end
if name == "_ENV" then
return debug.upvalueid(f, i)
end
i = i + 1
end
end
local function gen_upvalue_key(inspect_path, name)
-- if "_ENV" == name then
-- return name
-- end
return nninspect.merge_name(inspect_path, name)
end
local function collect_uv(f , uv, env, inspect_path, seened)
seened = seened or {}
if seened[f] then
return
end
-- print("collect_uv", inspect_path)
seened[f] = true
local i = 1
while true do
local name, value = debug.getupvalue(f, i)
-- print(name, value)
if name == nil then
break
end
local id = debug.upvalueid(f, i)
local key = gen_upvalue_key(inspect_path, name)
if uv[key] then -- 不同函数中有同名upvalue值
assert(uv[key].id == id, string.format("ambiguity local value %s", key))
else
uv[key] = { func = f, index = i, id = id }
if type(value) == "function" then
-- if envid(value) == env then -- 仅收集共享同一环境的函数
collect_uv(value, uv, env, key, seened)
-- end
end
end
i = i + 1
end
end
local function collect_all_uv(func, inspect_path)
local uv = {}
collect_uv(func, uv, envid(func), inspect_path)
-- if not uv["_ENV"] then
-- uv["_ENV"] = {func = collect_uv, index = 1}
-- end
return uv
end
local function _patch(uv, f, seened, inspect_path)
seened = seened or {} -- for stack overflow
if seened[f] then
return
end
seened[f] = true
local i = 1
while true do
local name, value = debug.getupvalue(f, i)
if name == nil then
break
end
-- print("getupvalue", name)
-- if value then -- 新值为nil或false也要绑定旧值,比如db对象需要在初始化时才付值,热更时就需要绑定原值
local key = gen_upvalue_key(inspect_path, name)
local old_uv = uv[key] -- 监控代码包裹了一层 package/loaded/role.grole_info/HOTFIX_FUN_NAME/fun/value
-- gdata.once(name, key, uv, old_uv) -- , value)
if type(value) == "function" then -- 函数不要替换为旧值,仅函数中的半包非函数变量才替换为旧值
_patch(uv, value, seened, key)
if old_uv then
-- gdata.once("upvaluejoin修改旧函数绑定新的upvalue子函数", key, value, debug.upvalueid(f, i))
debug.upvaluejoin(old_uv.func, old_uv.index, f, i)
end
else -- 这里并没有对table类型进行递归处理
if old_uv then
-- gdata.once("upvaluejoin新函数绑定旧函数非函数值", key, debug.upvalueid(old_uv.func, old_uv.index))
debug.upvaluejoin(f, i, old_uv.func, old_uv.index)
end
end
-- end
i = i + 1
end
end
-- 递归模式将旧函数中对应upvalue替换到新函数中
function M.update_upvalue(old_func, new_func, seened, inspect_path)
-- print("start:", inspect_path)
local uv = {}
if "function" == type(old_func) then
uv = collect_all_uv(old_func, inspect_path)
-- nndebug.print(uv)
-- os.exit(1)
_patch(uv, new_func, seened, inspect_path)
end
return new_func
end
-- 此部分为从skynet的hotfix中复制并修改的源码------------------------------------------
function M.update_value(old_value, new_value, inspect_path)
-- nndebug.print(old_value, new_value, inspect_path)
local seened = {}
local t = type(old_value)
if (t == type(new_value)) then
if "function" == t then
M.update_upvalue(old_value, new_value, seened, inspect_path)
elseif "table" == t then
-- 替换每个函数及原值为nil的值,注意:此方案并不符合所有情况,所以此功能仅能用作开发环境,线上环境需我们人工严格审核过逻辑才有可能ok
for k,v in pairs(new_value) do -- 这里并没有对table类型进行嵌套递归更新
if (nil == old_value[k]) or ("function" == type(v)) then -- 相当于替换了表/模块的成员函数和空值
old_value[k] = M.update_upvalue(old_value[k], v, seened, gen_upvalue_key(inspect_path, k))
-- gdata.once("set", gen_upvalue_key(inspect_path, k))
end
end
-- 处理元表
local old_meta = getmetatable(old_value)
local new_meta = getmetatable(new_value)
if new_meta and not old_meta then
setmetatable(old_value, new_meta)
end
end
elseif (nil == old_value) or (nil == new_value) then
-- do nothing
else
error("暂不支持类型不同的热更新")
end
return new_value
end
-- 这个应该是真正有实用价值的接口
-- 对于模块更新来说,如果直接付值新模块,会导致原模块中模块级非函数成员变量丢失,所以仍应使用旧模块,而只是替换其成员函数原型(需保持原函数的upvalue)
function M.hotfix_code(inspect_path, code, no_set_table_for_module)
local path, name = nninspect.split_inspect_path(inspect_path)
-- nndebug.print(inspect_path, path, name)
local parent, valid_path = nninspect.resolve_path_(path)
-- nndebug.print(parent, valid_path, name)
local t = type(parent)
assert(("function" == type(parent)) or ("table" == type(parent)),"暂仅支持父对象为表和函数类型的成员值更新")
local fun = load(code, "hotfix", "bt", _ENV)
local new_obj = fun()
-- nndebug.print("new_obj", new_obj)
local ret = nil
if t == "table" then
new_obj = M.update_value(parent[name], new_obj, inspect_path)
if not no_set_table_for_module then -- 对package.loaded不能替换,否则引起同时存在两份拷贝
-- gdata.once("set new_obj", name, new_obj)
parent[name] = new_obj
end
ret = true
elseif t == "function" then
local upvalue_index = nndebug.get_upvalue_index_by_name(parent, name)
if upvalue_index then
-- 这里如果为hotfix的话,可能应该考虑用debug.upvaluejoin更严密,因为如果新旧函数同时存在的话,就会产生两份数据从而产生不一至行为
-- nndebug.print("setupvalue", inspect_path)
debug.setupvalue(parent, upvalue_index, new_obj)
-- debug.upvaluejoin(f, i, old_uv.func, old_uv.index)
ret = true
end
-- ret = nil
else
-- 暂不支持其它类型设置子值
ret = false
end
return ret, new_obj
end
-- test M.hotfix_code
-- local function hello( ... )
-- print(...)
-- end
-- function call_hello( ... )
-- hello(...)
-- end
-- call_hello(1, 2, 3)
-- M.hotfix_code("call_hello/hello", [[return function( ... )
-- print(4, 5, 6)
-- end]])
-- call_hello(1, 2, 3)
-- for test M.hotfix_code
-- gdata = {dispatch = {}}
-- local value = 10
-- function M.show_value_()
-- value = value + 1
-- print("before", value)
-- end
-- function gdata.dispatch.req_create_role()
-- print("req_create_role", value)
-- end
-- function M.change_value_(new_value)
-- value = new_value
-- end
-- M.show_value_()
-- print("upvalueid M.show_value_", debug.upvalueid (M.show_value_, 2))
-- print("upvalueid req_create_role", debug.upvalueid (gdata.dispatch.req_create_role, 2))
-- _ENV.M = M
-- M.hotfix_code("M", [[
-- local M = {}
-- local value = 1
-- function M.show_value_()
-- value = value + 1
-- print("after", value)
-- end
-- function gdata.dispatch.req_create_role()
-- print("req_create_role", value)
-- end
-- function M.change_value_(new_value)
-- value = new_value
-- end
-- gdata.dispatch.req_create_role()
-- return M]])
-- -- M.change_value_(2)
-- M.show_value_()
-- gdata.dispatch.req_create_role()
-- print("upvalueid M.show_value_", debug.upvalueid (M.show_value_, 2))
-- print("upvalueid req_create_role", debug.upvalueid (gdata.dispatch.req_create_role, 2))
-- 这个接口很难做到完美,基本只能用于调试测试环境,因为风险太大
---@param old_require_module_path string 要更新模块的require路径参数,用【.】分隔路径名;如【"common.gconstant"】
---@param code string 新模块代码
function M.hotfix_module_by_code(old_require_module_path, code)
-- 热更新机制
local old_module = nninspect.get_module(old_require_module_path)
local old_hotfix_fun_version = old_module and old_module[M.HOTFIX_FUN_VERSION] or 0
local ret, new_module = M.hotfix_code(nninspect.gen_module_inspect_path(old_require_module_path), code, true)
if "table" == type(new_module) then
local new_hotfix_fun_version = new_module and new_module[M.HOTFIX_FUN_VERSION] or 0
-- gdata.once("old_hotfix_fun_version, new_hotfix_fun_version, old_module[M.HOTFIX_FUN_NAME]", old_hotfix_fun_version, new_hotfix_fun_version, old_module[M.HOTFIX_FUN_NAME])
if new_hotfix_fun_version > old_hotfix_fun_version and (old_module and ("function" == type(old_module[M.HOTFIX_FUN_NAME]))) then
-- nndebug.print("call ", M.HOTFIX_FUN_NAME)
old_module[M.HOTFIX_FUN_NAME]()
old_module[M.HOTFIX_FUN_VERSION] = new_hotfix_fun_version
end
if old_module and ("function" == type(old_module[M.on_after_hotfix])) then
old_module[M.on_after_hotfix]()
end
end
return ret, new_module
end
-- 这个接口很难做到完美,基本只能用于调试测试环境,因为风险太大
---@param old_require_module_path string 要更新模块的require路径参数,用【.】分隔路径名;如【"common.gconstant"】
---@param new_module_file_path_name ? string 可选参数,要使用的新代码模块,默认即为原始模块,用路径分隔符【/】分隔路径名,如【"common/gconstant.lua"】
function M.hotfix_module(old_require_module_path, new_module_file_path_name)
local code
new_module_file_path_name = new_module_file_path_name or package.searchpath(old_require_module_path, package.path)
if new_module_file_path_name then
local fp = io.open(new_module_file_path_name)
if fp then
code = fp:read('*all')
io.close(fp)
end
end
assert(code, "读热更新文件失败")
return M.hotfix_module_by_code(old_require_module_path, code)
end
-- for test
local total = 0
local function add(one, two)
return one + two
end
local function add_sum(one, two)
total = total + add(one, two)
return total
end
function M.test_hotfix_module( ... )
print("before", add_sum(1, 1))
M.hotfix_module("nnhotfix", "test/nnhotfix.lua")
print("after", add_sum(1, 1))
end
local function test( ... )
local total = 0
local function add(one, two)
return one + two
end
local function add_sum(one, two)
total = total + add(one, two)
return total
end
local new_code = [[
local total = 0
local function add(one, two)
return one + two + total
end
local function add_sum(one, two)
total = total + add(one, two) + 10
return total
end
return add_sum
]]
print("before add_sum", 1, 2, add_sum(1, 2))
add_sum = M.update_upvalue(add_sum, load(new_code)())
print("after add_sum", 1, 2, add_sum(1, 2))
end
-- test()
return M |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
服务器针对部分模块功能的热更新,我用的是云风提供的reload功能,比如有一个模块mod.lua,可以针对这个模块的方法进行热更新,上传一个mod_update.lua到服务器,执行热更新后生效,后续其他的协议都是按照mod_update的逻辑执行的,然后问题是,如果我再需要修改mod.lua的某个方法,同样上传mod_update.lua,执行热更新,就无效了,从日志看,新的代码并不会被执行,还是老的mod_update再执行,这个是什么问题呢?
Beta Was this translation helpful? Give feedback.
All reactions