Skip to content

fix: bound the alias lock wait in migration apply - #1747

Merged
bcdonadio merged 1 commit into
mainfrom
fix/1691-s22
Oct 25, 2026
Merged

bcdonadio merged 1 commit into
mainfrom
fix/1691-s22

Conversation

@bcdonadio

Copy link
Copy Markdown
Contributor

Why

Fixes #1691. During lcm migration apply, reconciling the destination project's aliases takes LOCK TABLE lcm.project_aliases IN SHARE ROW EXCLUSIVE MODE with no lock deadline. If another session holds a conflicting alias write, the lock waits until the session statement_timeout (60 s by default) cancels it, and apply fails with a generic storage error instead of a closed refusal. Nothing is written and no hold is taken, but the operator gets no actionable reason.

How

  • ensureProjectAliases sets a transaction-local lock_timeout of 5 s for the LOCK statement only, and restores the previous value before the reconcile read, so later inserts keep their normal deadlines.
  • A 55P03 (lock timeout) or 57014 (statement timeout) raised by that LOCK becomes the new PostgreSqlIdentityAliasLockTimeoutError (non-retryable, project-scoped, no path or alias data).
  • The translation happens after executor.transaction rejects, and only when the rejection is the exact error recorded from the LOCK. The PostgreSQL runtime rethrows the first failed query's error ahead of any error thrown by the callback, so translating inside the callback would be discarded.
  • resolveMigrationTargetProjectId maps the typed error to migration-target-identity-unresolvable with evidence alias-lock-timeout (exit 5, before the hold), on both the existing-project and post-create paths. Other errors still propagate.
  • The table lock stays. A per-project advisory lock would only exclude writers that take it, and the other alias writers do not.

Testing

  • Repository unit tests model the runtime's error precedence and check the statement order, the 55P03/57014 mapping at the LOCK, and that the same codes from later statements, set_config failures, other SQLSTATEs and non-storage errors stay unmapped. New tests fail at the base commit.
  • Target-identity tests cover both adopt paths and unchanged alias-conflict behaviour.
  • Live PostgreSQL (real runtime, statementTimeoutMs 30000): with an uncommitted alias insert in another session, reconciliation rejects with the typed error in under 15 s, apply's resolver refuses alias-lock-timeout, nothing is written, and reconciliation succeeds once the blocker rolls back. test/postgresql/identity.integration.ts passes 13/13, including the existing wait-then-commit race.
  • Focused unit run: 97 passed; identity-repository.ts and target-identity.ts at 100% statements, branches, functions and lines. pnpm run typecheck and eslint pass.

Reviewer guide

Start with ensureProjectAliases in src/storage/postgresql/identity-repository.ts (the lock-timeout setup, provenance capture and outside translation), then adopt in src/migration/target-identity.ts. Docs: the alias-lock-timeout evidence in docs/migration-operations.md. Metadata: patch changeset and test/codecov-ownership/1691-alias-lock-timeout.yml.

ensureProjectAliases took LOCK TABLE lcm.project_aliases with no lock
timeout of its own. While another session held an uncommitted alias
write, apply waited until statement_timeout cancelled the statement
and then reported a generic storage error instead of a closed
pre-hold refusal.

The reconcile now sets a transaction-local lock_timeout of 5 s around
the LOCK statement only, restoring the prior setting once the lock is
held. A 55P03 or 57014 failure of that statement becomes the typed
PostgreSqlIdentityAliasLockTimeoutError, and target-identity maps it
to migration-target-identity-unresolvable with evidence
alias-lock-timeout. Nothing is written and no hold is taken.

The translation happens outside executor.transaction: the real
runtime rethrows the first failed query's cached error ahead of the
callback's error, so a typed error thrown inside the callback would
be replaced. The wrapper only maps when the rejection is the very
error recorded from the LOCK statement; later-statement timeouts and
set_config failures stay unchanged. A 57014 at the LOCK is attributed
to the configured statement_timeout; a caller cancellation is
indistinguishable and accepted.

The table lock stays. A per-project lock would not exclude the other
alias writers, which never take it.

Refs #1691

Signed-off-by: Bernardo Donadio <bcdonadio@bcdonadio.com>
@bcdonadio bcdonadio self-assigned this Oct 25, 2026
@bcdonadio
bcdonadio enabled auto-merge October 25, 2026 04:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-25T04:11:07.538025Z 7d90e52 PR opened
🔒 Security Review ✅ Completed 2026-10-25T04:12:31.439781Z 7d90e52 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@bcdonadio
bcdonadio merged commit c9342f8 into main Oct 25, 2026
21 checks passed
@bcdonadio
bcdonadio deleted the fix/1691-s22 branch October 25, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migration alias reconcile lock wait surfaces as a storage error

1 participant