fix: serialize review list with trigger

This commit is contained in:
codebanditssss 2026-07-05 18:22:03 +05:30
parent bc5bce5459
commit c88fdaa9ad
2 changed files with 74 additions and 0 deletions

View File

@ -366,6 +366,9 @@ func (e *Engine) List(ctx stdctx.Context, workerID domain.SessionID) (SessionRev
if workerID == "" { if workerID == "" {
return SessionReviews{}, fmt.Errorf("%w: worker session id is required", ErrInvalid) return SessionReviews{}, fmt.Errorf("%w: worker session id is required", ErrInvalid)
} }
unlock := e.lockWorker(workerID)
defer unlock()
runs, err := e.store.ListReviewRunsBySession(ctx, workerID) runs, err := e.store.ListReviewRunsBySession(ctx, workerID)
if err != nil { if err != nil {
return SessionReviews{}, err return SessionReviews{}, err

View File

@ -180,6 +180,22 @@ func (f *fakeLauncher) Stop(_ context.Context, handleID string) error {
return nil return nil
} }
type blockingSpawnLauncher struct {
fakeLauncher
entered chan struct{}
release chan struct{}
}
func (f *blockingSpawnLauncher) Spawn(_ context.Context, spec LaunchSpec) (string, error) {
f.gotSpec = spec
f.specs = append(f.specs, spec)
close(f.entered)
<-f.release
f.spawned = true
f.spawnCount++
return f.handle, nil
}
func liveWorker() domain.SessionRecord { func liveWorker() domain.SessionRecord {
return domain.SessionRecord{ return domain.SessionRecord{
ID: "mer-1", ID: "mer-1",
@ -268,6 +284,61 @@ func TestTriggerConcurrentSameWorkerSpawnsOnce(t *testing.T) {
} }
} }
func TestListWaitsForTriggerBeforeFailingDeadReviewer(t *testing.T) {
store := &fakeStore{
review: &domain.Review{ID: "rev-1", SessionID: "mer-1", ReviewerHandleID: "review-mer-old"},
}
launcher := &blockingSpawnLauncher{
fakeLauncher: fakeLauncher{handle: "review-mer-1", alive: false},
entered: make(chan struct{}),
release: make(chan struct{}),
}
eng := newEngineForTest(store, fakeSessions{rec: liveWorker(), ok: true}, prAt("sha1"), fakeProjects{}, launcher)
triggerDone := make(chan error, 1)
go func() {
_, err := eng.Trigger(context.Background(), "mer-1")
triggerDone <- err
}()
<-launcher.entered
listDone := make(chan struct {
reviews SessionReviews
err error
}, 1)
go func() {
got, err := eng.List(context.Background(), "mer-1")
listDone <- struct {
reviews SessionReviews
err error
}{reviews: got, err: err}
}()
select {
case got := <-listDone:
t.Fatalf("List returned while Trigger was still spawning: got=%+v err=%v", got.reviews, got.err)
case <-time.After(50 * time.Millisecond):
}
close(launcher.release)
if err := <-triggerDone; err != nil {
t.Fatalf("Trigger: %v", err)
}
got := <-listDone
if got.err != nil {
t.Fatalf("List: %v", got.err)
}
if got.reviews.ReviewerHandleID != "review-mer-1" {
t.Fatalf("reviewer handle = %q, want fresh handle", got.reviews.ReviewerHandleID)
}
if len(got.reviews.Runs) != 1 || got.reviews.Runs[0].Status != domain.ReviewRunRunning {
t.Fatalf("runs = %+v, want still-running fresh run", got.reviews.Runs)
}
if store.runs[0].Status != domain.ReviewRunRunning {
t.Fatalf("stored run was failed during trigger/list race: %+v", store.runs[0])
}
}
func TestTriggerFallsBackToExistingRunOnUniqueConflict(t *testing.T) { func TestTriggerFallsBackToExistingRunOnUniqueConflict(t *testing.T) {
// The idempotency check passes (no run yet), but the insert loses to a // The idempotency check passes (no run yet), but the insert loses to a
// concurrent writer the unique index already accepted. // concurrent writer the unique index already accepted.