Stop lease validation from depending on the current clock
The API was in a restart loop, exiting with `invalid event: until_ns required`. ValidateEvent compared until_ns against time.Now() for TaskLeased and TaskLeaseRenewed, so a lease event that was valid when written failed validation once it expired. store.Open replays the log tail after the snapshot and log.Fatal's on the first invalid event, so the coordinator refused its own history and could not start. Validation of a durable event must be time-independent. Well-formedness is this function's question; freshness belongs to Store.Lease and Store.ExpireLeases, which compute until_ns themselves. Latent since the field was introduced. It needed a renewal in the post-snapshot tail plus a restart after that renewal expired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -289,3 +289,38 @@ func TestDecisionEventValidation(t *testing.T) {
|
||||
t.Fatalf("empty supersede payload: want ErrInvalid, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A durable event must validate the same way forever. Comparing until_ns
|
||||
// against the current clock made every lease event fail once it expired, so
|
||||
// replaying the log after a restart refused the store's own history and the
|
||||
// coordinator could not start. Found live: the API entered a restart loop
|
||||
// logging "invalid event: until_ns required".
|
||||
func TestLeaseEventsValidateAfterTheyExpire(t *testing.T) {
|
||||
past := float64(time.Now().Add(-24 * time.Hour).UnixNano())
|
||||
for _, e := range []Event{
|
||||
{ID: "a", Type: "TaskLeased", TaskID: "t", Version: 2, Surface: "system", Payload: mustPayload(map[string]any{
|
||||
"harness_id": "h1", "until_ns": past, "expected_version": 1,
|
||||
})},
|
||||
{ID: "b", Type: "TaskLeaseRenewed", TaskID: "t", Version: 3, Surface: "system", Payload: mustPayload(map[string]any{
|
||||
"harness_id": "h1", "until_ns": past, "expected_version": 2,
|
||||
})},
|
||||
} {
|
||||
if err := ValidateEvent(e); err != nil {
|
||||
t.Fatalf("%s failed validation after expiry: %v", e.Type, err)
|
||||
}
|
||||
}
|
||||
// Well-formedness is still checked.
|
||||
if err := ValidateEvent(Event{ID: "c", Type: "TaskLeaseRenewed", TaskID: "t", Version: 3, Surface: "system", Payload: mustPayload(map[string]any{
|
||||
"harness_id": "h1", "expected_version": 2,
|
||||
})}); err == nil {
|
||||
t.Fatal("a renewal with no until_ns was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func mustPayload(v map[string]any) []byte {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user