You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

310 lines
13 KiB

package comm
import (
"errors"
"fmt"
"sync"
"yunyan/lego/sys/log"
"yunyan/lego/sys/mysql"
)
// 业务功能配置的「热重载」——console 后台改完配置广播事件,业务服务据此重读业务库、
// 热替换对应的 sys 客户端,免去重启。
//
// 能不能热更取决于该 sys 持有什么:
// - 凭据类客户端(email/sms/…):只是拿着 key/密码的 HTTP/SMTP 客户端,可整体替换 → applied
// - 未注册热重载函数的模块:如实回 restart_required,绝不谎报成功
// - 重载失败:保留旧客户端与旧配置继续服务 → failed
//
// 与 LoadOrSeedModuleConfig 的关键差别:本函数**只读覆盖、绝不 seed**——
// 运行时不该因为某模块库里没配就把 yaml 默认值写进库。
var (
reloaderMu sync.RWMutex
// reloaders SysKey → 热重载函数。由业务服务启动时 RegisterModuleReloader 注册。
reloaders = map[string]func(map[string]interface{}) error{}
)
// RegisterModuleReloader 注册某 SysKey(见 ModuleDef.SysKey) 的热重载函数。
// 约定:实现必须「校验失败返回 error 且保留旧客户端」,绝不 panic——
// 后台一次误保存不该打挂线上进程。
func RegisterModuleReloader(sysKey string, fn func(map[string]interface{}) error) {
reloaderMu.Lock()
reloaders[sysKey] = fn
reloaderMu.Unlock()
}
func lookupReloader(sysKey string) (func(map[string]interface{}) error, bool) {
reloaderMu.RLock()
defer reloaderMu.RUnlock()
fn, ok := reloaders[sysKey]
return fn, ok
}
// HasModuleReloaders 本服务是否持有任何可热重载的业务凭据客户端。
// gateway/timer/mcp 一个都不注册——它们收到下发时如实回「本服务不承载业务配置」,而非报错。
func HasModuleReloaders() bool {
reloaderMu.RLock()
defer reloaderMu.RUnlock()
return len(reloaders) > 0
}
// ───────── 集群内下发的 rpc 载荷(rpcx 用 MsgPack 序列化,普通 struct 即可,无需 pb) ─────────
// ReloadModuleConfigReq api 服务 → 各业务服务:请重载业务功能配置。
type ReloadModuleConfigReq struct {
Reason string // 触发原因(save / reset),仅用于日志
}
// ResetModuleConfigReq api 服务 → 配置属主(home):把某模块还原为你 yaml 里的初始值。
type ResetModuleConfigReq struct {
Module string
}
// ModuleConfigResp 单个服务对下发的应答。Err 非空表示该服务处理失败——
// 用字段而非 rpc error 承载,好让 api 汇总时能区分「某个服务失败」与「整体调用失败」。
type ModuleConfigResp struct {
Service string // 服务名(home/api…)
Instance string // <服务名>@<主机名>
Skipped bool // 本服务不承载业务配置,什么也没做
Results []ModuleApplyResult // 逐模块三态
Err string
}
// reloadContext 执行一次重载所需的全部依赖,由业务服务启动时登记。
// 有了它,HTTP 重载接口(处在某个业务模块里)不必再层层拿到服务的 Settings 与库句柄。
type reloadContext struct {
db mysql.ISys
encKey string
sys map[string]map[string]interface{}
}
var (
reloadCtxMu sync.RWMutex
reloadCtx *reloadContext
)
// SetModuleReloadContext 登记重载上下文(业务服务启动、各 sys OnInit 之后调用)。
func SetModuleReloadContext(db mysql.ISys, encKey string, sys map[string]map[string]interface{}) {
reloadCtxMu.Lock()
reloadCtx = &reloadContext{db: db, encKey: encKey, sys: sys}
reloadCtxMu.Unlock()
}
// DoReloadModuleConfig 用已登记的上下文执行一次业务配置热重载。
// 未登记(本服务不承载业务配置)时返回 error,由调用方如实上报,绝不假装成功。
func DoReloadModuleConfig() ([]ModuleApplyResult, error) {
reloadCtxMu.RLock()
ctx := reloadCtx
reloadCtxMu.RUnlock()
if ctx == nil {
return nil, errors.New("本服务未登记业务配置重载上下文")
}
return ReloadModuleConfig(ctx.db, ctx.encKey, ctx.sys), nil
}
// ───────────────────────── 重置为「配置文件初始值」 ─────────────────────────
//
// 业务库里的配置被改脏、又没有备份时的兜底:把某模块整体还原成本服务 confs/*.yaml 里的初始值。
//
// 数据来源只能是「启动时、库值覆盖之前」抓的 yaml 快照——一旦 LoadOrSeedModuleConfig 跑完,
// 内存里的 Sys 就已经是库值了,那时再读就是把脏数据又写回去。
var (
fileCfgMu sync.RWMutex
fileCfg map[string]map[string]interface{} // SysKey → yaml 原值副本
)
// snapshotFileModuleConfig 深拷贝各模块的 yaml 原值。由 LoadOrSeedModuleConfig 在覆盖 Sys 之前调用。
func snapshotFileModuleConfig(sys map[string]map[string]interface{}) {
snap := make(map[string]map[string]interface{}, len(ModuleCatalog))
for i := range ModuleCatalog {
sec := sys[ModuleCatalog[i].SysKey]
if sec == nil {
continue
}
cp := make(map[string]interface{}, len(sec))
for k, v := range sec {
cp[k] = v
}
snap[ModuleCatalog[i].SysKey] = cp
}
fileCfgMu.Lock()
fileCfg = snap
fileCfgMu.Unlock()
}
func fileSnapshot(sysKey string) map[string]interface{} {
fileCfgMu.RLock()
defer fileCfgMu.RUnlock()
return fileCfg[sysKey]
}
// DoResetModuleConfigToFile 把某模块在业务库里的配置整体还原为本服务配置文件(yaml)的初始值。
// yaml 里没有该模块(或本服务未登记上下文)时**不做任何改动**并返回 error——
// 否则会把该模块清空后无值可填,比"脏数据"更糟。
func DoResetModuleConfigToFile(module string) ([]ModuleApplyResult, error) {
ctx := currentReloadCtx()
if ctx == nil {
return nil, errors.New("本服务未登记业务配置重载上下文")
}
def := FindModuleDef(module)
if def == nil {
return nil, fmt.Errorf("未知模块: %s", module)
}
if len(fileSnapshot(def.SysKey)) == 0 {
return nil, fmt.Errorf("本服务的配置文件里没有 %s(%s) 的配置,无法还原", def.Name, def.Module)
}
res, err := resetOneModuleToFile(ctx, def)
if err != nil {
return nil, err
}
return []ModuleApplyResult{res}, nil
}
// DoResetAllModuleConfigToFile 把业务库里**所有**模块的配置整体还原为配置文件(yaml)初始值——
// 用于升级/迁移把库写脏后的整体恢复。只处理 yaml 里确有配置的模块(快照非空);
// yaml 未定义的模块保持不动(避免清空后无值可填)。
//
// 单个模块失败不中断其它模块:把失败作为该模块的 result 收集,尽最大努力恢复整库。
// ⚠️ 破坏性:会覆盖后台手工保存过的值(如把 email 打回 yaml 的默认发信配置),调用方须二次确认。
func DoResetAllModuleConfigToFile() ([]ModuleApplyResult, error) {
ctx := currentReloadCtx()
if ctx == nil {
return nil, errors.New("本服务未登记业务配置重载上下文")
}
results := make([]ModuleApplyResult, 0, len(ModuleCatalog))
for i := range ModuleCatalog {
def := &ModuleCatalog[i]
if len(fileSnapshot(def.SysKey)) == 0 {
continue // yaml 没定义这个模块 → 不动它
}
res, err := resetOneModuleToFile(ctx, def)
if err != nil {
log.Errorf("[ModuleReset] 全量重置中 %s 失败: %v", def.Module, err)
res = ModuleApplyResult{Module: def.Module, Status: ApplyStatusFailed, Err: err.Error()}
}
results = append(results, res)
}
if len(results) == 0 {
return nil, errors.New("配置文件里没有任何业务模块配置,无法还原")
}
log.Infof("[ModuleReset] 已全量还原为配置文件初始值 modules=%d", len(results))
return results, nil
}
func currentReloadCtx() *reloadContext {
reloadCtxMu.RLock()
defer reloadCtxMu.RUnlock()
return reloadCtx
}
// resetOneModuleToFile 删该模块全部行 → 按 yaml 快照重写(密钥加密、带时间戳) → 覆盖 Sys → 触发热重载。
// 调用方已保证 fileSnapshot(def.SysKey) 非空。
func resetOneModuleToFile(ctx *reloadContext, def *ModuleDef) (ModuleApplyResult, error) {
snap := fileSnapshot(def.SysKey)
if err := ctx.db.Delete(TableAppModuleConfig, "module=?", def.Module); err != nil {
return ModuleApplyResult{}, fmt.Errorf("清除 %s 旧配置失败: %w", def.Module, err)
}
if err := seedModuleRows(ctx.db, ctx.encKey, def, snap); err != nil {
return ModuleApplyResult{}, fmt.Errorf("写入 %s 配置文件初始值失败(该模块配置现为空,请重试): %w", def.Module, err)
}
// Sys 与库保持一致,再让客户端切到初始值。
next := make(map[string]interface{}, len(snap))
for k, v := range snap {
next[k] = v
}
ctx.sys[def.SysKey] = next
res := ModuleApplyResult{Module: def.Module, Status: ApplyStatusRestartRequired}
if fn, ok := lookupReloader(def.SysKey); ok {
if err := fn(next); err != nil {
log.Errorf("[ModuleReset] 还原后热重载失败 module=%s err=%v", def.Module, err)
res = ModuleApplyResult{Module: def.Module, Status: ApplyStatusFailed, Err: err.Error()}
} else {
res = ModuleApplyResult{Module: def.Module, Status: ApplyStatusApplied}
}
}
log.Infof("[ModuleReset] 已还原为配置文件初始值 module=%s status=%s", def.Module, res.Status)
return res, nil
}
// ReloadModuleConfig 重读业务库 app_module_config,对每个「库里有配置」的模块尝试热重载。
//
// db 应用业务库(与 LoadOrSeedModuleConfig 同一个)
// encKey ${FIELD_ENCRYPT_KEY},须与 console 一致
// sys GetSettings().Sys(map 引用);仅在该模块重载成功/待重启时才提交新值
//
// 返回每个模块的结果,供上层组装 ConfigApplyAck 回给 console。
// 单个模块失败不影响其它模块,也不会改动它自己原有的客户端。
func ReloadModuleConfig(db mysql.ISys, encKey string, sys map[string]map[string]interface{}) []ModuleApplyResult {
results := make([]ModuleApplyResult, 0, len(ModuleCatalog))
if db == nil {
return append(results, ModuleApplyResult{Module: "*", Status: ApplyStatusFailed, Err: "业务库未连接"})
}
rows := make([]*AppModuleConfig, 0)
if err := db.Find(TableAppModuleConfig, &rows, ""); err != nil {
return append(results, ModuleApplyResult{Module: "*", Status: ApplyStatusFailed, Err: "读取业务配置失败: " + err.Error()})
}
byModule := make(map[string][]*AppModuleConfig)
for _, r := range rows {
byModule[r.Module] = append(byModule[r.Module], r)
}
for i := range ModuleCatalog {
def := &ModuleCatalog[i]
existing := byModule[def.Module]
if len(existing) == 0 {
continue // 库里没这个模块的配置 → 不动它(也不 seed)
}
// 先在副本上组装新配置:重载失败时 sys 保持原样,避免留下一份「生效不了的」脏配置。
cur := sys[def.SysKey]
next := make(map[string]interface{}, len(cur)+len(existing))
for k, v := range cur {
next[k] = v
}
// **fail-closed,且以模块为原子**:只要本模块有任一密钥字段解不开,就整模块放弃重载。
// 半新半旧的凭据组合比原地不动更危险;而把解不开的密文当明文塞进 next,等于让
// email.Reload 拿密文当 SMTP 密码、让 wechatpay 拿密文当私钥路径去 open——
// 后者正是 home 进程被 log.Fatal 杀死、5-in-1 容器 crash loop、全站 502 的那条路。
decryptFailed := 0
for _, r := range existing {
val := r.Value
if r.Encrypted && val != "" {
plain, e := Decrypt(encKey, val)
if e != nil {
decryptFailed++
log.Errorf("[ModuleReload] 密钥字段解密失败 module=%s key=%s err=%v 本服务key指纹=%s %s",
r.Module, r.Key, e, KeyFingerprint(encKey), DiagnoseDecryptFailure(encKey, r.Value))
continue
}
val = plain
}
next[r.Key] = coerceVal(val, cur[r.Key])
}
if decryptFailed > 0 {
results = append(results, ModuleApplyResult{Module: def.Module, Status: ApplyStatusFailed,
Err: fmt.Sprintf("%d 个密钥字段解密失败,已跳过本模块重载(继续使用原有配置)——见本服务日志里的 key 指纹与诊断", decryptFailed)})
continue
}
fn, ok := lookupReloader(def.SysKey)
if !ok {
sys[def.SysKey] = next // 下次重启即生效
results = append(results, ModuleApplyResult{Module: def.Module, Status: ApplyStatusRestartRequired})
continue
}
if err := fn(next); err != nil {
log.Errorf("[ModuleReload] 模块热重载失败 module=%s err=%v(已保留旧配置继续服务)", def.Module, err)
results = append(results, ModuleApplyResult{Module: def.Module, Status: ApplyStatusFailed, Err: err.Error()})
continue
}
sys[def.SysKey] = next
log.Infof("[ModuleReload] 模块热重载成功 module=%s", def.Module)
results = append(results, ModuleApplyResult{Module: def.Module, Status: ApplyStatusApplied})
}
return results
}