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:
2026-08-26 23:48:04 +04:00
parent 4fbf3ac966
commit 77a2b323fa
2 changed files with 45 additions and 2 deletions
+35
View File
@@ -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
}
+10 -2
View File
@@ -299,12 +299,19 @@ func ValidatePayload(typ string, p map[string]any) error {
if err := requiredString("harness_id"); err != nil {
return err
}
// Validation of a durable event must not depend on the current clock.
// Comparing until_ns against time.Now() here made every lease event
// fail validation once it expired, so replaying the log after a
// restart refused the store's own history and the coordinator could
// not start at all. Freshness is a lease question, answered by
// Store.Lease and Store.ExpireLeases; well-formedness is this
// function's question.
until, untilOK := p["until_ns"].(float64)
if ttl, ok := p["ttl"].(float64); ok {
if ttl <= 0 {
return fmt.Errorf("%w: ttl invalid", ErrInvalid)
}
} else if !untilOK || until <= float64(time.Now().UnixNano()) {
} else if !untilOK || until <= 0 {
return fmt.Errorf("%w: ttl required", ErrInvalid)
}
if v, ok := p["expected_version"].(float64); !ok || v < 0 || v != float64(int(v)) {
@@ -314,8 +321,9 @@ func ValidatePayload(typ string, p map[string]any) error {
if err := requiredString("harness_id"); err != nil {
return err
}
// Time-independent for the same reason as TaskLeased above.
until, ok := p["until_ns"].(float64)
if !ok || until <= float64(time.Now().UnixNano()) {
if !ok || until <= 0 {
return fmt.Errorf("%w: until_ns required", ErrInvalid)
}
if v, ok := p["expected_version"].(float64); !ok || v < 0 || v != float64(int(v)) {