pgbus 0.13.9 → 0.14.0
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 +4 -4
- data/CHANGELOG.md +2 -0
- data/lib/pgbus/bus_record.rb +15 -0
- data/lib/pgbus/database_tasks_guard.rb +34 -0
- data/lib/pgbus/engine.rb +6 -0
- data/lib/pgbus/event_bus/registry.rb +3 -15
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus.rb +31 -0
- data/lib/tasks/pgbus_autovacuum.rake +31 -15
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66fe070813c414410d4b55f2b9c9f1dfcacd70236f97d22f8e037ec7a2d906c4
|
|
4
|
+
data.tar.gz: 285e8851e43a02384734c0dfda2ae240ea4450554b5c8dca478e351b04a57528
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 160efab6bf57a74ab7d190b127b7e0f64426fc17877407a87c0f70a3ba328bea8e0dfa6c44e2477263e746b16a23544843a044e1eb88c285147a6e31e129f404
|
|
7
|
+
data.tar.gz: 7dc944582757565e3d0c8339661e533745f750731da645f2aaf6693673083b31b470f1b92265e880278b7b5720a593cc266eb9d3a11aaf554a527780f7cd481b
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
### Fixed
|
|
4
4
|
|
|
5
|
+
- **Boot-time BusRecord connections no longer wedge `db:test:purge` — pgbus disconnects its own pools before every database purge/drop (issue #409).** With pgbus on a dedicated database (`config.connects_to`), any boot-time touch of a pgbus model left an idle session on that database for the life of the process. Rails' purge/drop only disconnects the connection it establishes for the target db_config — it knows nothing about gem-owned pools — so the rake process's own idle session blocked its own `DROP DATABASE`: a hang without a `statement_timeout`, a fast `ActiveRecord::QueryCanceled` with one, and either way `db:test:prepare` / `maintain_test_schema!` permanently broken on that machine (every retry re-boots, re-opens the connection, and re-blocks; terminating sessions externally can't help). Three changes: **(1)** `Pgbus::DatabaseTasksGuard` is prepended onto `ActiveRecord::Tasks::DatabaseTasks` at boot, so every route to a purge/drop — `db:test:purge` / `db:purge` / `db:drop` and their per-database variants, `maintain_test_schema!`'s in-process purge, parallel-testing's `TestDatabases` — first runs `Pgbus::BusRecord.disconnect_all_pools!` (all roles; a no-op on primary-database installs, `ActiveRecord::Base`'s pool is never touched). **(2)** The task-aware guard the event-bus registry already used internally is now public as **`Pgbus.database_task?`** — true while the process runs a `db:*` / `assets:*`-family rake task — so apps can skip their own boot-time warm-ups in the contexts where a database may legitimately not exist. **(3)** `pgbus:tune_autovacuum` (enhanced onto `db:schema:load`) no longer holds a permanent connection lease from `BusRecord.connection`: it checks out via `connection_pool.with_connection` and disconnects the pool afterward, leaving no idle session behind inside longer rake chains. Refs #409.
|
|
6
|
+
|
|
5
7
|
- **Concurrent queue creation no longer kills the loser with `PG::UniqueViolation` on `pg_class` (issue #404).** The sibling of the #403 trigger race, one DDL step earlier: `CREATE TABLE IF NOT EXISTS` is not race-safe — two backends creating a not-yet-existing queue both pass the existence check (READ COMMITTED; neither sees the other's uncommitted catalog rows), both insert into `pg_class`, and the loser raises `unique_violation` on `pg_class_relname_nsp_index` instead of the friendly `duplicate_table`. All the dedup guards (`@queues_created`, `Stream#@ensured`, `synchronized`) are process-local, so with lazily-created per-record durable stream queues two processes first-touching the same brand-new stream hit `pgmq.create` simultaneously — and via `send_message`'s `ensure_queue` the same window applies to plain job queues on cold post-deploy herds (production: AppSignal incident #963, an unrescued `PG::UniqueViolation` out of an unrelated `after_commit`). Three DDL steps now rescue the duplicate as success, mirroring the #397/#403 shape: **(1)** `create_queue_table` — `pgmq.create` is a single statement and therefore atomic, so when the loser unblocks the winner has committed the whole queue; the loser re-checks `pgmq.meta` and returns, retrying `pgmq.create` once when the re-check can't confirm (e.g. a leftover physical table without a meta row) with a second failure propagating (the retry's own error — the original duplicate is preserved deeper in its cause chain via Ruby's implicit `$!` chaining); **(2)** `create_fifo_index_if_needed` under group_mode — the duplicate proves the index exists; **(3)** the stream archive `msg_id` index in `ensure_stream_queue` — a raw `CREATE INDEX IF NOT EXISTS` with the same catalog race, where the loser now proceeds to register and memoize the stream instead of dropping the first broadcast. Duplicate detection reuses `DUPLICATE_INSTALL_ERROR_CLASSES`, matched directly (raw `conn.exec` paths) or as the `cause` of pgmq-ruby's `ConnectionError` wrapper. No locking added, no happy-path cost. Refs #404.
|
|
6
8
|
|
|
7
9
|
- **Concurrent lazy stream-queue ensures no longer drop the loser's broadcast with `PG::DuplicateObject` on the NOTIFY trigger (issue #403).** `Client#enable_notify_if_needed` is check-then-act — `notify_trigger_current?` is a plain SELECT and the surrounding `synchronized` is a process-local mutex — so after a deploy, when every process's `@queues_created` memo is cold, two processes handling the first broadcast for the same stream both see "trigger not current" and both run PGMQ's `DROP TRIGGER IF EXISTS` + `CREATE CONSTRAINT TRIGGER` cycle. The loser's CREATE blocks on the winner's table lock and fails with `duplicate_object` once the winner commits — surfaced as `PGMQ::Errors::ConnectionError`, killing that broadcast (in production: a deploy-time thundering herd turning unrelated `after_commit` writes into 500s, AppSignal incident #546). The existing mitigations all miss this window: the 0.12.x `notify_trigger_current?` check made repeat ensures cheap but left the TOCTOU race, the #397 advisory lock serializes schema install only, and the supervisor's pre-fork bootstrap covers job-container children but not web processes. The loser now treats the duplicate as success — the trigger provably exists because a concurrent caller just created it (same shape as the #397 duplicate-object rescue on schema install; matched on the trigger identifier, which survives message localization, plus the `PG::DuplicateObject` cause or the "already exists" text). Before returning, it re-checks `notify_trigger_current?`: racing ensures pass the same throttle so the check normally confirms and returns, but a job-queue ensure (250ms) racing the stream override (0ms) can leave the wrong interval installed — that mismatch retries `enable_notify_insert` once to converge; a second loss propagates. No locking added, no happy-path cost. Refs #403.
|
data/lib/pgbus/bus_record.rb
CHANGED
|
@@ -12,5 +12,20 @@ module Pgbus
|
|
|
12
12
|
# and the engine's app/models path isn't registered yet.
|
|
13
13
|
class BusRecord < ActiveRecord::Base
|
|
14
14
|
self.abstract_class = true
|
|
15
|
+
|
|
16
|
+
# Disconnects every pool owned by this class — the pools `connects_to`
|
|
17
|
+
# creates when pgbus runs on a dedicated database — across all roles.
|
|
18
|
+
# When pgbus runs in the primary database there is no BusRecord-owned
|
|
19
|
+
# pool and this is a no-op; ActiveRecord::Base's pool is never touched.
|
|
20
|
+
#
|
|
21
|
+
# Called by Pgbus::DatabaseTasksGuard before every database purge/drop so
|
|
22
|
+
# an idle boot-time session on the pgbus database can never block that
|
|
23
|
+
# same process's DROP DATABASE (issue #409). Safe to call at any time:
|
|
24
|
+
# the next checkout after a disconnect reconnects transparently.
|
|
25
|
+
def self.disconnect_all_pools!
|
|
26
|
+
connection_handler.connection_pool_list(:all).each do |pool|
|
|
27
|
+
pool.disconnect! if pool.connection_class == self
|
|
28
|
+
end
|
|
29
|
+
end
|
|
15
30
|
end
|
|
16
31
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# Prepended onto ActiveRecord::Tasks::DatabaseTasks' singleton class by the
|
|
5
|
+
# engine (initializer "pgbus.db") so every database purge/drop first
|
|
6
|
+
# disconnects the gem's own BusRecord pools.
|
|
7
|
+
#
|
|
8
|
+
# Why: with pgbus on a dedicated database (config.connects_to), any
|
|
9
|
+
# boot-time touch of a Pgbus model leaves an idle session on that database
|
|
10
|
+
# for the life of the process. Rails' purge/drop only disconnects the
|
|
11
|
+
# connection it establishes for the target db_config — it knows nothing
|
|
12
|
+
# about gem-owned pools — so the process's own idle session blocks its own
|
|
13
|
+
# DROP DATABASE (or kills it via statement_timeout), permanently wedging
|
|
14
|
+
# db:test:prepare (issue #409).
|
|
15
|
+
#
|
|
16
|
+
# Intercepting DatabaseTasks (rather than enhancing the rake tasks) covers
|
|
17
|
+
# every route to a purge/drop: db:test:purge / db:purge / db:drop and their
|
|
18
|
+
# per-database variants, maintain_test_schema!'s in-process purge, and
|
|
19
|
+
# parallel-testing's TestDatabases — they all funnel through these methods.
|
|
20
|
+
#
|
|
21
|
+
# Installed only in processes that booted the app; a bare `db:drop` process
|
|
22
|
+
# that never ran initializers has no BusRecord pool to block on anyway.
|
|
23
|
+
module DatabaseTasksGuard
|
|
24
|
+
def purge(...)
|
|
25
|
+
Pgbus::BusRecord.disconnect_all_pools!
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def drop(...)
|
|
30
|
+
Pgbus::BusRecord.disconnect_all_pools!
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/pgbus/engine.rb
CHANGED
|
@@ -61,6 +61,12 @@ module Pgbus
|
|
|
61
61
|
initializer "pgbus.db" do
|
|
62
62
|
ActiveSupport.on_load(:active_record) do
|
|
63
63
|
Pgbus::BusRecord.connects_to(**Pgbus.configuration.connects_to) if Pgbus.configuration.connects_to
|
|
64
|
+
|
|
65
|
+
# Disconnect the gem's own pools before any database purge/drop, so an
|
|
66
|
+
# idle boot-time BusRecord session on a dedicated pgbus database can
|
|
67
|
+
# never block that same process's DROP DATABASE (issue #409). No-op
|
|
68
|
+
# when pgbus runs in the primary database (no BusRecord-owned pool).
|
|
69
|
+
ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(Pgbus::DatabaseTasksGuard)
|
|
64
70
|
end
|
|
65
71
|
end
|
|
66
72
|
|
|
@@ -104,23 +104,11 @@ module Pgbus
|
|
|
104
104
|
|
|
105
105
|
private
|
|
106
106
|
|
|
107
|
-
# Rake task-name prefixes during which pgbus must NOT open a PGMQ
|
|
108
|
-
# connection: creating/dropping/migrating/loading the schema (a live
|
|
109
|
-
# connection blocks DROP DATABASE) and asset precompile (no DB expected).
|
|
110
|
-
SCHEMA_TASK_PREFIXES = %w[db: db_test: assets: webpacker: yarn:].freeze
|
|
111
|
-
private_constant :SCHEMA_TASK_PREFIXES
|
|
112
|
-
|
|
113
107
|
# True when running inside a rake schema/asset task, where setup_all!(safe:)
|
|
114
|
-
# should skip rather than open a connection.
|
|
115
|
-
#
|
|
108
|
+
# should skip rather than open a connection. Delegates to the shared,
|
|
109
|
+
# public detector (issue #409) so there is a single implementation.
|
|
116
110
|
def schema_task_context?
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
tasks = ::Rake.application.top_level_tasks
|
|
120
|
-
tasks.any? { |t| SCHEMA_TASK_PREFIXES.any? { |prefix| t.to_s.start_with?(prefix) } }
|
|
121
|
-
rescue StandardError => e
|
|
122
|
-
Pgbus.logger.debug { "[Pgbus] schema_task_context? detection failed, assuming non-schema: #{e.class}: #{e.message}" }
|
|
123
|
-
false
|
|
111
|
+
Pgbus.database_task?
|
|
124
112
|
end
|
|
125
113
|
|
|
126
114
|
def matches?(pattern, routing_key)
|
data/lib/pgbus/version.rb
CHANGED
data/lib/pgbus.rb
CHANGED
|
@@ -10,6 +10,13 @@ module Pgbus
|
|
|
10
10
|
# this to be configurable.
|
|
11
11
|
DEAD_LETTER_SUFFIX = "_dlq"
|
|
12
12
|
|
|
13
|
+
# Rake task-name prefixes during which pgbus must NOT open a database
|
|
14
|
+
# connection: schema management (an idle session on a dedicated pgbus
|
|
15
|
+
# database blocks that same process's DROP DATABASE during db:test:purge —
|
|
16
|
+
# issue #409) and asset precompile (where a database may legitimately not
|
|
17
|
+
# exist). Consulted by Pgbus.database_task?.
|
|
18
|
+
DATABASE_TASK_PREFIXES = %w[db: db_test: assets: webpacker: yarn:].freeze
|
|
19
|
+
|
|
13
20
|
# Error-hierarchy policy (the 1.0 contract, issue #282):
|
|
14
21
|
#
|
|
15
22
|
# * OPERATIONAL errors — a pgbus subsystem failed to do its job at runtime
|
|
@@ -149,6 +156,30 @@ module Pgbus
|
|
|
149
156
|
@configuration ||= Configuration.new
|
|
150
157
|
end
|
|
151
158
|
|
|
159
|
+
# True when the current process is running a rake task (db:*, assets:*, …
|
|
160
|
+
# — see DATABASE_TASK_PREFIXES) during which pgbus must not touch the
|
|
161
|
+
# database: the database may not exist yet, and an idle connection on a
|
|
162
|
+
# dedicated pgbus database blocks that same process's DROP DATABASE
|
|
163
|
+
# during db:test:purge (issue #409). Use it to guard boot-time database
|
|
164
|
+
# touches in an initializer:
|
|
165
|
+
#
|
|
166
|
+
# Rails.application.config.after_initialize do
|
|
167
|
+
# Pgbus::StreamQueue.table_exists? unless Pgbus.database_task?
|
|
168
|
+
# end
|
|
169
|
+
#
|
|
170
|
+
# False outside a Rake run, and false (never raise into boot) when
|
|
171
|
+
# detection itself fails.
|
|
172
|
+
def database_task?
|
|
173
|
+
return false unless defined?(::Rake) && ::Rake.respond_to?(:application)
|
|
174
|
+
|
|
175
|
+
::Rake.application.top_level_tasks.any? do |task|
|
|
176
|
+
DATABASE_TASK_PREFIXES.any? { |prefix| task.to_s.start_with?(prefix) }
|
|
177
|
+
end
|
|
178
|
+
rescue StandardError => e
|
|
179
|
+
logger.debug { "[Pgbus] database_task? detection failed, assuming non-schema: #{e.class}: #{e.message}" }
|
|
180
|
+
false
|
|
181
|
+
end
|
|
182
|
+
|
|
152
183
|
def configure
|
|
153
184
|
yield configuration
|
|
154
185
|
# Fail loud at boot on an invalid value rather than leaving it dormant
|
|
@@ -5,26 +5,42 @@ namespace :pgbus do
|
|
|
5
5
|
task tune_autovacuum: :environment do
|
|
6
6
|
require "pgbus/autovacuum_tuning"
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
apply_tuning = lambda do |conn|
|
|
9
|
+
# Only run if pgmq schema exists (tables may not be created yet during
|
|
10
|
+
# initial setup — the install migration handles tuning itself).
|
|
11
|
+
pgmq_exists = conn.select_value(
|
|
12
|
+
"SELECT 1 FROM information_schema.schemata WHERE schema_name = 'pgmq'"
|
|
13
|
+
)
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
)
|
|
15
|
+
unless pgmq_exists
|
|
16
|
+
puts "[pgbus] PGMQ schema not found — skipping autovacuum tuning."
|
|
17
|
+
next
|
|
18
|
+
end
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
next
|
|
19
|
-
end
|
|
20
|
+
puts "[pgbus] Applying autovacuum tuning to PGMQ queue/archive tables..."
|
|
21
|
+
conn.execute(Pgbus::AutovacuumTuning.sql_for_all_queues)
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
puts "[pgbus] Applying autovacuum tuning to high-churn pgbus tables..."
|
|
24
|
+
conn.execute(Pgbus::AutovacuumTuning.sql_for_high_churn_tables)
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
puts "[pgbus] Autovacuum tuning complete."
|
|
27
|
+
end
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
if Pgbus.configuration.connects_to
|
|
30
|
+
# Scoped checkout + disconnect: this task is enhanced onto
|
|
31
|
+
# db:schema:load, so a permanent `.connection` lease on the dedicated
|
|
32
|
+
# pgbus database would leave an idle session that blocks a later
|
|
33
|
+
# DROP DATABASE in the same rake process (issue #409).
|
|
34
|
+
begin
|
|
35
|
+
Pgbus::BusRecord.connection_pool.with_connection { |conn| apply_tuning.call(conn) }
|
|
36
|
+
ensure
|
|
37
|
+
# Disconnect even when tuning raises — the session must not outlive
|
|
38
|
+
# the task either way.
|
|
39
|
+
Pgbus::BusRecord.connection_pool.disconnect!
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
apply_tuning.call(ActiveRecord::Base.connection)
|
|
43
|
+
end
|
|
28
44
|
end
|
|
29
45
|
end
|
|
30
46
|
|
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.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -270,6 +270,7 @@ files:
|
|
|
270
270
|
- lib/pgbus/concurrency/semaphore.rb
|
|
271
271
|
- lib/pgbus/configuration.rb
|
|
272
272
|
- lib/pgbus/configuration/capsule_dsl.rb
|
|
273
|
+
- lib/pgbus/database_tasks_guard.rb
|
|
273
274
|
- lib/pgbus/dedicated_connection.rb
|
|
274
275
|
- lib/pgbus/dedup_cache.rb
|
|
275
276
|
- lib/pgbus/doctor.rb
|