47eb23cf68
页面上的"可发起封存"原来只判 LIBRARY_AGENT_SOCKET 非空:正式栈的 API 根本没挂
那个 socket,而 agent 曾顶着正式单元名冻结候选实例——配置非空、页面可点、真发起
却封错库。
现在:agent 新增 health 动作(不占封存操作锁、不跑子进程,只回答身份/降级/忙),
worker 每 30 秒探测并写入迁移 074 的 library_executor_status;overview、快照列表与
POST /library/captures 共用同一份判定(过期、不可达、实例不符、降级、正忙都拒绝并
给原因)。封存结果由 agent 附上自报身份,worker 在停写入者之前再探测一次、执行后
用结果复核,不一致就不登记测试封存——心跳只能证明"刚才"。
期望身份用 LIBRARY_SOURCE_*(封存源),不是 LIBRARY_TARGET_*(同步目标),否则
正确的执行器会被判成不匹配。另加 GET /library/restore-availability:恢复能否执行由
双闸、该快照的恢复授权与实例维护状态决定,页面不再写死 disabled。
真跑(候选隔离栈,两处 socket 隔离不变):overview 返回 executor
{kgmaint-weknora/kgmaint-app};停 agent 后转不可达并 503 带原因;重启后恢复。
78 lines
3.3 KiB
Go
78 lines
3.3 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"knowledge-governance/internal/libraryexecutor"
|
||
)
|
||
|
||
// libraryExecutorView 是"封存执行器到底能不能用"的页面口径。
|
||
//
|
||
// 为什么不是 `LIBRARY_AGENT_SOCKET != ""`:正式栈里 API 根本没有挂载那个 socket,
|
||
// 配置非空并不代表链路通;更危险的是 agent 可能顶着正式单元名却在冻结另一个实例。
|
||
// 这里只认 worker 写入的真实观测,并把观测时间与执行器自报的身份一起给出来。
|
||
type libraryExecutorView struct {
|
||
Available bool `json:"available"`
|
||
Reason string `json:"reason,omitempty"`
|
||
Instance string `json:"instance,omitempty"`
|
||
Project string `json:"project,omitempty"`
|
||
SourceApp string `json:"source_app,omitempty"`
|
||
ArchiveRoot string `json:"archive_root,omitempty"`
|
||
Busy bool `json:"busy"`
|
||
Degraded string `json:"degraded,omitempty"`
|
||
ObservedAt *time.Time `json:"observed_at,omitempty"`
|
||
}
|
||
|
||
type libraryExecutorStore interface {
|
||
LibraryExecutorStatus(ctx context.Context, instance string) (*libraryexecutor.Observation, error)
|
||
}
|
||
|
||
// libraryExecutorAvailability 把"配置里的期望"与"worker 观测到的真实身份"合起来判定:
|
||
// 观测不存在、过期、不可达、实例不符、降级、正忙,任何一条都不可用,并给出原因。
|
||
func libraryExecutorAvailability(ctx context.Context, config Config) (bool, string, libraryExecutorView) {
|
||
// 期望身份是**封存源**:agent 冻结的是源库写入者,与同步目标(LIBRARY_TARGET_*)
|
||
// 本来就是两套名字(本机候选栈:源 kgmaint、目标 kgseal)。
|
||
instance := config.LibrarySourceInstance
|
||
view := libraryExecutorView{Instance: instance}
|
||
if instance == "" {
|
||
return false, "未配置封存来源实例身份(LIBRARY_SOURCE_INSTANCE),无法确认执行器归属", view
|
||
}
|
||
store, ok := config.Store.(libraryExecutorStore)
|
||
if !ok {
|
||
return false, "执行器状态存储不可用", view
|
||
}
|
||
observation, err := store.LibraryExecutorStatus(ctx, instance)
|
||
if err != nil {
|
||
return false, "读取执行器状态失败:" + err.Error(), view
|
||
}
|
||
if observation == nil {
|
||
return false, "封存执行器还没有上报状态(worker 未运行,或实例身份与 worker 不一致)", view
|
||
}
|
||
available, reason := libraryexecutor.Evaluate(*observation,
|
||
libraryexecutor.Expectation{Instance: instance, Project: config.LibrarySourceProject,
|
||
ArchiveRoot: config.LibraryArchiveRoot},
|
||
executorStatusMaxAge(config), time.Now().UTC())
|
||
observedAt := observation.ObservedAt
|
||
view.Available = available
|
||
view.Reason = reason
|
||
view.Project = observation.Identity.Project
|
||
view.SourceApp = observation.Identity.SourceApp
|
||
view.ArchiveRoot = observation.Identity.ArchiveRoot
|
||
view.Busy = observation.Busy
|
||
view.Degraded = observation.Degraded
|
||
view.ObservedAt = &observedAt
|
||
return available, reason, view
|
||
}
|
||
|
||
// defaultLibraryExecutorStatusMaxAge 与 worker 的探测间隔(30s)配套:
|
||
// 没配上限绝不等于"永不过期"——那会把一次陈旧的"可用"永久留在页面上。
|
||
const defaultLibraryExecutorStatusMaxAge = 2 * time.Minute
|
||
|
||
func executorStatusMaxAge(config Config) time.Duration {
|
||
if config.LibraryExecutorStatusMaxAge > 0 {
|
||
return config.LibraryExecutorStatusMaxAge
|
||
}
|
||
return defaultLibraryExecutorStatusMaxAge
|
||
}
|