feat: support multi-PR review runs#396
Conversation
a912669 to
6bc1a7c
Compare
7520adc to
46879fc
Compare
46879fc to
b45f263
Compare
neversettle17-101
left a comment
There was a problem hiding this comment.
AO review — feat: support multi-PR review runs
Verdict: Changes requested (one medium correctness concern + a small leak; the rest is solid).
This is a clean, well-tested refactor from single-PR to multi-PR review runs. The pure Plan() shared between the trigger and list paths is a nice design, the migration carefully dedupes before adding the (session_id, pr_url, target_sha) unique index and folds in batch_id, and the batch-gated delivery via drainBatch + ApplyReviewBatch is reasonable. Build is green and go test ./internal/review/... ./internal/service/review/... ./internal/lifecycle/... ./internal/httpd/controllers/... ./internal/storage/sqlite/... all pass.
Concerns below — the batch-delivery one is worth confirming is intended before merge.
Medium
- A never-terminal run blocks delivery of the whole batch.
drainBatchreturns early as soon as any run in the batch is stillrunning. Delivery only ever happens on aSubmitcall, and a failed/superseded status is only produced by launch failures or a newer-commit trigger — not by a reviewer that dies or simply never submits one PR's run. So if the shared reviewer pane crashes after reviewing PR1 but before submitting PR2, PR1'schanges_requestedfeedback is never delivered to the worker, and there's no sweeper to force the stuck run terminal. Single-PR triggers didn't couple PRs this way. Is there an intended recovery path (timeout / reviewer-death reaction) I'm missing?
Low
drainLocksgrows unbounded.lockBatchDrainadds a*sync.Mutexper(workerID, batchID)and never removes it, so the map leaks one entry per batch over the daemon's lifetime. Consider deleting the entry once the batch is fully delivered, or using a sharded/striped lock keyed by a bounded hash.firstReusableRuncan return an unrelated PR's run. When nothing new is created it returns the first review state with anyLatestRun, regardless of PR or eligibility, into the legacyTriggerResult.Run. Harmless givenCreated:falseand thatReviewsnow carries the real state, but the picked run is arbitrary across PRs.
Notes (non-blocking)
upsertReviewnow always writesPRURL: "", leavingdomain.Review.PRURLa permanently-empty field. Fine for the multi-PR model, but the field is now dead — worth a follow-up to drop it or document why it stays.- Re-triggering a
changes_requestedPR at its current head relies on the unique-indexErrDuplicateReviewRunfallback to no-op; works, but does an extra insert attempt each trigger.
2be6ed1 to
a132ce0
Compare
neversettle17-101
left a comment
There was a problem hiding this comment.
AO re-review — feat: support multi-PR review runs (backend)
Verdict: Approve (both prior blocking concerns resolved; only minor notes remain).
Thanks for the rework in fix: submit multi-pr reviews as one batch and fix: make queued reviews autonomous — this addresses both findings from my previous review:
- Stuck-run no longer blocks batch delivery (was my medium concern). Delivery is now scoped to the runs in a single reviewer CLI submission (
SubmitMany→deliverSubmitted/deliverableRuns) rather than gated on the whole batch having norunningrun. A missing/stuck result for another PR in the same trigger can no longer withhold feedback. The newTestSubmitBatchRunDoesNotWaitForOtherRunningRunsandTestSubmitManySendsCombinedChangesRequestedlock this in. drainLocksleak removed (was my low concern). The per-(worker,batch) mutex map anddrainBatchare gone entirely.- The prompt/CLI now drive an autonomous multi-PR queue with a single batched
ao review submit --reviews -, andsubmitOneis idempotent for already-complete/deliveredruns, so a retried submission recovers cleanly.
Build green; go test ./internal/review/... ./internal/service/review/... ./internal/lifecycle/... ./internal/httpd/controllers/... ./internal/cli/... ./internal/storage/sqlite/... all pass.
Minor notes (non-blocking)
SubmitManypersists results item-by-item before delivering. If item N is invalid (bad verdict / run not running), items 0..N-1 are alreadycompletein the store when the call aborts with no delivery. Idempotency makes a full retry recover, but pre-validating every item before the firstUpdateReviewRunResultwould avoid leaving a batch half-committed.ReviewIndexis now effectively dead. Since the per-run notify loop was removed inTrigger, onlycreated[0]is ever launched (index 0) andreviewQueueTextno longer readsspec.ReviewIndex. Consider dropping the field fromLaunchSpec/ports.ReviewInvocationto avoid a misleading knob.- HTTP submit accepts both shapes. The controller takes either top-level
runId/verdict/...or areviews[]array; the CLI guards against combining them but the API doesn't. Harmless (array wins, top-level ignored) but worth a note.
| } | ||
| runs := make([]domain.ReviewRun, 0, len(reviews)) | ||
| for _, review := range reviews { | ||
| run, err := s.submitOne(ctx, workerID, review) |
There was a problem hiding this comment.
Results are persisted one at a time here; if a later item fails validation in submitOne, earlier items are already marked complete while the whole call returns an error with no delivery. submitOne's idempotency lets a full retry recover, but validating all items up front (before any UpdateReviewRunResult) would avoid leaving a batch half-committed.
| PRURL: run.PRURL, | ||
| TargetSHA: run.TargetSHA, | ||
| ReviewQueue: queue, | ||
| ReviewIndex: index, |
There was a problem hiding this comment.
ReviewIndex is now always 0 (the per-run notify loop was removed, so only created[0] is launched) and reviewQueueText no longer reads it. Consider removing the field from LaunchSpec/ReviewInvocation so it doesn't read as a live knob.
Summary
Go Rewrite Notes
Tests
Refs #395