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 则只跑一次。
73 lines
2.9 KiB
Go
73 lines
2.9 KiB
Go
package libraryagent
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"errors"
|
||
"strings"
|
||
)
|
||
|
||
// Expectation 是调用方对"这一条请求该由哪个执行器执行"的声明。
|
||
//
|
||
// 为什么是调用方声明而不是执行器自报:自报值只能证明"我是谁",不能证明"你找的
|
||
// 就是我"。心跳之后、真正停写入者之前,socket 可能被换成另一个 agent;只有把
|
||
// 期望随**同一个请求**送进去、由接收方在操作互斥内、执行任何源操作之前比对,
|
||
// 才能让"换过 socket"变成零副作用(错的那个 noche 一次 run 都不调)。
|
||
type Expectation struct {
|
||
Project string `json:"project"`
|
||
SourceApp string `json:"source_app,omitempty"`
|
||
ArchiveRoot string `json:"archive_root,omitempty"`
|
||
// Fingerprint 覆盖上面三项,供接收方回显、调用方事后核对:
|
||
// 证明"执行器看到的就是我发的那份期望",而不是被替换后重新生成的。
|
||
Fingerprint string `json:"fingerprint"`
|
||
}
|
||
|
||
// NewExpectation 由治理侧配置构造期望(绝不从执行器应答里取字段)。
|
||
func NewExpectation(project, sourceApp, archiveRoot string) Expectation {
|
||
expectation := Expectation{
|
||
Project: strings.TrimSpace(project),
|
||
SourceApp: strings.TrimSpace(sourceApp),
|
||
ArchiveRoot: strings.TrimSpace(archiveRoot),
|
||
}
|
||
expectation.Fingerprint = expectation.fingerprint()
|
||
return expectation
|
||
}
|
||
|
||
func (e Expectation) fingerprint() string {
|
||
sum := sha256.Sum256([]byte(e.Project + "\x00" + e.SourceApp + "\x00" + e.ArchiveRoot))
|
||
return "sha256:" + hex.EncodeToString(sum[:])
|
||
}
|
||
|
||
// Complete 表示期望足够具体:没有 project 的期望无法核对身份,必须 fail closed;
|
||
// 指纹与字段不一致说明请求在半路被改过。
|
||
func (e Expectation) Complete() bool {
|
||
return e.Project != "" && e.Fingerprint == e.fingerprint()
|
||
}
|
||
|
||
// matchExpectation 在操作互斥内比对"本执行器配置"与"调用方期望"。
|
||
// 只比较期望里给出的字段:project 必填,source_app/archive_root 给了就必须一致。
|
||
func (c Config) matchExpectation(expectation Expectation) error {
|
||
if !expectation.Complete() {
|
||
return errors.New("capture 请求缺少完整的期望执行器身份,拒绝执行")
|
||
}
|
||
if c.Project != expectation.Project {
|
||
return errors.New("执行器与请求期望的实例不一致:" +
|
||
"agent=" + quoteOrEmpty(c.Project) + ",期望=" + quoteOrEmpty(expectation.Project) + ",拒绝执行")
|
||
}
|
||
if expectation.SourceApp != "" && c.SourceApp != expectation.SourceApp {
|
||
return errors.New("执行器与请求期望的源容器不一致:" +
|
||
"agent=" + quoteOrEmpty(c.SourceApp) + ",期望=" + quoteOrEmpty(expectation.SourceApp) + ",拒绝执行")
|
||
}
|
||
if expectation.ArchiveRoot != "" && c.ArchiveRoot != expectation.ArchiveRoot {
|
||
return errors.New("执行器与请求期望的归档根不一致,拒绝执行")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func quoteOrEmpty(value string) string {
|
||
if value == "" {
|
||
return `""`
|
||
}
|
||
return `"` + value + `"`
|
||
}
|