Skip to content

fix(client): serialize pgmq schema install across clients and processes - #398

Merged
mhenrixon merged 1 commit into
mainfrom
fix/pgmq-install-race
Aug 12, 2026
Merged

fix(client): serialize pgmq schema install across clients and processes#398
mhenrixon merged 1 commit into
mainfrom
fix/pgmq-install-race

Conversation

@mhenrixon

@mhenrixon mhenrixon commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • ensure_pgmq_schema is now serialized process-wide (class-level mutex) instead of per-instance — synchronized was a no-op on the dedicated-connection path, and on the shared-AR Proc path two instances each held their own mutex around one shared libpq connection, so concurrent bootstrap DDL could desync the protocol (message type 0x… arrived from server while idle) and wedge a thread forever.
  • Check+install runs in one explicit transaction holding pg_advisory_xact_lock(PGMQ_INSTALL_LOCK_KEY) — cross-process serialization that self-releases at COMMIT/ROLLBACK and is safe through transaction-pooling poolers (a session lock's unlock could land on a different server connection).
  • A duplicate-object install failure (PG::UniqueViolation, PG::Duplicate* — a process without the advisory lock won the race) is rescued by re-checking pgmq.meta: present → proceed as installed; absent → re-raise wrapped in SchemaNotReady.

Downstream forensics: getzazu/app#3413 (merge-queue CI_TIMEOUT shard evictions).

Closes #397

Test plan

  • New specs: transaction/advisory-lock framing order, duplicate-loser rescue (both re-check outcomes), process-wide serialization across two client instances (latch-based, deterministic failure direction)
  • bundle exec rspec spec/pgbus/ spec/generators/ — 3854 examples, 0 failures, coverage floors pass
  • bundle exec rubocop clean on changed files

Summary by cubic

Serialize PGMQ schema installation across client instances and processes to eliminate install races that caused PG::UniqueViolation, protocol desyncs on shared libpq connections, and threads wedged on socket reads. Previously each client used a per-instance guard; now installs are process- and cross-process safe.

  • Process-wide serialization: replace per-instance synchronized with a class-level mutex around ensure_pgmq_schema.
  • Cross-process serialization: wrap check+install in a single transaction holding pg_advisory_xact_lock(PGMQ_INSTALL_LOCK_KEY); the xact-scoped lock self-releases and is safe with transaction-pooling poolers.
  • Duplicate install handling: treat PG::UniqueViolation/PG::Duplicate* as a race loss, re-check pgmq.meta, and proceed if present; otherwise re-raise as SchemaNotReady.
  • Tests: new specs cover lock/transaction framing, duplicate-race recheck behavior, and deterministic process-wide serialization across two client instances.
  • No migration required; external API and behavior remain the same aside from race-free bootstrap.

Written for commit 7b6d493. Summary will update on new commits.

Review in cubic

ensure_pgmq_schema's @schema_ensured + synchronized guard is per-instance
(and synchronized is a no-op on the dedicated-connection path), so two
clients could run check+install concurrently: the loser's UniqueViolation
surfaced as SchemaNotReady, and on the shared-AR Proc path the concurrent
install DDL on one shared libpq connection desynced the protocol and
wedged a thread on a socket read forever (getzazu/app#3413).

- Serialize bootstrap process-wide via a class-level mutex.
- Run check+install in one transaction holding pg_advisory_xact_lock on a
  fixed key — cross-process serialization, xact-scoped so it self-releases
  and stays safe through transaction-pooling poolers.
- Rescue duplicate-object install failures by re-checking pgmq.meta:
  present = another process won, proceed; absent = re-raise.

Closes #397
@cubic-dev-ai

cubic-dev-ai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Running ultrareview automatically — This change reworks concurrency-sensitive schema bootstrap (process-wide mutex, pg_advisory_xact_lock, transaction/error framing) where a subtle bug could wedge connections or corrupt install state across processes — worth a deep review.. I'll post findings when complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultrareview completed in 10m 33s

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread lib/pgbus/client.rb
Comment thread spec/pgbus/client_spec.rb
@mhenrixon
mhenrixon merged commit a00df23 into main Aug 12, 2026
13 checks passed
@mhenrixon
mhenrixon deleted the fix/pgmq-install-race branch August 12, 2026 14:49
@mhenrixon mhenrixon self-assigned this Aug 12, 2026
mhenrixon added a commit that referenced this pull request Aug 12, 2026
…nstall

The shared Proc-supplied PG::Connection is single-owner: every access must
hold @pgmq_mutex. The new transaction-status probe ran outside it, and the
#397/#398 rework had swapped ensure_pgmq_schema's synchronized for the
class-level install mutex — silently dropping the connection-ownership
guard the original code had. Both now hold synchronized (nested inside the
class install mutex; safe order, nothing acquires them reversed).

Also dedupes the create+tune DDL pair shared by queue and DLQ creation.

Refs #399 review
mhenrixon added a commit that referenced this pull request Aug 12, 2026
…nsaction (#399)

* fix(client): schema install must not commit or destroy a caller's transaction

Follow-up to #398 review (P1 + P3):

- The Proc-supplied shared connection can arrive mid-transaction
  (perform_later inside an application `transaction do`); the unconditional
  BEGIN was a warning no-op there, so the matching COMMIT/ROLLBACK operated
  on the CALLER's transaction. The framing is now transaction_status-aware:
  idle -> owned BEGIN..COMMIT, in-transaction -> SAVEPOINT/RELEASE with
  ROLLBACK TO SAVEPOINT on failure.
- The process-wide serialization spec's fixed `sleep 0.05` could false-pass
  on a saturated scheduler; it now waits deterministically for thread B to
  block or terminate before asserting zero connection traffic.

Refs #398

* fix(client): don't cache schema_ensured from a savepoint-path install

A savepoint-path ensure rides the caller's transaction, so the install is
only durable once THAT commits — caching @schema_ensured there means an
outer rollback leaves the schema missing while every future check is
skipped. Only the owned-COMMIT path caches now; the savepoint path
re-checks on the next ensure (one SELECT).

Also: the serialization spec's settle-wait now fails explicitly if thread B
never settles, instead of silently degrading back into a fixed-duration
window.

Refs #399 review

* fix(client): queue-creation cache follows the same durability rule

Queue DDL on the shared Proc-supplied connection joins the caller's open
transaction; caching @queues_created there outlives a caller rollback, so
later ensures skip recreation and message operations fail against a
missing queue. When the DDL rides a caller's transaction the queue is
created (idempotent CREATE IF NOT EXISTS) but not cached; dedicated-path
DDL runs on pgmq-ruby's own pool connections and caches as before.

Refs #399 review

* fix(client): hold the connection mutex for the txn probe and schema install

The shared Proc-supplied PG::Connection is single-owner: every access must
hold @pgmq_mutex. The new transaction-status probe ran outside it, and the
#397/#398 rework had swapped ensure_pgmq_schema's synchronized for the
class-level install mutex — silently dropping the connection-ownership
guard the original code had. Both now hold synchronized (nested inside the
class install mutex; safe order, nothing acquires them reversed).

Also dedupes the create+tune DDL pair shared by queue and DLQ creation.

Refs #399 review
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.

ensure_pgmq_schema: concurrent install race across clients/processes can desync a shared libpq connection

1 participant