Files
knowledge-governance/internal/platform/postgres/library_executor_status.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

72 lines
2.7 KiB
Go

package postgres
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"knowledge-governance/internal/libraryexecutor"
)
// 为什么落库而不是 API 直接探测:agent socket 只挂在 worker 容器里,API 那个
// LIBRARY_AGENT_SOCKET 即使配了也 dial 不到。谁真能执行就由谁观测,页面读结果。
const executorStatusColumns = `instance, reachable, busy, degraded, identity, status_error, observed_at`
// UpsertLibraryExecutorStatus 记录一次真实探测。降级/不可达同样要写:
// "最新一次观测是什么"比"最后一条好消息"更有用。
func (s *Store) UpsertLibraryExecutorStatus(ctx context.Context, observation libraryexecutor.Observation, observer string, now time.Time) error {
identity, err := json.Marshal(observation.Identity)
if err != nil {
return fmt.Errorf("encode library executor identity: %w", err)
}
if observation.Instance == "" {
return errors.New("library executor observation needs an instance")
}
observedAt := observation.ObservedAt
if observedAt.IsZero() {
observedAt = now
}
_, err = s.db.Exec(ctx, `
INSERT INTO library_executor_status (instance, reachable, busy, degraded, identity, status_error, observer, observed_at)
VALUES ($1,$2,$3,$4,$5::jsonb,$6,$7,$8)
ON CONFLICT (instance) DO UPDATE SET
reachable = EXCLUDED.reachable,
busy = EXCLUDED.busy,
degraded = EXCLUDED.degraded,
identity = EXCLUDED.identity,
status_error = EXCLUDED.status_error,
observer = EXCLUDED.observer,
observed_at = EXCLUDED.observed_at`,
observation.Instance, observation.Reachable, observation.Busy, observation.Degraded,
string(identity), observation.Error, observer, observedAt.UTC())
if err != nil {
return fmt.Errorf("record library executor status: %w", err)
}
return nil
}
// LibraryExecutorStatus 读某个实例最近一次观测;没有记录返回 nil(不是"可用")。
func (s *Store) LibraryExecutorStatus(ctx context.Context, instance string) (*libraryexecutor.Observation, error) {
row := s.db.QueryRow(ctx, `SELECT `+executorStatusColumns+` FROM library_executor_status WHERE instance=$1`, instance)
var observation libraryexecutor.Observation
var raw []byte
err := row.Scan(&observation.Instance, &observation.Reachable, &observation.Busy, &observation.Degraded,
&raw, &observation.Error, &observation.ObservedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("read library executor status: %w", err)
}
if len(raw) > 0 {
if err := json.Unmarshal(raw, &observation.Identity); err != nil {
return nil, fmt.Errorf("library executor identity unreadable: %w", err)
}
}
return &observation, nil
}