pgbus 0.9.8 → 0.9.9

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +329 -0
  3. data/README.md +336 -25
  4. data/lib/generators/pgbus/{add_job_locks_generator.rb → add_uniqueness_keys_generator.rb} +5 -5
  5. data/lib/pgbus/active_job/adapter.rb +1 -1
  6. data/lib/pgbus/config_loader.rb +29 -2
  7. data/lib/pgbus/configuration.rb +202 -73
  8. data/lib/pgbus/doctor.rb +28 -3
  9. data/lib/pgbus/execution_pools/async_pool.rb +2 -2
  10. data/lib/pgbus/generators/config_converter.rb +7 -5
  11. data/lib/pgbus/generators/migration_detector.rb +20 -16
  12. data/lib/pgbus/instrumentation.rb +2 -1
  13. data/lib/pgbus/integrations/appsignal/subscriber.rb +17 -2
  14. data/lib/pgbus/metrics/subscriber.rb +26 -2
  15. data/lib/pgbus/metrics.rb +3 -2
  16. data/lib/pgbus/pgmq_schema/pgmq_v1.11.1.sql +2126 -0
  17. data/lib/pgbus/pgmq_schema.rb +7 -2
  18. data/lib/pgbus/process/consumer.rb +141 -12
  19. data/lib/pgbus/process/supervisor.rb +33 -10
  20. data/lib/pgbus/process/worker.rb +6 -16
  21. data/lib/pgbus/recurring/schedule.rb +1 -2
  22. data/lib/pgbus/serializer.rb +4 -4
  23. data/lib/pgbus/streams/broadcastable_override.rb +0 -8
  24. data/lib/pgbus/streams/signed_name.rb +2 -2
  25. data/lib/pgbus/uniqueness.rb +11 -12
  26. data/lib/pgbus/version.rb +1 -1
  27. data/lib/pgbus/web/data_source.rb +20 -4
  28. data/lib/pgbus/web/payload_filter.rb +3 -3
  29. data/lib/pgbus/web/streamer/instance.rb +6 -4
  30. data/lib/pgbus/web/streamer/listener.rb +21 -36
  31. data/lib/pgbus.rb +46 -6
  32. metadata +4 -6
  33. data/app/models/pgbus/job_lock.rb +0 -98
  34. data/lib/generators/pgbus/templates/add_job_locks.rb.erb +0 -21
  35. data/lib/rubocop/cop/pgbus/no_ruby_timeout.rb +0 -42
  36. data/lib/rubocop/pgbus.rb +0 -5
@@ -11,7 +11,9 @@ module Pgbus
11
11
  #
12
12
  # Drops settings that:
13
13
  # - match the gem default (no point restating it)
14
- # - are deprecated (e.g. pool_size, which is now auto-tuned)
14
+ # - are deprecated (settings that no longer exist in the public API —
15
+ # see DEPRECATED_SETTINGS; pool_size is NOT one of these, it's a
16
+ # live accessor that overrides auto-tuning and is preserved)
15
17
  #
16
18
  # Converts seconds to durations when they evenly divide into a clean
17
19
  # unit (7 days, 30 days, 10 minutes). Falls back to the raw integer
@@ -28,7 +30,7 @@ module Pgbus
28
30
  # generator's CLI wrapper writes the new initializer and tells the
29
31
  # user to delete the YAML when ready.
30
32
  class ConfigConverter
31
- class Error < StandardError; end
33
+ class Error < Pgbus::Error; end
32
34
 
33
35
  # Setters that accept ActiveSupport::Duration (PR 5).
34
36
  DURATION_SETTINGS = %w[
@@ -38,15 +40,15 @@ module Pgbus
38
40
 
39
41
  # Settings that no longer exist in the public API. The converter
40
42
  # silently drops these from the generated initializer so users on
41
- # legacy YAML get a clean migration.
43
+ # legacy YAML get a clean migration. NOTE: pool_size is intentionally
44
+ # NOT here — it's a live Configuration#pool_size accessor users set to
45
+ # override auto-tuning, so it must survive the conversion unchanged.
42
46
  #
43
- # - pool_size -> auto-tuned from worker thread counts
44
47
  # - notify_throttle_ms -> Pgbus::Client::NOTIFY_THROTTLE_MS
45
48
  # - circuit_breaker_* -> Pgbus::CircuitBreaker constants
46
49
  # - archive_compaction_* -> Pgbus::Process::Dispatcher constants
47
50
  # - dead_letter_queue_suffix -> Pgbus::DEAD_LETTER_SUFFIX (frozen)
48
51
  DEPRECATED_SETTINGS = %w[
49
- pool_size
50
52
  notify_throttle_ms
51
53
  circuit_breaker_threshold circuit_breaker_base_backoff circuit_breaker_max_backoff
52
54
  archive_compaction_interval archive_compaction_batch_size
@@ -35,9 +35,10 @@ module Pgbus
35
35
  #
36
36
  # 5. Indexes missing on existing tables → queued. Additive, safe.
37
37
  #
38
- # 6. Modern replacements for legacy tables (e.g. pgbus_job_locks
39
- # pgbus_uniqueness_keys) queue the migration path only if the
40
- # legacy table still exists. Otherwise queue the fresh install.
38
+ # 6. Modern uniqueness table (pgbus_uniqueness_keys) missing queue
39
+ # add_uniqueness_keys unconditionally. A legacy pgbus_job_locks
40
+ # table left over from an older install is not dropped by this
41
+ # detector — run `pgbus:migrate_job_locks` by hand to retire it.
41
42
  class MigrationDetector
42
43
  # Sentinel returned when the database looks empty of pgbus tables.
43
44
  # The caller (pgbus:update generator) should redirect the user to
@@ -58,13 +59,14 @@ module Pgbus
58
59
 
59
60
  # generator_key → Rails generator name. Passed to Thor's invoke.
60
61
  #
61
- # Note: uniqueness_keys uses the migrate_job_locks generator for
62
- # both the fresh-install and upgrade-from-job_locks paths. The
63
- # template is idempotent: `unless table_exists?(:pgbus_uniqueness_keys)`
64
- # creates it, and `if table_exists?(:pgbus_job_locks)` drops the
65
- # legacy table. One generator covers both cases.
62
+ # Note: uniqueness_keys invokes add_uniqueness_keys, which only
63
+ # creates the modern pgbus_uniqueness_keys table. If the legacy
64
+ # pgbus_job_locks table is also present (an install mid-migration),
65
+ # run `pgbus:migrate_job_locks` separately to drop it — that
66
+ # generator is not part of this detector's default flow since it
67
+ # requires the legacy table to already exist.
66
68
  GENERATOR_MAP = {
67
- uniqueness_keys: "pgbus:migrate_job_locks",
69
+ uniqueness_keys: "pgbus:add_uniqueness_keys",
68
70
  add_job_stats: "pgbus:add_job_stats",
69
71
  add_job_stats_latency: "pgbus:add_job_stats_latency",
70
72
  add_job_stats_queue_index: "pgbus:add_job_stats_queue_index",
@@ -81,7 +83,7 @@ module Pgbus
81
83
  # Human-friendly description of each migration for the generator
82
84
  # output. Keeps the update generator's run log readable.
83
85
  DESCRIPTIONS = {
84
- uniqueness_keys: "uniqueness keys table (job deduplication, also upgrades legacy job_locks if present)",
86
+ uniqueness_keys: "uniqueness keys table (job deduplication)",
85
87
  add_job_stats: "job stats table (Insights dashboard)",
86
88
  add_job_stats_latency: "job stats latency columns (enqueue_latency_ms, retry_count)",
87
89
  add_job_stats_queue_index: "job stats (queue_name, created_at) index",
@@ -128,10 +130,9 @@ module Pgbus
128
130
  CORE_INSTALL_TABLES.none? { |t| table_exists?(t) }
129
131
  end
130
132
 
131
- # Legacy pgbus_job_locks modern pgbus_uniqueness_keys.
132
- # The migrate_job_locks template is idempotent: it creates
133
- # uniqueness_keys if missing and drops job_locks if present.
134
- # One symbol covers both cases; see GENERATOR_MAP.
133
+ # Queue add_uniqueness_keys whenever the modern table is missing.
134
+ # This does not touch a legacy pgbus_job_locks table — that is
135
+ # handled separately by `pgbus:migrate_job_locks`, see GENERATOR_MAP.
135
136
  def uniqueness_key_migrations
136
137
  return [] if table_exists?("pgbus_uniqueness_keys")
137
138
 
@@ -180,8 +181,11 @@ module Pgbus
180
181
  def recurring_migrations
181
182
  # pgbus_recurring_tasks + pgbus_recurring_executions are the two
182
183
  # tables the recurring generator creates. If BOTH exist, nothing
183
- # to do. If either is missing, we need the generator (which is
184
- # idempotent via if_not_exists on both tables).
184
+ # to do. If either is missing, we queue the generator. The
185
+ # add_recurring template uses plain create_table (no if_not_exists),
186
+ # so idempotency is provided by this detector, not the template:
187
+ # the migration is only queued while a table is absent, and never
188
+ # re-invoked once both tables are present.
185
189
  return [] if table_exists?("pgbus_recurring_tasks") && table_exists?("pgbus_recurring_executions")
186
190
 
187
191
  [:add_recurring]
@@ -21,7 +21,8 @@ module Pgbus
21
21
  # pgbus.stream.broadcast — stream broadcast (sync or deferred)
22
22
  # pgbus.outbox.publish — outbox row created
23
23
  # pgbus.recurring.enqueue — scheduler enqueued a due recurring task
24
- # pgbus.worker.recycle — worker hit a recycle threshold
24
+ # pgbus.worker.recycle — worker hit a recycle threshold (reason/jobs_processed/memory_mb/lifetime_seconds)
25
+ # pgbus.consumer.recycle — consumer hit a recycle threshold (same payload shape as pgbus.worker.recycle)
25
26
  # pgbus.serializer.serialize — job/event serialization
26
27
  # pgbus.serializer.deserialize — job/event deserialization
27
28
  #
@@ -41,7 +41,13 @@ module Pgbus
41
41
  subscribe("pgbus.stream.broadcast") { |event| on_stream_broadcast(event) },
42
42
  subscribe("pgbus.outbox.publish") { |event| on_outbox_publish(event) },
43
43
  subscribe("pgbus.recurring.enqueue") { |event| on_recurring_enqueue(event) },
44
- subscribe("pgbus.worker.recycle") { |event| on_worker_recycle(event) }
44
+ subscribe("pgbus.worker.recycle") { |event| on_worker_recycle(event) },
45
+ subscribe("pgbus.consumer.recycle") { |event| on_consumer_recycle(event) }
46
+ # pgbus.client.pool is deliberately NOT subscribed here — the
47
+ # minutely Probe#track_pool already reports pgbus_pool_size /
48
+ # pgbus_pool_available gauges for AppSignal (see probe.rb).
49
+ # Subscribing here too would double-report on every heartbeat
50
+ # and skew the gauges.
45
51
  ]
46
52
  @installed = true
47
53
  end
@@ -250,7 +256,16 @@ module Pgbus
250
256
  ::Appsignal.increment_counter(
251
257
  "#{METRIC_PREFIX}worker_recycled",
252
258
  1,
253
- { reason: payload[:reason] }
259
+ { reason: payload[:reason], kind: "worker" }
260
+ )
261
+ end
262
+
263
+ def on_consumer_recycle(event)
264
+ payload = event.payload
265
+ ::Appsignal.increment_counter(
266
+ "#{METRIC_PREFIX}worker_recycled",
267
+ 1,
268
+ { reason: payload[:reason], kind: "consumer" }
254
269
  )
255
270
  end
256
271
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "socket"
4
+
3
5
  module Pgbus
4
6
  module Metrics
5
7
  # Consumes Pgbus::Instrumentation events and forwards them to the configured
@@ -36,7 +38,9 @@ module Pgbus
36
38
  subscribe("pgbus.stream.broadcast") { |event| on_stream_broadcast(event) },
37
39
  subscribe("pgbus.outbox.publish") { |event| on_outbox_publish(event) },
38
40
  subscribe("pgbus.recurring.enqueue") { |event| on_recurring_enqueue(event) },
39
- subscribe("pgbus.worker.recycle") { |event| on_worker_recycle(event) }
41
+ subscribe("pgbus.worker.recycle") { |event| on_worker_recycle(event) },
42
+ subscribe("pgbus.consumer.recycle") { |event| on_consumer_recycle(event) },
43
+ subscribe("pgbus.client.pool") { |event| on_client_pool(event) }
40
44
  ]
41
45
  @installed = true
42
46
  end
@@ -176,10 +180,30 @@ module Pgbus
176
180
  def on_worker_recycle(event)
177
181
  backend.increment(
178
182
  "#{METRIC_PREFIX}worker_recycled", 1,
179
- compact(reason: event.payload[:reason])
183
+ compact(reason: event.payload[:reason], kind: "worker")
184
+ )
185
+ end
186
+
187
+ def on_consumer_recycle(event)
188
+ backend.increment(
189
+ "#{METRIC_PREFIX}worker_recycled", 1,
190
+ compact(reason: event.payload[:reason], kind: "consumer")
180
191
  )
181
192
  end
182
193
 
194
+ # ── Connection pool ───────────────────────────────────────────────
195
+
196
+ def on_client_pool(event)
197
+ payload = event.payload
198
+ tags = { hostname: hostname }
199
+ backend.gauge("#{METRIC_PREFIX}pool_size", payload[:size], tags)
200
+ backend.gauge("#{METRIC_PREFIX}pool_available", payload[:available], tags)
201
+ end
202
+
203
+ def hostname
204
+ Socket.gethostname
205
+ end
206
+
183
207
  # Drop nil-valued tags so backends don't emit empty labels.
184
208
  def compact(tags)
185
209
  tags.compact
data/lib/pgbus/metrics.rb CHANGED
@@ -3,8 +3,9 @@
3
3
  module Pgbus
4
4
  # Backend-agnostic metrics adapter.
5
5
  #
6
- # Pgbus emits ~16 `pgbus.*` ActiveSupport::Notifications events (see
7
- # Pgbus::Instrumentation). The AppSignal integration is one consumer of those
6
+ # Pgbus emits a family of `pgbus.*` ActiveSupport::Notifications events;
7
+ # `Pgbus::Instrumentation` is the single source of truth for the catalog. The
8
+ # AppSignal integration is one consumer of those
8
9
  # events; this namespace is a second, vendor-neutral consumer that forwards the
9
10
  # same event→metric mapping to a configurable backend so teams on Prometheus,
10
11
  # Datadog, or plain StatsD get metrics without hand-writing subscribers.