Skip to content

feat(purchase): recheck ladder switches before any provider call (#118 PR4r) - #848

Merged
cristim merged 2 commits into
mainfrom
feat/ladder-exec-recheck
Oct 13, 2026
Merged

cristim merged 2 commits into
mainfrom
feat/ladder-exec-recheck

Conversation

@cristim

@cristim cristim commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Refs #118. Inert: nothing creates ladder executions yet (no schema, no task wiring), so this changes behavior only for rows whose idempotency key is a ladder tranche id.

What

executeAndFinalize now rechecks, before any provider call, that an execution belonging to a ladder tranche is still allowed to buy: LadderingEnabled and LadderExecutionEnabled are on, the ladder config is enabled with mode auto_approve, and its run is not canceled. The check sits in preExecutionRefusal beside the armed re-drive and stale Azure pricing refusals, so every executor entry is covered (approve, scheduler fire, claimAndExecute for SQS and the cron sweep, stranded re-drive).

Details

  • Tranche lookup: idempotency_key compared to ladder_tranches.id as TEXT (GetLadderExecutionGate, one query joining config and run). Non-UUID keys (<root>:<acct> multi-account, legacy ExecutionIDs, empty) match nothing and are not ladder executions. Only a real DB error refuses (fail closed). A deleted config or run (FK is ON DELETE SET NULL) also refuses.
  • A refusal is a beforeSubmission error: the row ends failed with no_submission stamped. The ladder reconciler reads that proof (decideFiredTranche) to fail the tranche and release netting.
  • Accepted residual race: a switch flipped after the recheck passes and before the provider call (milliseconds; no lock is held across a provider call).

Verification

  • Unit (mock store, counting fake provider; FIXTURE): flag off, planning off, config disabled, mode email_approval, run canceled, config/run deleted, lookup error, global config error/nil all refuse with 0 provider calls and failed+no_submission; all-on buys once; multi-account, legacy and empty keys are unaffected with flags off; each of the four entries refuses.
  • Real PG17 (throwaway local cluster) for the text compare: non-UUID keys return no error; deleted config/run come back nil.
  • Mutations each fail tests: check dropped (16 failures), refusal not wrapped as beforeSubmission (11), lookup error fails open (2), t.id = $1::uuid compare (integration test fails with 22P02).
  • golangci-lint (go1.26.9) 0 issues, gocyclo ok, git-secrets clean, go vet -tags=integration ok.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Ladder purchases are now checked against current configuration and run status immediately before submission. Purchases are stopped if required settings are unavailable, disabled, or no longer eligible; non-ladder purchases are unaffected.
    • Refused purchases are recorded as failed without being sent to the provider.

…PR4r)

Ladder executions can wait approved or scheduled for hours. Re-read the
global ladder flags, the ladder config (enabled, auto_approve) and the run
(not canceled) in executeAndFinalize, so every executor entry (approve, fire,
claimAndExecute/SQS, re-drive) is covered. The tranche is found by comparing
the idempotency key to ladder_tranches.id as text; other key shapes are not
ladder executions. A lookup error refuses. A refusal is a beforeSubmission
error, so the row ends failed with no_submission and the reconciler can
release netting. Inert: nothing creates ladder executions yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim cristim added urgency/this-sprint Within the current sprint priority/p1 Next up; this sprint severity/high Significant harm impact/few Limited audience effort/m Days type/bug Defect triaged Item has been triaged labels Oct 13, 2026
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

  • Run on-demand review

This review includes 9 billable files and costs up to $2.25.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 37 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available. Your 59 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 5eaac554-5460-400c-ac98-bcab233c52f5
📥 Commits

Reviewing files that changed from the base of the PR and between 2fedce2 and 401bd98.

📒 Files selected for processing (9)
  • internal/analytics/collector_test.go
  • internal/config/interfaces.go
  • internal/config/store_postgres_ladder_gate_integration_test.go
  • internal/config/store_postgres_ladder_reconcile.go
  • internal/mocks/stores.go
  • internal/purchase/ladder_execution_recheck.go
  • internal/purchase/ladder_execution_recheck_test.go
  • internal/purchase/manager.go
  • internal/server/test_helpers_test.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: fe4a5b0c-926d-45a1-bfec-96e8435a2e21
📥 Commits

Reviewing files that changed from the base of the PR and between f606770 and 2fedce2.

📒 Files selected for processing (9)
  • internal/analytics/collector_test.go
  • internal/config/interfaces.go
  • internal/config/store_postgres_ladder_gate_integration_test.go
  • internal/config/store_postgres_ladder_reconcile.go
  • internal/mocks/stores.go
  • internal/purchase/ladder_execution_recheck.go
  • internal/purchase/ladder_execution_recheck_test.go
  • internal/purchase/manager.go
  • internal/server/test_helpers_test.go

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

📝 Walkthrough

Purchase execution now rechecks ladder configuration and run state before provider submission. The PostgreSQL store exposes gate data by idempotency key. Refusals cover unavailable global settings, invalid or missing ladder configuration, and missing or canceled runs. Non-ladder keys continue without a gate.

Changes: Ladder execution safeguards

Layer / File(s) Summary
Ladder gate data and store access
internal/config/interfaces.go, internal/config/store_postgres_ladder_reconcile.go, internal/config/store_postgres_ladder_gate_integration_test.go, internal/mocks/stores.go, internal/analytics/collector_test.go, internal/server/test_helpers_test.go
The store interface and PostgreSQL implementation expose ladder configuration and run status by idempotency key. Integration tests cover matched and unmatched keys, canceled runs, and missing config or run records. Mocks implement the new lookup.
Pre-submission ladder checks
internal/purchase/ladder_execution_recheck.go, internal/purchase/ladder_execution_recheck_test.go
The recheck refuses execution on lookup errors, unreadable or disabled global flags, disabled or non-auto_approve ladder configuration, and missing or canceled runs. Tests cover refusal cases, permitted ladder execution, and unaffected non-ladder keys.
Execution entry-point wiring
internal/purchase/manager.go, internal/purchase/ladder_execution_recheck_test.go
executeAndFinalize runs pre-execution checks before executePurchase. Tests cover refusal through approval, claim-and-execute, scheduled firing, and stranded-approval recovery paths.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Executor
  participant PostgresStore
  participant GlobalConfig
  participant Provider
  Executor->>PostgresStore: GetLadderExecutionGate(idempotencyKey)
  PostgresStore-->>Executor: Gate fields or nil gate
  Executor->>GlobalConfig: Read ladder flags for a matching gate
  GlobalConfig-->>Executor: Flag values or read error
  alt Checks pass
    Executor->>Provider: Execute purchase
  else A check refuses
    Executor-->>Executor: Refuse before provider call
  end
Loading
Merge Risk: ⚪ Minimal · up to 2fedc

No identified issue blocks merging after normal checks.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 9 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly summarizes the main change: rechecking ladder switches before a provider call. The parenthetical identifier is extra but does not obscure the purpose.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@cristim

cristim commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

Gate verdict: APPROVE (money path, independent adversarial review)

Reviewed SHA: 401bd989b9e08c0f1b780ffd014136a390a311a7 (update-branch merge of main 3c5a62b into reviewed 2fedce21; the PR's own diff vs main is byte-identical before and after, 9 files +405/-6).

Paths traced (source): the only provider call (execution.go:1154 PurchaseCommitment) is reached only via executePurchase, whose single caller is executeAndFinalize (manager.go). All entries funnel there after a running claim: approve / DirectExecute / RunPlannedPurchaseNow (approvals.go:425), scheduler fire (scheduled_fire.go:100), claimAndExecute for SQS and the cron sweep (manager.go:214), stranded re-drive (manager.go:580). Every claim returns the row with idempotency_key (RETURNING at store_postgres.go:1263-1270; GetStaleApprovedExecutions selects it), so the key the recheck reads is never blank by accident. Retry successors keep the key verbatim (handler_purchases.go ~2144), so they are rechecked too. Multi-account children are only reached after the root passes.

Lookup: t.id::text = $1 with LEFT JOINs; non-UUID keys and no match come back (nil, nil); only a real DB error refuses. Column names and values checked against migrations 000079/80/81/83 and the go pin (auto_approve, cancelled). Single read-only pool query, no lock held across the provider call. The residual race (switch flipped between recheck and provider call) is documented in code and in the PR body.

Refusal class: wrapped as beforeSubmission, so the row ends failed with no_submission. decideFiredTranche (ladder_reconcile.go:61) fails the tranche only when the whole retry chain has that proof, and stays fired while a successor is linked, so netting is released only when nothing bought.

Local evidence (git archive at exact head, GOTOOLCHAIN=go1.26.9, throwaway PG17 17.11 cluster, no Docker, under the suite lock):

  • go vet ./... and go vet -tags integration ./internal/...: exit 0
  • go test ./...: exit 0
  • go test -tags integration ./internal/api/... ./internal/config/ ./internal/purchase/: all ok (TestGetLadderExecutionGate and TestLadderReconcile_* ran and passed, they did not skip)
  • Fails-before: main's manager.go with the PR's tests gives 16 failures (all refusal cases and all 4 entry paths)
  • 17 mutants, 16 killed: check dropped, $1::uuid compare (22P02), lookup error fails open, not beforeSubmission, skip scheduler fire / approve / re-drive+SQS, mode / run-cancel / both flags / config-disabled checks removed, INNER JOIN runs / configs, match on execution_id, gate-nil refuses (84 failures in non-ladder suites)
  • git-secrets with --register-aws, BSD and GNU grep: clean
  • CI: 25/25 SUCCESS, mergeStateStatus CLEAN at 401bd98

Follow-ups (non-blocking):

  1. Surviving mutant: GetLadderExecutionGate returning (nil, nil) on a real DB error (store_postgres_ladder_reconcile.go, the read ladder execution gate branch) is not caught by any test. That would fail open. The code is correct today, but a pgxmock unit test for the error branch would pin it.
  2. t.id::text = $1 cannot use the PK index, so every purchase sequential-scans ladder_tranches. That is fine at today's size. Parsing as a UUID first and comparing t.id = $1::uuid only for valid UUIDs would use the index.
  3. A tranche with NULL run_id is always refused ("run no longer exists"). PR4a must set run_id on every tranche it fires.
  4. Until the reconciler is wired, a refused tranche stays fired, so Retry can still mint a successor. That successor is rechecked and the original bought nothing, so this cannot double-buy.

@cristim
cristim merged commit fc55fc7 into main Oct 13, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/few Limited audience priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant