fix(agent): 手机已经放弃的请求不再执行
中继答 504 并说"可能已经执行了,重试前请先确认"——这句话诚实的前提是它保持为真。 批次是串行处理的,所以满邮箱里第八个信封可以在它被签名之后 8 个上游超时才轮到; 那时手机早已被告知失败、用户可能已经重试。再执行它,就把一次诚实的"未知"变成了 一条重复发出的短信。 现在 agent 在动任何东西之前先比对自己的截止时间(策略里的 requestTtlSeconds), 超龄一律以 expired 拒绝并落审计。判定放在新鲜度检查之后、策略判定之前:它比签名 更便宜,而且一个过期请求无论路径是否在白名单里都不该执行。 为了让这条规则**可达**,策略新增一条断言:TTL 不得大于时钟偏移窗口。否则新鲜度 检查会先触发,这条规则永远不会被执行——而"永不触发的规则"是出事时才会被发现坏了 的那种东西。 单个上游调用的超时也移进了策略(agentUpstreamTimeoutSeconds)。"中继期限必须长于 一次上游调用"这个比较,与二进制里的常量是比不了的;现在两侧从同一个文件读,策略 加载时就能拒绝一组自相矛盾的数值。原来那个 20s 常量保留为默认值的来源,不再硬编码。 测试两项:一个请求在期限内被转发、一个超龄请求被拒且**没有被转发**(计数按路径, 否则首次转发前的 /api/auth/status 探测会让断言以错误的理由通过);另一项断言策略 本身满足上述两个关系。变异验证:关掉判定,第一项失败。 Go 全包、Node 119 项、Swift 101 项通过。
This commit is contained in:
@@ -594,3 +594,68 @@ func TestAFailedAuditWriteIsReported(t *testing.T) {
|
||||
t.Fatalf("a failed audit write was not reported; log was: %s", captured.String())
|
||||
}
|
||||
}
|
||||
|
||||
// A request whose phone has already given up must not be executed.
|
||||
//
|
||||
// The relay answers 504 with "it may or may not have executed it; check the node before retrying",
|
||||
// which is honest — but only while it stays true. A batch is processed serially, so the eighth
|
||||
// envelope of a full mailbox can begin up to eight upstream timeouts after it was signed. By then the
|
||||
// phone has been told the request failed and the user may have retried; executing it turns an honest
|
||||
// "unknown" into a duplicate SMS.
|
||||
func TestARequestPastItsDeadlineIsNotExecuted(t *testing.T) {
|
||||
// Counted by path: the agent probes /api/auth/status before its first forward, and counting every
|
||||
// request would make the first assertion pass for the wrong reason.
|
||||
executed := 0
|
||||
f := newFixture(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/sms/send" {
|
||||
executed++
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
pol := testPolicy(t)
|
||||
body := []byte(`{"phone_number":"+8613800000000","content":"hello"}`)
|
||||
intent, err := wire.IntentDigest("POST", "/api/sms/send", body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inner := wire.InnerRequest{Method: "POST", Path: "/api/sms/send", Body: body, Intent: intent}
|
||||
|
||||
// One second inside the deadline: executed, because the phone is still waiting.
|
||||
envelope := envelopeAt("nd_test0001", newReqID(1), uint64(f.now.Unix()))
|
||||
if _, refusal := f.executor.Execute(context.Background(), envelope, inner); refusal != nil {
|
||||
t.Fatalf("a request inside its deadline was refused: %v", refusal)
|
||||
}
|
||||
if executed != 1 {
|
||||
t.Fatalf("expected one execution, got %d", executed)
|
||||
}
|
||||
|
||||
// One second past it: refused, and nothing is forwarded.
|
||||
f.advance(time.Duration(pol.Limits.RequestTTLSeconds+1) * time.Second)
|
||||
late := envelopeAt("nd_test0001", newReqID(2), uint64(f.now.Unix()-int64(pol.Limits.RequestTTLSeconds)-1))
|
||||
_, refusal := f.executor.Execute(context.Background(), late, inner)
|
||||
if refusal == nil {
|
||||
t.Fatal("a request past its deadline was executed; the phone was told it had failed")
|
||||
}
|
||||
if refusal.Class != ClassExpired {
|
||||
t.Fatalf("refused as %s, want %s", refusal.Class, ClassExpired)
|
||||
}
|
||||
if executed != 1 {
|
||||
t.Fatalf("the late request was forwarded anyway (%d executions)", executed)
|
||||
}
|
||||
}
|
||||
|
||||
// The deadline check is only reachable because the policy keeps the TTL inside the clock-skew window.
|
||||
// If it were not, the freshness check would fire first and the deadline rule could never be reached —
|
||||
// a rule that never fires is one nobody discovers is broken until an incident.
|
||||
func TestThePolicyKeepsTheDeadlineInsideTheFreshnessWindow(t *testing.T) {
|
||||
pol := testPolicy(t)
|
||||
if pol.Limits.RequestTTLSeconds > pol.Limits.ClockSkewSeconds {
|
||||
t.Fatalf("requestTtlSeconds (%d) exceeds clockSkewSeconds (%d); the deadline check is unreachable",
|
||||
pol.Limits.RequestTTLSeconds, pol.Limits.ClockSkewSeconds)
|
||||
}
|
||||
// And the relay's deadline is the longer bound, or the phone gives up before the node does.
|
||||
if pol.Limits.RequestTTLSeconds <= pol.Limits.AgentUpstreamTimeoutSeconds {
|
||||
t.Fatalf("the relay deadline (%ds) is not longer than one upstream call (%ds)",
|
||||
pol.Limits.RequestTTLSeconds, pol.Limits.AgentUpstreamTimeoutSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user