feat(executor): lock_timeout + bounded retry for native DDL - #27
Merged
Conversation
Every transactional native DDL statement now runs under SET LOCAL lock_timeout/statement_timeout with a bounded jittered-backoff retry loop; only SQLSTATE 55P03 lock timeouts retry, exhaustion returns a typed BudgetError. CREATE INDEX CONCURRENTLY stays outside the wrapper by design (non-transactional; lock_timeout can cancel its snapshot waits and strand an invalid index) and keeps its statement_timeout + invalid-index recovery contract.
Review findings on the bounded-retry path: the flag-to-policy wiring was untested, zero backoff slipped validation while attempts remained plural, the AttemptNative wrapper had become a stale-doc delegation shim, and exhausted attempt counts were invisible to operators and automation triaging a refusal.
Kiran01bm
marked this pull request as ready for review
August 12, 2026 04:37
Kiran01bm
requested review from
JashLal,
aparajon,
eeSeeGee,
jayjanssen,
jemiahw and
morgo
as code owners
August 12, 2026 04:37
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
morgo
approved these changes
Aug 12, 2026
morgo
left a comment
Collaborator
There was a problem hiding this comment.
🤖 Approved by Morgan's AI agent (moderate-risk bar for pg-sprite): bounded lock_timeout retry is correctly scoped — only SQLSTATE 55P03 retries, each attempt in a fresh transaction so SET LOCAL can't leak through the pool, policy validation rejects unbounded/zero-backoff configs, and the capped-doubling backoff matches its tests. ST-7 target proof still gates every attempt. Nice touch exposing the exhausted attempt count in the verdict JSON for automation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every transactional native DDL statement now executes under
SET LOCAL lock_timeout/statement_timeoutwith a bounded exponential-backoff retry loop, so a DDL statement can never sit in the lock queue blocking production traffic queued behind it. Only SQLSTATE55P03(lock timeout) retries; exhaustion returns a typedBudgetErrorrecording the attempt count.What
pkg/executor/optimistic.go: configurableRetryPolicy(defaults: 3 attempts, 100ms initial backoff, 1s cap),ExecuteNativerunning each attempt in a fresh transaction with transaction-local timeouts, an injectable sleeper seam for deterministic tests;57014(query canceled / statement timeout) deliberately does not retry. Validation rejects zero backoff whenever more than one attempt is configured.internal/cli/cli.go,internal/cli/migrate.go: all existing transactional native execution routed throughExecuteNative; attempts/backoff configurable via flags (1disables retry) with safe defaults preserved for zero-valued programmatic construction, and parsing tests proving the flags reach the policy.pkg/verdict: refusal verdicts carryattempts(JSON and human output), and the debug log records the exhausted attempt count, so operators and automation can tell an exhausted bounded retry from a single-attempt refusal.Why
DDL lock acquisition in PostgreSQL queues behind long-running transactions, and everything else then queues behind the DDL — an unbounded wait converts one slow reader into an outage. Transaction-local timeouts (
SET LOCAL) cannot leak into pooled connections.CREATE INDEX CONCURRENTLYstays outside the wrapper by design: it cannot run in a transaction, andlock_timeoutcan cancel its safe snapshot waits and strand an invalid index — it keeps itsstatement_timeout+ invalid-index recovery contract.Before / after