6ed06357f9
build / build (push) Successful in 1m27s
原来 worker 先 health 再 capture,两次拨号之间 socket 可能被换成另一个实例的 执行器:错的 agent 已经把写入者停了、快照也出了,事后 postcheck 只能拒绝登记, 副作用无法撤销。 现在 capture 请求自带期望身份(治理侧配置生成:project 必填,source_app / archive_root 配了就绑定)与覆盖三项的稳定指纹;接收方在操作互斥内、执行任何源 操作之前比对,缺字段、指纹与字段不一致、project/source_app 不符都直接拒绝, **错的执行器一次 capture 都不跑**。封存结果回显该指纹,worker 事后复核按它对照, postcheck 只作为证据防线。恢复路径不要求期望(它只把写入者恢复回来)。 负例是确定性的:两个真实 agent 各占一个 socket,把调用方指向错的那个,断言它的 capture 调用为 0 且仍能回答 health;同一份期望打到对的 socket 则只跑一次。
108 lines
3.6 KiB
Go
108 lines
3.6 KiB
Go
package libraryagent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
// ClientConfig is what a governance process needs: one socket path. It never
|
|
// carries source credentials or Docker access — those stay with the agent.
|
|
type ClientConfig struct {
|
|
Socket string
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// Result is the agent's answer to one capture/status/recover request.
|
|
type Result struct {
|
|
OK bool
|
|
Scope string
|
|
Job string
|
|
SnapshotID string
|
|
Replay bool
|
|
Error string
|
|
Payload json.RawMessage
|
|
}
|
|
|
|
// Capture asks the agent to seal one library scope. The job id makes the call
|
|
// idempotent: repeating it returns the already sealed candidate.
|
|
func Capture(ctx context.Context, config ClientConfig, scope, job string, expect Expectation) (Result, error) {
|
|
return CaptureReleased(ctx, config, scope, job, nil, expect)
|
|
}
|
|
|
|
// CaptureReleased 在 Capture 之上带上治理平台已审计的精确死信解除指纹。
|
|
func CaptureReleased(ctx context.Context, config ClientConfig, scope, job string, released []string, expect Expectation) (Result, error) {
|
|
request := map[string]any{"action": "capture", "scope": scope, "job": job, "expect": expect}
|
|
if len(released) > 0 {
|
|
request["released"] = released
|
|
}
|
|
return call(ctx, config, request)
|
|
}
|
|
|
|
// Status probes the agent (source wiring, writer group, archive usage).
|
|
func Status(ctx context.Context, config ClientConfig) (Result, error) {
|
|
return call(ctx, config, map[string]any{"action": "status"})
|
|
}
|
|
|
|
// Health 只问三件事:这个执行器指向谁(project/容器/归档)、是否在跑、是否降级。
|
|
// 它不跑子进程、不占操作锁,所以可以当心跳用;Status 会去查源库,代价高得多。
|
|
func Health(ctx context.Context, config ClientConfig) (Result, error) {
|
|
return call(ctx, config, map[string]any{"action": "health"})
|
|
}
|
|
|
|
// Recover restores writers from an interrupted freeze journal.
|
|
func Recover(ctx context.Context, config ClientConfig) (Result, error) {
|
|
return call(ctx, config, map[string]any{"action": "recover"})
|
|
}
|
|
|
|
func call(ctx context.Context, config ClientConfig, request map[string]any) (Result, error) {
|
|
if config.Socket == "" {
|
|
return Result{}, errors.New("library agent socket is not configured")
|
|
}
|
|
timeout := config.Timeout
|
|
if timeout <= 0 {
|
|
timeout = 2 * time.Minute
|
|
}
|
|
bounded, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
var dialer net.Dialer
|
|
conn, err := dialer.DialContext(bounded, "unix", config.Socket)
|
|
if err != nil {
|
|
return Result{}, errors.New("library agent is unreachable")
|
|
}
|
|
defer conn.Close()
|
|
if deadline, ok := bounded.Deadline(); ok {
|
|
_ = conn.SetDeadline(deadline)
|
|
}
|
|
if err = json.NewEncoder(conn).Encode(request); err != nil {
|
|
return Result{}, errors.New("library agent request failed")
|
|
}
|
|
var response struct {
|
|
OK bool `json:"ok"`
|
|
Scope string `json:"scope"`
|
|
Job string `json:"job"`
|
|
Error string `json:"error"`
|
|
Payload json.RawMessage `json:"payload"`
|
|
}
|
|
if err = json.NewDecoder(conn).Decode(&response); err != nil {
|
|
return Result{}, errors.New("library agent returned an invalid response")
|
|
}
|
|
result := Result{OK: response.OK, Scope: response.Scope, Job: response.Job, Error: response.Error, Payload: response.Payload}
|
|
var payload struct {
|
|
SnapshotID string `json:"snapshot_id"`
|
|
Replay bool `json:"replay"`
|
|
}
|
|
if len(response.Payload) > 0 && json.Unmarshal(response.Payload, &payload) == nil {
|
|
result.SnapshotID, result.Replay = payload.SnapshotID, payload.Replay
|
|
}
|
|
if !result.OK {
|
|
if result.Error == "" {
|
|
result.Error = "library agent refused the request"
|
|
}
|
|
return result, errors.New(result.Error)
|
|
}
|
|
return result, nil
|
|
}
|