fix(store): unique event IDs on lease/expiry, honest duplicate-ingest signal (S5, S6)
S5: Store.Lease and Store.ExpireLeases both set Event.ID to the task id, so
every TaskLeased/TaskReleased event for a given task collided on ID across
every lease of that task — unsound for ApplyAdvisory or any future
ID-based lookup. Both now call domain.NewID().
S6: Append's TaskCreated dedup path returned nil (success) without
appending anything. main.go's handler then did
`s.Events(0)[len(s.Events(0))-1]` and returned that — an unrelated event —
with 201 Created, and every other Append caller (Gitea poll/webhook, JSONL
ingest) had no way to distinguish "duplicate, as expected" from "genuinely
appended".
Add domain.ErrDuplicate, returned instead of nil on a duplicate
(source, external_id). Add Store.TaskBySource to resolve the
already-ingested task by that same dedup key. Update every caller:
- main.go's POST /v1/tasks now returns 200 with the existing task on
ErrDuplicate instead of fabricating a 201 with the wrong event.
- provider.Gitea.Poll/IngestWebhook and provider.JSONL.Ingest treat
ErrDuplicate as expected (already-seen issue/line), not a failure —
without this, Gitea polling would have errored out of its loop on the
first already-ingested issue in every batch, since Poll previously
relied on the old nil-on-dup behavior to keep scanning.
TestLeaseAndExpireEventIDsAreUnique and TestTaskBySourceResolvesDuplicate
cover the store-level fixes; TestAppendReplayAndDeduplicate updated for the
new error signal.
AUDIT.md S5, S6.
This commit is contained in:
+16
-3
@@ -189,7 +189,7 @@ func (s *Store) Append(e domain.Event) error {
|
||||
return err
|
||||
}
|
||||
if id := s.external[p["source"].(string)+"\x00"+p["external_id"].(string)]; id != "" {
|
||||
return nil
|
||||
return domain.ErrDuplicate
|
||||
}
|
||||
}
|
||||
t, taskExists := s.tasks[e.TaskID]
|
||||
@@ -326,6 +326,19 @@ func (s *Store) Task(id string) (domain.Task, bool) {
|
||||
return t, ok
|
||||
}
|
||||
|
||||
// TaskBySource resolves the task ingested for a given (source, external_id)
|
||||
// pair — the dedup key Append.ErrDuplicate rejects re-ingestion against.
|
||||
func (s *Store) TaskBySource(source, externalID string) (domain.Task, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
id := s.external[source+"\x00"+externalID]
|
||||
if id == "" {
|
||||
return domain.Task{}, false
|
||||
}
|
||||
t, ok := s.tasks[id]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
func (s *Store) Lease(id, harness string, ttl time.Duration) (domain.Event, error) {
|
||||
if ttl <= 0 {
|
||||
return domain.Event{}, fmt.Errorf("%w: ttl must be positive", domain.ErrInvalid)
|
||||
@@ -338,7 +351,7 @@ func (s *Store) Lease(id, harness string, ttl time.Duration) (domain.Event, erro
|
||||
return domain.Event{}, domain.ErrConflict
|
||||
}
|
||||
p, _ := json.Marshal(map[string]any{"harness_id": harness, "ttl": ttl.Seconds(), "until_ns": time.Now().Add(ttl).UnixNano(), "expected_version": t.Version})
|
||||
e := domain.Event{ID: id, Type: "TaskLeased", TaskID: id, Version: t.Version + 1, Payload: p, Surface: string(authz.System)}
|
||||
e := domain.Event{ID: domain.NewID(), Type: "TaskLeased", TaskID: id, Version: t.Version + 1, Payload: p, Surface: string(authz.System)}
|
||||
return e, s.Append(e)
|
||||
}
|
||||
|
||||
@@ -347,7 +360,7 @@ func (s *Store) ExpireLeases(now time.Time) ([]domain.Event, error) {
|
||||
for _, t := range s.Tasks() {
|
||||
if t.State == domain.StateLeased && t.Lease != nil && !t.Lease.Until.After(now) {
|
||||
p, _ := json.Marshal(map[string]any{"reason": "lease_expired", "harness_id": t.Lease.HarnessID})
|
||||
e := domain.Event{ID: t.ID, Type: "TaskReleased", TaskID: t.ID, Version: t.Version + 1, Payload: p, Surface: string(authz.System)}
|
||||
e := domain.Event{ID: domain.NewID(), Type: "TaskReleased", TaskID: t.ID, Version: t.Version + 1, Payload: p, Surface: string(authz.System)}
|
||||
if err := s.Append(e); err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user