pgbus 0.13.7 → 0.13.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36e4016b7a3103001ac29bb75d10c949b7cc8866a1ef511ad64a1ca61643e14a
4
- data.tar.gz: 96897f60545b8ac14ec13d1cf267fecf408e690badb41973e0c9f7a9eb6967c3
3
+ metadata.gz: 403234bf91b0ae06bd9c1331b6356450c98cc7a062a7ebcfb45f159950d07c2c
4
+ data.tar.gz: a314687f0071c9588433dcd452c146178cc1afc52a2e46a675e998004bdbef2f
5
5
  SHA512:
6
- metadata.gz: 64601019ee0ffebe7babed1295b471aed0a116ee68efa25e11216c0709a3e217a31020e0d4f99abee78b17891f794d679db54bef293b2b1193815cc4df3cb721
7
- data.tar.gz: 1524b6c8e4434d81e433c1a550259c44e5e47c308fbd594ae1777ae22472040504b97e173e9d4a2b11102ba1a7a79ab52ad7dd7c9efcc12c16f007cd48f779fb
6
+ metadata.gz: 80ce61932ee3f9c34562c353219ae5c19a2a1b312c8418e6ed645e32c66ac2e7f590edd7b9cc861e4f13b4bd784a7f2712d4b45b11f0a0c49b30fa117a664501
7
+ data.tar.gz: 91856c566af986f8de528c40e036a00548adf0d7bb0d6e1fc96abb887120613df0b9f8fb22a04292bf4bb794baf06e6a7668b823b05fed730d52b420ff5cfc9b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ### Fixed
4
4
 
5
+ - **`StreamQueue.record!` no longer depends on Rails' pool schema-cache index resolution — one bad probe stopped poisoning stream registration for the process lifetime (issue #401).** `record!` used `upsert(unique_by: :queue_name)`, which resolves the unique index through the connection pool's schema cache. That cache stores a negative `data_source_exists?` answer permanently, and `SchemaCache#indexes` returns `[]` (uncached) whenever the cached probe says false — while the `table_exists?` guard at the top of `record!` is a live query. So a single wrong first probe on the pool cache (observed under PgBouncer transaction pooling in production, and from a coalescer flush thread racing foreground test DB work in CI) made the guard pass and the upsert raise `ArgumentError: No unique index found for queue_name` — swallowed at DEBUG — on **every** subsequent `record!` in that process until restart, leaving streams unregistered from that process's perspective (maintenance, orphan sweep, and wildcard classification degrade). The registry write is now a raw `INSERT … ON CONFLICT (queue_name) DO NOTHING` on the model's connection: the unique index is owned by the gem's own migration, so there is nothing for Rails to resolve, no schema-cache traffic leaves the hot first-broadcast path, and a poisoned cache can no longer break registration (pinned by an integration regression spec that deliberately poisons the pool cache, plus a `sql.active_record` assertion that no `SCHEMA` query is issued once the `table_exists?` memo is warm). Failure logging is now class-aware: a transient database error (`ActiveRecord::ActiveRecordError`) still logs at DEBUG per attempt, but a non-database failure — the bug-signal class the old `ArgumentError` belonged to — logs at WARN once per process (DEBUG thereafter) instead of drowning a process-lifetime malfunction in per-broadcast DEBUG spam. Return values and `backfill!`/`all_names` cache semantics are unchanged. Refs #401.
6
+
5
7
  - **The schema-install transaction framing no longer commits or destroys a caller's open transaction (#398 review follow-up).** The #397 fix wrapped check+install in `BEGIN`…`COMMIT`/`ROLLBACK` unconditionally. On the Proc-supplied shared-connection path (the Rails lambda), the connection can arrive **mid-transaction** — e.g. `perform_later` inside an application `transaction do` block — where `BEGIN` is a warning-level no-op and the matching `COMMIT`/`ROLLBACK` then commits half of, or destroys, the *caller's* transaction. The framing is now `transaction_status`-aware: an idle connection gets the owned `BEGIN`…`COMMIT` as before; a connection already inside a transaction rides it via `SAVEPOINT pgbus_pgmq_install` / `RELEASE` (`ROLLBACK TO SAVEPOINT` on failure), so the caller's transaction is never touched. On the savepoint path the advisory lock joins the caller's transaction and is held until it ends — over-holding only delays a concurrent installer, never corrupts it — and `@schema_ensured` is NOT cached there: the install is only durable once the caller commits, so a cached true after an outer rollback would skip every future check against a missing schema. The same durability rule now governs `@queues_created`: queue DDL on the shared Proc-supplied connection joins the caller's open transaction, so queue creation there runs uncached (idempotent `CREATE IF NOT EXISTS`) and the next ensure re-checks — a cache write outliving a caller rollback would make later message operations fail against a missing queue. All shared-connection access in these paths — including the transaction-status probe and the schema install itself — holds the per-instance connection mutex, restoring the single-owner invariant the #397 fix had narrowed. Refs #398.
6
8
 
7
9
  - **PGMQ schema installation is now race-safe across clients and processes (issue #397).** `ensure_pgmq_schema` guarded check+install with `@schema_ensured` + `synchronized` — both per-instance, and `synchronized` is a no-op on the dedicated-connection path — so two Client instances (or two threads on the dedicated path) could install concurrently. The loser's `PG::UniqueViolation` (`Key (nspname)=(pgmq) already exists`) surfaced as `SchemaNotReady` even though the schema was fine, and on the shared-AR Proc path — where two instances each hold their *own* mutex around one shared libpq connection — the concurrent install traffic desynced the protocol (`message type 0x… arrived from server while idle`) and left a thread blocked on a socket read forever (downstream forensics: a CI shard going silent until the merge queue's timeout evicted the PR, getzazu/app#3413). Three changes: **(1)** schema bootstrap is serialized process-wide through a class-level mutex, not per-instance state; **(2)** check+install runs inside one explicit transaction holding `pg_advisory_xact_lock` on a fixed key (`Pgbus::Client::PGMQ_INSTALL_LOCK_KEY`), serializing installers across processes — xact-scoped so the lock releases itself at COMMIT/ROLLBACK and stays safe through transaction-pooling poolers, where a session lock's unlock could land on a different server connection; **(3)** a duplicate-object install failure (`PG::UniqueViolation`, `PG::DuplicateSchema`, `PG::DuplicateTable`, `PG::DuplicateObject`, `PG::DuplicateFunction` — a process without the advisory lock, e.g. older pgbus or the extension path, won the race) is rescued by re-checking `pgmq.meta`: present means proceed as installed, absent means the original error is re-raised wrapped in `SchemaNotReady`. Refs #397.
@@ -25,17 +25,34 @@ module Pgbus
25
25
  class StreamQueue < BusRecord
26
26
  self.table_name = "pgbus_stream_queues"
27
27
 
28
+ # Serializes the WARN-once latch in log_record_failure: record! runs on
29
+ # the coalescer flush thread as well as callers' threads, and an
30
+ # unsynchronized check-and-set could WARN more than once per process.
31
+ @record_failure_mutex = Mutex.new
32
+
28
33
  class << self
29
- # Upserts the physical queue name. Idempotent and cheap to call on
34
+ # Inserts the physical queue name. Idempotent and cheap to call on
30
35
  # every broadcast; the caller (`ensure_stream_queue`) also memoizes
31
36
  # per-process, so the DB write happens once per stream per process.
32
37
  # Errors are swallowed — a registry hiccup must never abort a broadcast.
33
38
  # Returns true on a successful write, false when the table is absent or
34
- # the upsert failed (so callers like `backfill!` can report accurately).
39
+ # the insert failed (so callers like `backfill!` can report accurately).
40
+ #
41
+ # Deliberately raw SQL rather than `upsert(unique_by:)` (issue #401):
42
+ # Rails resolves `unique_by:` through the pool's schema cache, which
43
+ # caches a negative `data_source_exists?` probe permanently — one wrong
44
+ # first probe (while the table genuinely exists and the live
45
+ # `table_exists?` guard above passes) poisons every subsequent record!
46
+ # in the process with "No unique index found". The unique index is owned
47
+ # by this gem's own migration, so there is nothing to resolve.
35
48
  def record!(queue_name)
36
49
  return false unless table_exists?
37
50
 
38
- upsert({ queue_name: queue_name }, unique_by: :queue_name)
51
+ conn = connection
52
+ conn.execute(
53
+ "INSERT INTO #{conn.quote_table_name(table_name)} (queue_name) " \
54
+ "VALUES (#{conn.quote(queue_name)}) ON CONFLICT (queue_name) DO NOTHING"
55
+ )
39
56
  # Keep the in-process cache consistent with the write so a subsequent
40
57
  # stream? check reflects this registration without a re-query. Only
41
58
  # update an ALREADY-LOADED cache — if @all_names is still nil (this
@@ -43,12 +60,15 @@ module Pgbus
43
60
  # fabricate a one-entry set and silently hide every other
44
61
  # already-registered stream until the next reset_cache!. Leaving it
45
62
  # nil lets the next all_names call do a real load, which already
46
- # includes this row since the upsert above has committed.
63
+ # includes this row since the insert above has committed.
47
64
  @all_names&.add(queue_name)
48
65
  true
49
- rescue StandardError => e
66
+ rescue ActiveRecord::ActiveRecordError => e
50
67
  Pgbus.logger.debug { "[Pgbus] Failed to record stream queue #{queue_name}: #{e.message}" }
51
68
  false
69
+ rescue StandardError => e
70
+ log_record_failure(queue_name, e)
71
+ false
52
72
  end
53
73
 
54
74
  # Set of all registered physical stream queue names. Memoized so a
@@ -124,6 +144,18 @@ module Pgbus
124
144
 
125
145
  private
126
146
 
147
+ # A non-ActiveRecord error out of a plain INSERT is a bug signal (the
148
+ # old path's ArgumentError from index resolution was one), not a DB
149
+ # hiccup — surface it at WARN once per process instead of drowning a
150
+ # process-lifetime malfunction in per-broadcast DEBUG spam.
151
+ def log_record_failure(queue_name, error)
152
+ message = "[Pgbus] Failed to record stream queue #{queue_name}: #{error.class}: #{error.message}"
153
+ first = @record_failure_mutex.synchronize do
154
+ @record_failure_warned ? false : (@record_failure_warned = true)
155
+ end
156
+ first ? Pgbus.logger.warn { message } : Pgbus.logger.debug { message }
157
+ end
158
+
127
159
  def load_names
128
160
  return Set.new unless table_exists?
129
161
 
data/lib/pgbus/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.13.7"
4
+ VERSION = "0.13.8"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.7
4
+ version: 0.13.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson