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 带原因;重启后恢复。
123 lines
4.7 KiB
Go
123 lines
4.7 KiB
Go
// Package libraryexecutor 回答一个问题:主机侧封存执行器现在是不是真的可用,
|
||
// 而且是不是指向我们这个实例。
|
||
//
|
||
// 为什么不能在页面里读一个配置字符串了事:正式栈曾经出现过"agent 顶着正式单元名
|
||
// 却在冻结候选实例"的事故——配置非空、页面显示可发起、真发起却封错了库。可用的
|
||
// 定义必须是**运行时观测**:socket 通、执行器自报的实例与期望一致、没有降级。
|
||
package libraryexecutor
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"time"
|
||
|
||
"knowledge-governance/internal/libraryagent"
|
||
)
|
||
|
||
// Identity 是执行器自报的身份:它要冻结哪个容器的写入者、归档写到哪。
|
||
// 只有容器名与路径,没有任何凭据。
|
||
type Identity struct {
|
||
Project string `json:"project"`
|
||
ArchiveRoot string `json:"archive_root"`
|
||
JournalRoot string `json:"journal_root"`
|
||
SourceApp string `json:"source_app"`
|
||
SourcePG string `json:"source_pg"`
|
||
SourceES string `json:"source_es"`
|
||
Writers string `json:"writers"`
|
||
SourceS3Endpoint string `json:"source_s3_endpoint,omitempty"`
|
||
}
|
||
|
||
// Observation 是一次真实探测的结果,会被落库供页面读取。
|
||
type Observation struct {
|
||
Instance string `json:"instance"`
|
||
Reachable bool `json:"reachable"`
|
||
Busy bool `json:"busy"`
|
||
Degraded string `json:"degraded,omitempty"`
|
||
Identity Identity `json:"identity"`
|
||
Error string `json:"error,omitempty"`
|
||
ObservedAt time.Time `json:"observed_at"`
|
||
}
|
||
|
||
// Expectation 是治理侧对执行器的要求。ArchiveRoot 不参与判定:容器里的路径是
|
||
// 挂载后的路径,和宿主机上的真实路径本来就不是一个字符串;用它当门禁只会造成
|
||
// 永久误报,所以只作为观测值展示给运维比对。
|
||
type Expectation struct {
|
||
Instance string
|
||
Project string
|
||
ArchiveRoot string
|
||
}
|
||
|
||
// Probe 用一次短超时的 health 往返取回执行器身份。它不跑子进程、不碰源库,
|
||
// 所以可以当心跳反复调用。instance 是"这次观测属于哪个实例",由调用方的
|
||
// LIBRARY_TARGET_INSTANCE 决定。
|
||
func Probe(ctx context.Context, instance, socket string, timeout time.Duration, now func() time.Time) Observation {
|
||
if now == nil {
|
||
now = time.Now
|
||
}
|
||
observation := Observation{Instance: instance, ObservedAt: now().UTC()}
|
||
if socket == "" {
|
||
observation.Error = "未配置主机侧封存 agent socket(LIBRARY_AGENT_SOCKET)"
|
||
return observation
|
||
}
|
||
if timeout <= 0 {
|
||
timeout = 3 * time.Second
|
||
}
|
||
result, err := libraryagent.Health(ctx, libraryagent.ClientConfig{Socket: socket, Timeout: timeout})
|
||
if err != nil {
|
||
observation.Error = err.Error()
|
||
return observation
|
||
}
|
||
var payload struct {
|
||
Identity
|
||
Busy bool `json:"busy"`
|
||
Degraded string `json:"degraded"`
|
||
}
|
||
if err = json.Unmarshal(result.Payload, &payload); err != nil {
|
||
observation.Error = "执行器健康应答无法解析"
|
||
return observation
|
||
}
|
||
observation.Reachable = true
|
||
observation.Busy = payload.Busy
|
||
observation.Degraded = payload.Degraded
|
||
observation.Identity = payload.Identity
|
||
return observation
|
||
}
|
||
|
||
// Evaluate 把"观测 + 期望 + 新鲜度"换算成页面上的可用性与原因。
|
||
// 返回的原因必须能直接告诉运维该去改什么,而不是"不可用"三个字。
|
||
func Evaluate(observation Observation, want Expectation, maxAge time.Duration, now time.Time) (bool, string) {
|
||
if want.Instance == "" {
|
||
return false, "未配置封存来源实例身份(LIBRARY_SOURCE_INSTANCE),无法确认执行器归属"
|
||
}
|
||
if observation.Instance != want.Instance {
|
||
return false, fmt.Sprintf("执行器状态来自另一个实例(%q),不是 %q", observation.Instance, want.Instance)
|
||
}
|
||
age := now.Sub(observation.ObservedAt)
|
||
if maxAge > 0 && age > maxAge {
|
||
return false, fmt.Sprintf("执行器状态已过期:%d 秒前的观测,上限 %d 秒(worker 未上报?)",
|
||
int(age.Seconds()), int(maxAge.Seconds()))
|
||
}
|
||
if !observation.Reachable {
|
||
reason := "主机侧封存执行器不可达"
|
||
if observation.Error != "" {
|
||
reason += ":" + observation.Error
|
||
}
|
||
return false, reason
|
||
}
|
||
if want.Project == "" {
|
||
return false, "未配置执行器源身份(LIBRARY_SOURCE_PROJECT),无法核对 agent 指向哪个实例"
|
||
}
|
||
if observation.Identity.Project != want.Project {
|
||
return false, fmt.Sprintf("执行器指向的实例不匹配:agent=%q,期望=%q",
|
||
observation.Identity.Project, want.Project)
|
||
}
|
||
if observation.Degraded != "" {
|
||
return false, "执行器处于降级状态(上次恢复失败):" + observation.Degraded
|
||
}
|
||
if observation.Busy {
|
||
return false, "执行器正在执行另一个封存/恢复,等它结束再发起"
|
||
}
|
||
return true, ""
|
||
}
|