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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +367 -0
- data/README.md +454 -25
- data/Rakefile +10 -1
- data/app/helpers/pgbus/application_helper.rb +37 -0
- data/app/views/pgbus/processes/_processes_table.html.erb +4 -1
- data/config/locales/da.yml +4 -0
- data/config/locales/de.yml +4 -0
- data/config/locales/en.yml +4 -0
- data/config/locales/es.yml +4 -0
- data/config/locales/fi.yml +4 -0
- data/config/locales/fr.yml +4 -0
- data/config/locales/it.yml +4 -0
- data/config/locales/ja.yml +4 -0
- data/config/locales/nb.yml +4 -0
- data/config/locales/nl.yml +4 -0
- data/config/locales/pt.yml +4 -0
- data/config/locales/sv.yml +4 -0
- data/lib/generators/pgbus/{add_job_locks_generator.rb → add_uniqueness_keys_generator.rb} +5 -5
- data/lib/pgbus/active_job/adapter.rb +1 -1
- data/lib/pgbus/active_job/executor.rb +25 -4
- data/lib/pgbus/cli/dlq.rb +164 -0
- data/lib/pgbus/cli.rb +18 -1
- data/lib/pgbus/client/connection_health.rb +194 -0
- data/lib/pgbus/client.rb +592 -73
- data/lib/pgbus/config_loader.rb +50 -4
- data/lib/pgbus/configuration.rb +294 -79
- data/lib/pgbus/dedup_cache.rb +8 -0
- data/lib/pgbus/doctor.rb +275 -0
- data/lib/pgbus/engine.rb +15 -0
- data/lib/pgbus/execution_pools/async_pool.rb +9 -2
- data/lib/pgbus/execution_pools/thread_pool.rb +7 -0
- data/lib/pgbus/generators/config_converter.rb +7 -5
- data/lib/pgbus/generators/migration_detector.rb +20 -16
- data/lib/pgbus/instrumentation.rb +3 -1
- data/lib/pgbus/integrations/appsignal/probe.rb +23 -1
- data/lib/pgbus/integrations/appsignal/subscriber.rb +17 -2
- data/lib/pgbus/metrics/backend.rb +38 -0
- data/lib/pgbus/metrics/backends/prometheus.rb +123 -0
- data/lib/pgbus/metrics/backends/statsd.rb +64 -0
- data/lib/pgbus/metrics/prometheus_exporter.rb +34 -0
- data/lib/pgbus/metrics/subscriber.rb +214 -0
- data/lib/pgbus/metrics.rb +43 -0
- data/lib/pgbus/pgmq_schema/pgmq_v1.11.1.sql +2126 -0
- data/lib/pgbus/pgmq_schema.rb +7 -2
- data/lib/pgbus/process/consumer.rb +354 -18
- data/lib/pgbus/process/consumer_priority.rb +34 -0
- data/lib/pgbus/process/dispatcher.rb +265 -41
- data/lib/pgbus/process/heartbeat.rb +18 -5
- data/lib/pgbus/process/memory_usage.rb +48 -0
- data/lib/pgbus/process/notify_listener.rb +26 -7
- data/lib/pgbus/process/notify_probe.rb +96 -0
- data/lib/pgbus/process/primary_validator.rb +53 -0
- data/lib/pgbus/process/signal_handler.rb +6 -0
- data/lib/pgbus/process/supervisor.rb +423 -50
- data/lib/pgbus/process/worker.rb +288 -35
- data/lib/pgbus/recurring/schedule.rb +1 -2
- data/lib/pgbus/recurring/scheduler.rb +15 -1
- data/lib/pgbus/serializer.rb +4 -4
- data/lib/pgbus/streams/broadcastable_override.rb +0 -8
- data/lib/pgbus/streams/signed_name.rb +2 -2
- data/lib/pgbus/streams/turbo_broadcastable.rb +7 -5
- data/lib/pgbus/table_maintenance.rb +13 -2
- data/lib/pgbus/uniqueness.rb +11 -12
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +36 -4
- data/lib/pgbus/web/health_app.rb +102 -0
- data/lib/pgbus/web/health_server.rb +144 -0
- data/lib/pgbus/web/payload_filter.rb +3 -3
- data/lib/pgbus/web/streamer/instance.rb +58 -2
- data/lib/pgbus/web/streamer/listener.rb +69 -21
- data/lib/pgbus.rb +77 -0
- data/lib/tasks/pgbus_doctor.rake +12 -0
- metadata +19 -4
- data/app/models/pgbus/job_lock.rb +0 -98
- 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
|