Files
knowledge-governance/internal/httpapi/library_executor.go
T
姚章浩 47eb23cf68 feat(library): 封存执行器可用性与还原条件来自真实观测
页面上的"可发起封存"原来只判 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 带原因;重启后恢复。
2026-10-29 13:01:37 +08:00

78 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}