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
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
|
|
}
|
|
|