pgbus 0.9.7 → 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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +367 -0
  3. data/README.md +454 -25
  4. data/Rakefile +10 -1
  5. data/app/helpers/pgbus/application_helper.rb +37 -0
  6. data/app/views/pgbus/processes/_processes_table.html.erb +4 -1
  7. data/config/locales/da.yml +4 -0
  8. data/config/locales/de.yml +4 -0
  9. data/config/locales/en.yml +4 -0
  10. data/config/locales/es.yml +4 -0
  11. data/config/locales/fi.yml +4 -0
  12. data/config/locales/fr.yml +4 -0
  13. data/config/locales/it.yml +4 -0
  14. data/config/locales/ja.yml +4 -0
  15. data/config/locales/nb.yml +4 -0
  16. data/config/locales/nl.yml +4 -0
  17. data/config/locales/pt.yml +4 -0
  18. data/config/locales/sv.yml +4 -0
  19. data/lib/generators/pgbus/{add_job_locks_generator.rb → add_uniqueness_keys_generator.rb} +5 -5
  20. data/lib/pgbus/active_job/adapter.rb +1 -1
  21. data/lib/pgbus/active_job/executor.rb +25 -4
  22. data/lib/pgbus/cli/dlq.rb +164 -0
  23. data/lib/pgbus/cli.rb +18 -1
  24. data/lib/pgbus/client/connection_health.rb +194 -0
  25. data/lib/pgbus/client.rb +592 -73
  26. data/lib/pgbus/config_loader.rb +50 -4
  27. data/lib/pgbus/configuration.rb +294 -79
  28. data/lib/pgbus/dedup_cache.rb +8 -0
  29. data/lib/pgbus/doctor.rb +275 -0
  30. data/lib/pgbus/engine.rb +15 -0
  31. data/lib/pgbus/execution_pools/async_pool.rb +9 -2
  32. data/lib/pgbus/execution_pools/thread_pool.rb +7 -0
  33. data/lib/pgbus/generators/config_converter.rb +7 -5
  34. data/lib/pgbus/generators/migration_detector.rb +20 -16
  35. data/lib/pgbus/instrumentation.rb +3 -1
  36. data/lib/pgbus/integrations/appsignal/probe.rb +23 -1
  37. data/lib/pgbus/integrations/appsignal/subscriber.rb +17 -2
  38. data/lib/pgbus/metrics/backend.rb +38 -0
  39. data/lib/pgbus/metrics/backends/prometheus.rb +123 -0
  40. data/lib/pgbus/metrics/backends/statsd.rb +64 -0
  41. data/lib/pgbus/metrics/prometheus_exporter.rb +34 -0
  42. data/lib/pgbus/metrics/subscriber.rb +214 -0
  43. data/lib/pgbus/metrics.rb +43 -0
  44. data/lib/pgbus/pgmq_schema/pgmq_v1.11.1.sql +2126 -0
  45. data/lib/pgbus/pgmq_schema.rb +7 -2
  46. data/lib/pgbus/process/consumer.rb +354 -18
  47. data/lib/pgbus/process/consumer_priority.rb +34 -0
  48. data/lib/pgbus/process/dispatcher.rb +265 -41
  49. data/lib/pgbus/process/heartbeat.rb +18 -5
  50. data/lib/pgbus/process/memory_usage.rb +48 -0
  51. data/lib/pgbus/process/notify_listener.rb +26 -7
  52. data/lib/pgbus/process/notify_probe.rb +96 -0
  53. data/lib/pgbus/process/primary_validator.rb +53 -0
  54. data/lib/pgbus/process/signal_handler.rb +6 -0
  55. data/lib/pgbus/process/supervisor.rb +423 -50
  56. data/lib/pgbus/process/worker.rb +288 -35
  57. data/lib/pgbus/recurring/schedule.rb +1 -2
  58. data/lib/pgbus/recurring/scheduler.rb +15 -1
  59. data/lib/pgbus/serializer.rb +4 -4
  60. data/lib/pgbus/streams/broadcastable_override.rb +0 -8
  61. data/lib/pgbus/streams/signed_name.rb +2 -2
  62. data/lib/pgbus/streams/turbo_broadcastable.rb +7 -5
  63. data/lib/pgbus/table_maintenance.rb +13 -2
  64. data/lib/pgbus/uniqueness.rb +11 -12
  65. data/lib/pgbus/version.rb +1 -1
  66. data/lib/pgbus/web/data_source.rb +36 -4
  67. data/lib/pgbus/web/health_app.rb +102 -0
  68. data/lib/pgbus/web/health_server.rb +144 -0
  69. data/lib/pgbus/web/payload_filter.rb +3 -3
  70. data/lib/pgbus/web/streamer/instance.rb +58 -2
  71. data/lib/pgbus/web/streamer/listener.rb +69 -21
  72. data/lib/pgbus.rb +77 -0
  73. data/lib/tasks/pgbus_doctor.rake +12 -0
  74. metadata +19 -4
  75. data/app/models/pgbus/job_lock.rb +0 -98
  76. data/lib/generators/pgbus/templates/add_job_locks.rb.erb +0 -21
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgbus
4
+ class Client
5
+ # In-memory, process-local circuit breaker for database-down conditions.
6
+ #
7
+ # Distinct from Pgbus::CircuitBreaker (which is per-queue and persists its
8
+ # pause state in the database — useless when the database itself is down).
9
+ # This latch is a single shared object owned by Pgbus::Client that trips on
10
+ # raw connection failures across *any* operation, so a fleet of workers
11
+ # stops hammering a dead database and flooding the error tracker for the
12
+ # whole outage (issue #197).
13
+ #
14
+ # State machine:
15
+ # closed -> open after OPEN_THRESHOLD consecutive ConnectionErrors
16
+ # open -> half_open once the backoff window elapses (admits ONE probe)
17
+ # half_open -> closed on a successful probe (resets backoff)
18
+ # half_open -> open on a failed probe (backoff doubles, capped)
19
+ #
20
+ # Thresholds are constants, not configuration, mirroring CircuitBreaker:
21
+ # the values rarely need tuning and exposing them never proved useful.
22
+ #
23
+ # Thread safety: a single Mutex serializes every state transition. The
24
+ # guarded operation itself runs *outside* the lock (only the gate decision
25
+ # and the outcome recording are inside), so a slow probe never blocks other
26
+ # worker threads from failing fast.
27
+ class ConnectionHealth
28
+ # Consecutive ConnectionErrors that trip the breaker from closed to open.
29
+ OPEN_THRESHOLD = 5
30
+
31
+ # Initial backoff (seconds) on the first trip. Doubles on each re-open.
32
+ BASE_BACKOFF = 1.0
33
+
34
+ # Cap on the exponential backoff (seconds). After ~6 re-opens the curve
35
+ # plateaus here so a perpetually-down database stops probing more than
36
+ # once a minute.
37
+ MAX_BACKOFF = 60.0
38
+
39
+ def initialize(clock: -> { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) },
40
+ on_open: nil, on_close: nil)
41
+ @clock = clock
42
+ @on_open = on_open
43
+ @on_close = on_close
44
+ @mutex = Mutex.new
45
+ @state = :closed
46
+ @failure_count = 0
47
+ @trip_count = 0
48
+ @open_until = nil
49
+ end
50
+
51
+ # Gate an operation through the breaker.
52
+ #
53
+ # - closed / half-open-probe-admitted: yields, records the outcome.
54
+ # - open (window not elapsed) or half-open (probe already in flight):
55
+ # raises Pgbus::ConnectionCircuitOpenError without yielding — no pool
56
+ # checkout, no ConnectionError, no ErrorReporter noise.
57
+ #
58
+ # The breaker never swallows an error — every exception propagates. A
59
+ # ConnectionError in the closed state counts toward the trip threshold; a
60
+ # failure of the single half-open probe (on ANY error) re-opens the
61
+ # breaker; any other closed-state error is not counted.
62
+ def run_guarded
63
+ admitted = admit_or_raise # :run (was closed) or :probe (was open)
64
+
65
+ begin
66
+ result = yield
67
+ rescue StandardError => e
68
+ record_failure(admitted, e)
69
+ raise
70
+ end
71
+
72
+ record_success(admitted)
73
+ result
74
+ end
75
+
76
+ def closed?
77
+ @mutex.synchronize { @state == :closed }
78
+ end
79
+
80
+ def open?
81
+ @mutex.synchronize { @state == :open }
82
+ end
83
+
84
+ private
85
+
86
+ # Decide whether this call may proceed. Runs entirely inside the mutex.
87
+ # Returns the admission kind so the outcome recorders can attribute the
88
+ # result to THIS call (the probe vs. an ordinary closed-state read) —
89
+ # critical because the block runs outside the lock, so between admit and
90
+ # record the state may have changed under another thread.
91
+ #
92
+ # When admitting a probe from the open state it flips @state to :half_open
93
+ # so a second racing thread sees :half_open and is rejected — that is the
94
+ # single-probe guarantee.
95
+ def admit_or_raise
96
+ @mutex.synchronize do
97
+ case @state
98
+ when :closed
99
+ :run
100
+ when :open
101
+ raise_open unless window_elapsed?
102
+
103
+ @state = :half_open # admit exactly this thread as the probe
104
+ :probe
105
+ when :half_open
106
+ raise_open # a probe is already in flight
107
+ end
108
+ end
109
+ end
110
+
111
+ def record_success(admitted)
112
+ callback = nil
113
+ @mutex.synchronize do
114
+ if admitted == :probe && @state == :half_open
115
+ # The single probe succeeded — the database is reachable again.
116
+ close!
117
+ callback = @on_close
118
+ elsif @state == :closed
119
+ # An ordinary read completed while closed: clear the failure streak.
120
+ # Do NOT force-close here — a straggler read admitted while closed
121
+ # can return AFTER other threads have tripped the breaker; closing
122
+ # would wipe a freshly-opened breaker (issue #197 straggler race).
123
+ @failure_count = 0
124
+ end
125
+ end
126
+ callback&.call
127
+ end
128
+
129
+ # Any exception ends the operation. A probe failure — whether a
130
+ # ConnectionError or something else (e.g. Pgbus::ReadTimeoutError from a
131
+ # hung socket, which the read paths raise from INSIDE the guard) — must
132
+ # re-open the breaker. If it didn't, a non-ConnectionError probe would
133
+ # leave the latch wedged in :half_open forever, admitting no further
134
+ # probes and never recovering (issue #197 wedge). Ordinary closed-state
135
+ # failures only count toward the threshold when they are ConnectionErrors.
136
+ def record_failure(admitted, error)
137
+ callback_backoff = nil
138
+ @mutex.synchronize do
139
+ if admitted == :probe && @state == :half_open
140
+ callback_backoff = reopen
141
+ elsif @state == :closed && connection_error?(error)
142
+ @failure_count += 1
143
+ callback_backoff = reopen if @failure_count >= OPEN_THRESHOLD
144
+ end
145
+ # Otherwise (a straggler failing after another thread already changed
146
+ # the state) leave the current window untouched.
147
+ end
148
+ @on_open&.call(callback_backoff) if callback_backoff
149
+ end
150
+
151
+ # Transition to closed and fully reset. Must be called with @mutex held.
152
+ def close!
153
+ @state = :closed
154
+ @failure_count = 0
155
+ @trip_count = 0
156
+ @open_until = nil
157
+ end
158
+
159
+ # Transition to open, arm the backoff window, and return the window size
160
+ # so the caller can fire the on_open callback outside the lock. Must be
161
+ # called with @mutex held.
162
+ def reopen
163
+ @trip_count += 1
164
+ backoff = calculate_backoff(@trip_count)
165
+ @state = :open
166
+ @failure_count = 0
167
+ @open_until = clock_now + backoff
168
+ backoff
169
+ end
170
+
171
+ def window_elapsed?
172
+ @open_until.nil? || clock_now >= @open_until
173
+ end
174
+
175
+ def calculate_backoff(trip_count)
176
+ backoff = BASE_BACKOFF * (2**(trip_count - 1))
177
+ [backoff, MAX_BACKOFF].min
178
+ end
179
+
180
+ def raise_open
181
+ raise Pgbus::ConnectionCircuitOpenError,
182
+ "database connection circuit is open (#{OPEN_THRESHOLD}+ consecutive connection failures)"
183
+ end
184
+
185
+ def connection_error?(error)
186
+ defined?(PGMQ::Errors::ConnectionError) && error.is_a?(PGMQ::Errors::ConnectionError)
187
+ end
188
+
189
+ def clock_now
190
+ @clock.call
191
+ end
192
+ end
193
+ end
194
+ end