Skip to content

feat(exchange): scope pending-exchange cancellation by origin + ladder plumbing (L3) - #1368

Merged
cristim merged 2 commits into
mainfrom
feat/ladder-reshape-scoped-cancel
Jul 16, 2026
Merged

feat(exchange): scope pending-exchange cancellation by origin + ladder plumbing (L3)#1368
cristim merged 2 commits into
mainfrom
feat/ladder-reshape-scoped-cancel

Conversation

@cristim

@cristim cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member

What

Work item L3 of the auto-laddering full-automation plan (closes gap G10 / issue #1348). Prevents the standalone ri_exchange_reshape task and ladder runs from silently cancelling each other's pending exchanges, and adds the plumbing the ladder write path needs.

Today RunAutoExchange begins by cancelling all pending exchanges store-wide. Once laddering creates pending reshapes, the standalone task (its own 6h schedule) would silently cancel a ladder run's awaiting-approval reshapes.

Changes

  • Scoped cancellation: new CancelPendingExchangesByOrigin(ctx, origin common.ExchangeOrigin) replaces the store-wide cancel. ExchangeOriginStandalone cancels only ladder_run_id IS NULL rows; ExchangeOriginLadder cancels only IS NOT NULL. The origin is a validated typed enum (pkg/common, fails loud on an unknown value) — not a bare bool.
  • LadderRunID plumbing: added to the exchange record type and the ri_exchange_history.ladder_run_id column is now written/read (INSERT is 21 columns/args, all 5 SELECT/RETURNING + Scan aligned; an alignment regression test guards it). Ladder-originated exchanges are tagged; standalone rows stay NULL.
  • True dry-run: DryRun=true performs zero mutations (no cancel, no save, no Execute, no token generation) and returns Simulated=true.
  • Seam correctness: exchangeRunner.RunAutoExchange is widened to (ctx, cfg, ladderRunID *string, dryRun bool) so the concrete runner (wired in L16) is structurally forced to thread LadderRunID — it cannot compile while defaulting to nil and re-introducing the cross-task-cancel bug.

Scope / current state

Standalone-side scoping is active now (a single-source deployment behaves exactly as before). Ladder-side scoping activates when the write path's concrete runner is wired (L16); until then the ladder reshape path returns errWriteNotWired. The IS NOT NULL partition is deliberately coarse (is-ladder, not per-run); a per-run/per-config scoping key is tracked in #1367 for before ladder reshapes create pendings.

Verification (exit 0)

  • go build ./..., go vet ./... clean.
  • pkg go test ./exchange/... 100 pass; go test ./internal/server/... ./internal/config/... 1010 pass; integration (real PG/pgxmock) 733 pass; providers/aws ladder 115 pass; gocyclo -over 10 clean.
  • Tests prove: standalone cancel does NOT touch ladder pendings and vice-versa (fail-before/pass-after on a WHERE-clause swap); dry-run performs zero mutations (asserted via mock.AssertExpectations, no sleeps); LadderRunID round-trips (nil forwarded as nil, no fabricated value); unknown origin fails loud with no query issued; 21-arg INSERT column alignment.

Part of #1336. Tracker #1333. Addresses #1348.

@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-quarter Within the quarter impact/many Affects most users effort/m Days type/feat New capability labels Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b6aeae5-fa34-4c26-b831-c902c69af397

📥 Commits

Reviewing files that changed from the base of the PR and between 7adacd7 and 5b9a8e5.

⛔ Files ignored due to path filters (1)
  • pkg/go.sum is excluded by !**/*.sum
📒 Files selected for processing (18)
  • internal/analytics/collector_test.go
  • internal/config/interfaces.go
  • internal/config/store_postgres.go
  • internal/config/store_postgres_nil_db_test.go
  • internal/config/store_postgres_pgxmock_test.go
  • internal/config/types.go
  • internal/mocks/stores.go
  • internal/server/handler_ri_exchange.go
  • internal/server/handler_ri_exchange_test.go
  • internal/server/test_helpers_test.go
  • pkg/common/types.go
  • pkg/exchange/auto.go
  • pkg/exchange/auto_test.go
  • pkg/go.mod
  • pkg/ladder/capability.go
  • providers/aws/ladder/interfaces.go
  • providers/aws/ladder/reshape.go
  • providers/aws/ladder/reshape_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ladder-reshape-scoped-cancel

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cristim added 2 commits July 16, 2026 16:07
…e dry-run (G10 / #1348)

RunAutoExchange previously called CancelAllPendingExchanges (store-wide),
so the standalone ri_exchange_reshape task would silently cancel pending
reshapes created by ladder runs and vice versa.

Three sub-goals addressed:

1. SCOPE the cancellation: replace CancelAllPendingExchanges in
   RunAutoExchange with CancelPendingExchangesByOrigin(ctx, ladderScoped).
   - ladderScoped=false (standalone): WHERE ladder_run_id IS NULL
   - ladderScoped=true  (ladder):     WHERE ladder_run_id IS NOT NULL
   Keep CancelAllPendingExchanges on the interface for compat.

2. LADDER_RUN_ID plumbing: add LadderRunID *string to ExchangeRecord,
   RunAutoExchangeParams, and config.RIExchangeRecord; write it in
   SaveRIExchangeRecord ($21 param) and read it in all SELECT queries /
   queryRIExchangeRecords Scan; propagate through the adapter converters.
   The DB column already existed (migration 000080).

3. TRUE DRY-RUN: RunAutoExchangeParams.DryRun=true skips cancellation,
   SaveRIExchangeRecord, and Execute; returns outcomes tagged Simulated=true.
   No live approval tokens issued. ReshapeBuffer's DryRun rejection comment
   updated to reflect that the pkg/exchange layer now supports it (end-to-end
   wiring deferred to L16).

Also: extract nullPtrFromNullString helper in store_postgres.go to reduce
queryRIExchangeRecords cyclomatic complexity back below 10; add ExchangeOrigin
typed enum (ExchangeOriginStandalone / ExchangeOriginLadder).

Tests added:
- TestRunAutoExchange_StandaloneDoesNotCancelLadderPendings (regression for G10)
- TestRunAutoExchange_LadderDoesNotCancelStandalonePendings
- TestRunAutoExchange_DryRun_ManualMode_ZeroMutations (mock.AssertExpectations)
- TestRunAutoExchange_DryRun_AutoMode_ZeroMutations
- TestRunAutoExchange_LadderRunID_StampedOnRecords (round-trip)
- TestRunAutoExchange_NoLadderRunID_RecordHasNilLadderRunID
- TestPGXMock_CancelPendingExchangesByOrigin_{Standalone,Ladder}
- TestPostgresStore_CancelPendingExchangesByOrigin_NilDB
…m (L3 review)

Address Fable FIX-THEN-SHIP findings on the L3 diff:

1. [HIGH] Make ladder-side scoping reachable and honest. Widen the
   exchangeRunner.RunAutoExchange seam to carry (ladderRunID *string, dryRun
   bool) as explicit params, add BufferReshapeConfig.LadderRunID, and have
   ReshapeBuffer forward both. The concrete runner (L16) is now FORCED to
   thread LadderRunID, so it cannot default to nil and cancel standalone
   pendings (the G10 bug). Correct the reshape.go comments: standalone scoping
   is active now; ladder-side scoping activates when the runner is wired (L16).
   Remove the stale "dry-run not supported" rejection; DryRun is forwarded.

3. [MEDIUM] Replace the bare `ladderScoped bool` with a typed
   common.ExchangeOrigin enum end-to-end (exchange.RIExchangeStore,
   config.StoreInterface, PostgresStore, adapter, all mocks). The enum moves to
   pkg/common (no heavy SDK deps) so internal/config can import it. PostgresStore
   validates the origin and fails loud on an unknown value.

2. [MEDIUM] Document the deliberate coarse IS NOT NULL partition (cancels all
   ladder-linked pendings, not per-run) with a TODO(#1367) at the SQL.

4. [MEDIUM] Tighten the pgxmock branch tests to match `ladder_run_id IS NULL`
   vs `IS NOT NULL` (mutually exclusive regexes) so a branch mix-up fails;
   add an unknown-origin fail-loud test and a 21-arg SaveRIExchangeRecord
   INSERT column-alignment test.

5. [LOW] Document the FK ON DELETE SET NULL reclassification (deleting a ladder
   run makes its still-pending reshape standalone-cancellable) as known/acceptable.

6. [NIT] Touch updated_at in the scoped cancel queries for parity.

Verified fail-before/pass-after: swapping the two WHERE clauses fails both
TestPGXMock_CancelPendingExchangesByOrigin_{Standalone,Ladder}; correct SQL
passes.
@cristim
cristim force-pushed the feat/ladder-reshape-scoped-cancel branch from 5079701 to 5b9a8e5 Compare July 16, 2026 13:11
@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

Rebased onto post-#1362 main (7adacd715), resolving the 4 reported textual conflicts. All were identical, trivial import-block collisions (this branch adds pkg/common, #1362 added pkg/ladder); resolved by keeping both import lines (union) in:

  • internal/config/interfaces.go
  • internal/mocks/stores.go
  • internal/server/test_helpers_test.go
  • internal/analytics/collector_test.go

No semantic conflict: main's factory.go WithWriteSide is satisfied by the widened RunAutoExchange seam, and all StoreInterface implementers satisfy both widenings.

Union gates on the rebased head 5b9a8e525 (all exit 0):

Gate Result
go build ./... (root) exit 0
go vet ./... (root) exit 0
cd pkg && go test ./exchange/... 100 passed
go test ./internal/server/... ./internal/config/... 1041 passed
cd providers/aws && go build && go vet && go test ./ladder/... 115 passed
go test -tags=integration ./internal/config/... 749 passed

@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit 35410ee into main Jul 16, 2026
14 of 17 checks passed
@cristim
cristim deleted the feat/ladder-reshape-scoped-cancel branch July 16, 2026 13:25
@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

Merged to main after: two Fable review rounds (typed ExchangeOrigin enum, correct-by-construction ladder seam forcing LadderRunID/DryRun through, pgxmock WHERE-clause + 21-arg INSERT alignment tests), CodeRabbit clean, rebase onto post-#1362 main (4 import-union conflicts), and a local union re-verify against post-#1365 main (build/vet/ladder tests all exit 0) since both PRs touched providers/aws/ladder/interfaces.go. Standalone and ladder pending-exchange cancellation are now origin-scoped and cannot cross-cancel; per-run scoping before ladder reshapes create pendings is tracked in #1367 (needed with L16).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/feat New capability urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant