feat: add durable relative task delays and outage recovery regression

This commit is contained in:
2026-09-17 23:43:57 +08:00
parent 522fe2ef3e
commit a664ec9370
34 changed files with 811 additions and 28 deletions

View File

@@ -393,4 +393,66 @@ func TestIntegrationTaskAPI(t *testing.T) {
t.Fatal(err)
}
})
t.Run("relative_delay_uses_database_acceptance_and_original_identity", func(t *testing.T) {
delayed := input
delayed.DelayMS = 5000
key := "delayed-submit"
first := must("POST", "/v1/tasks", a.Token, key, marshal(delayed), 201)
var v service.SubmitResult
if err := json.Unmarshal(first.data, &v); err != nil || !v.Task.AvailableAt.Equal(v.Task.CreatedAt.Add(5*time.Second)) {
t.Fatal("delay not anchored to persisted database acceptance", err)
}
const n = 8
delayedBody := marshal(delayed)
results := make(chan apiReply, n)
errs := make(chan error, n)
for range n {
go func() {
out, err := call("POST", "/v1/tasks", a.Token, key, delayedBody)
results <- out
errs <- err
}()
}
for range n {
out, err := <-results, <-errs
var duplicate service.SubmitResult
if err != nil || out.status != 200 || json.Unmarshal(out.data, &duplicate) != nil || !duplicate.Deduplicated || duplicate.Task.ID != v.Task.ID || !duplicate.Task.AvailableAt.Equal(v.Task.AvailableAt) || !duplicate.Task.DeadlineAt.Equal(v.Task.DeadlineAt) {
t.Fatal("repeated delay reset execution time or identity", err)
}
}
changed := delayed
changed.DelayMS++
must("POST", "/v1/tasks", a.Token, key, marshal(changed), 409)
changed = delayed
changed.DelayMS = 0
changed.AvailableAt = &v.Task.AvailableAt
must("POST", "/v1/tasks", a.Token, key, marshal(changed), 409)
changed.DelayMS = delayed.DelayMS
must("POST", "/v1/tasks", a.Token, "both-timing-modes", marshal(changed), 400)
changed = delayed
tooEarly := v.Task.CreatedAt.Add(time.Second)
changed.StartDeadlineAt = &tooEarly
must("POST", "/v1/tasks", a.Token, "delay-after-start-deadline", marshal(changed), 400)
changed = delayed
changed.DelayMS = 86400000
must("POST", "/v1/tasks", a.Token, "delay-at-total-deadline", marshal(changed), 400)
if count("pulse_tasks", "namespace_id=? AND id=?", a.NamespaceID, v.Task.ID) != 1 || count("orbit_events", "namespace_id=? AND aggregate_id=?", a.NamespaceID, v.Task.ID) != 1 {
t.Fatal("delayed duplicate emitted another task or event")
}
// 省略与显式零保持原有即刻/绝对时间请求语义,不破坏升级前的摘要。
zero := bytes.Clone(bytes.TrimSuffix(raw, []byte("}")))
zero = append(zero, []byte(`,"delay_ms":0}`)...)
must("POST", "/v1/tasks", a.Token, "stable-submit", zero, 200)
batch := service.BatchSubmitInput{Items: []service.BatchSubmitItem{{IdempotencyKey: key, Task: delayed}, {IdempotencyKey: "delay-batch-new", Task: delayed}}}
batchReply := must("POST", "/v1/tasks:batch", a.Token, "", marshal(batch), 200)
var batchOut service.BatchSubmitResult
if json.Unmarshal(batchReply.data, &batchOut) != nil || len(batchOut.Items) != 2 || batchOut.Items[0].Receipt == nil || batchOut.Items[1].Receipt == nil || !batchOut.Items[0].Receipt.Deduplicated || batchOut.Items[0].Receipt.TaskID != v.Task.ID {
t.Fatal("batch did not reuse relative-delay identity")
}
fresh := must("GET", "/v1/tasks/"+string(batchOut.Items[1].Receipt.TaskID), a.Token, "", nil, 200)
var freshTask service.TaskView
if json.Unmarshal(fresh.data, &freshTask) != nil || !freshTask.AvailableAt.Equal(freshTask.CreatedAt.Add(5*time.Second)) {
t.Fatal("batch delay did not use its own acceptance time")
}
})
}