pgbus 0.9.7 → 0.9.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 +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +119 -1
- 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/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 +23 -4
- data/lib/pgbus/configuration.rb +98 -12
- data/lib/pgbus/dedup_cache.rb +8 -0
- data/lib/pgbus/doctor.rb +250 -0
- data/lib/pgbus/engine.rb +15 -0
- data/lib/pgbus/execution_pools/async_pool.rb +7 -0
- data/lib/pgbus/execution_pools/thread_pool.rb +7 -0
- data/lib/pgbus/instrumentation.rb +1 -0
- data/lib/pgbus/integrations/appsignal/probe.rb +23 -1
- 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 +190 -0
- data/lib/pgbus/metrics.rb +42 -0
- data/lib/pgbus/process/consumer.rb +215 -8
- 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 +396 -46
- data/lib/pgbus/process/worker.rb +298 -35
- data/lib/pgbus/recurring/scheduler.rb +15 -1
- data/lib/pgbus/streams/turbo_broadcastable.rb +7 -5
- data/lib/pgbus/table_maintenance.rb +13 -2
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +20 -4
- data/lib/pgbus/web/health_app.rb +102 -0
- data/lib/pgbus/web/health_server.rb +144 -0
- data/lib/pgbus/web/streamer/instance.rb +55 -1
- data/lib/pgbus/web/streamer/listener.rb +72 -9
- data/lib/pgbus.rb +37 -0
- data/lib/rubocop/cop/pgbus/no_ruby_timeout.rb +42 -0
- data/lib/rubocop/pgbus.rb +5 -0
- data/lib/tasks/pgbus_doctor.rake +12 -0
- metadata +18 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
module CLI
|
|
7
|
+
# Dead-letter queue management for headless deployments and incident
|
|
8
|
+
# runbooks. Every operation routes through +Web::DataSource+ so the CLI
|
|
9
|
+
# shares the dashboard's exact semantics — origin-queue re-enqueue,
|
|
10
|
+
# transactional retry, lock release on discard, sensitive-payload
|
|
11
|
+
# filtering — with zero raw SQL or direct PGMQ calls.
|
|
12
|
+
module DLQ
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
ROW_FORMAT = "%-10s %-32s %-28s %-8s %s"
|
|
16
|
+
|
|
17
|
+
def start(args, data_source: Pgbus::Web::DataSource.new)
|
|
18
|
+
subcommand = args.first
|
|
19
|
+
rest = args[1..] || []
|
|
20
|
+
|
|
21
|
+
case subcommand
|
|
22
|
+
when "list" then list(rest, data_source)
|
|
23
|
+
when "show" then show(rest, data_source)
|
|
24
|
+
when "retry" then retry_one(rest, data_source)
|
|
25
|
+
when "retry-all" then retry_all(data_source)
|
|
26
|
+
when "purge" then purge(rest, data_source)
|
|
27
|
+
else
|
|
28
|
+
warn "Unknown dlq subcommand: #{subcommand.inspect}" if subcommand
|
|
29
|
+
print_help
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def list(args, data_source)
|
|
35
|
+
options = parse_list_options(args)
|
|
36
|
+
messages = data_source.dlq_messages(page: options[:page], per_page: options[:per_page])
|
|
37
|
+
|
|
38
|
+
if messages.empty?
|
|
39
|
+
puts "No dead-letter messages."
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
print_list_table(messages)
|
|
44
|
+
puts "-" * 96
|
|
45
|
+
puts "Total dead-letter messages: #{data_source.dlq_total_count} " \
|
|
46
|
+
"(page #{options[:page]}, #{options[:per_page]} per page)"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def show(args, data_source)
|
|
50
|
+
msg_id = require_msg_id(args)
|
|
51
|
+
detail = data_source.dlq_message_detail(msg_id)
|
|
52
|
+
abort_missing(msg_id) unless detail
|
|
53
|
+
|
|
54
|
+
puts "msg_id: #{detail[:msg_id]}"
|
|
55
|
+
puts "dlq queue: #{detail[:queue_name]}"
|
|
56
|
+
puts "origin: #{origin_queue(detail[:queue_name])}"
|
|
57
|
+
puts "read_ct: #{detail[:read_ct]}"
|
|
58
|
+
puts "enqueued_at: #{detail[:enqueued_at]}"
|
|
59
|
+
puts "payload:"
|
|
60
|
+
puts Pgbus::Web::PayloadFilter.filter_json(detail[:message])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def retry_one(args, data_source)
|
|
64
|
+
msg_id = require_msg_id(args)
|
|
65
|
+
detail = data_source.dlq_message_detail(msg_id)
|
|
66
|
+
abort_missing(msg_id) unless detail
|
|
67
|
+
|
|
68
|
+
if data_source.retry_dlq_message(detail[:queue_name], detail[:msg_id])
|
|
69
|
+
puts "Re-enqueued msg_id #{detail[:msg_id]} to #{origin_queue(detail[:queue_name])}."
|
|
70
|
+
else
|
|
71
|
+
warn "Failed to retry msg_id #{detail[:msg_id]}."
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def retry_all(data_source)
|
|
77
|
+
count = data_source.retry_all_dlq
|
|
78
|
+
puts "Re-enqueued #{count} dead-letter message#{"s" unless count == 1}."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def purge(args, data_source)
|
|
82
|
+
if args.include?("--all")
|
|
83
|
+
purge_all(args, data_source)
|
|
84
|
+
else
|
|
85
|
+
purge_one(args, data_source)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def purge_one(args, data_source)
|
|
90
|
+
msg_id = require_msg_id(args)
|
|
91
|
+
detail = data_source.dlq_message_detail(msg_id)
|
|
92
|
+
abort_missing(msg_id) unless detail
|
|
93
|
+
|
|
94
|
+
if data_source.discard_dlq_message(detail[:queue_name], detail[:msg_id])
|
|
95
|
+
puts "Purged msg_id #{detail[:msg_id]} from #{detail[:queue_name]}."
|
|
96
|
+
else
|
|
97
|
+
warn "Failed to purge msg_id #{detail[:msg_id]}."
|
|
98
|
+
exit 1
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def purge_all(args, data_source)
|
|
103
|
+
unless args.include?("--yes")
|
|
104
|
+
warn "Refusing to purge all dead-letter messages without --yes."
|
|
105
|
+
exit 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
count = data_source.discard_all_dlq
|
|
109
|
+
puts "Purged #{count} dead-letter message#{"s" unless count == 1}."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_list_options(args)
|
|
113
|
+
options = { page: 1, per_page: 25 }
|
|
114
|
+
OptionParser.new do |opts|
|
|
115
|
+
opts.banner = "Usage: pgbus dlq list [options]"
|
|
116
|
+
opts.on("--page N", Integer, "Page number (default: 1)") { |v| options[:page] = v }
|
|
117
|
+
opts.on("--per-page N", Integer, "Messages per page (default: 25)") { |v| options[:per_page] = v }
|
|
118
|
+
end.parse!(args.dup)
|
|
119
|
+
options
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def print_list_table(messages)
|
|
123
|
+
puts format(ROW_FORMAT, "MSG_ID", "DLQ QUEUE", "ORIGIN QUEUE", "READ_CT", "ENQUEUED_AT")
|
|
124
|
+
puts "-" * 96
|
|
125
|
+
messages.each do |m|
|
|
126
|
+
puts format(ROW_FORMAT, m[:msg_id], m[:queue_name], origin_queue(m[:queue_name]),
|
|
127
|
+
m[:read_ct], m[:enqueued_at])
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def origin_queue(dlq_queue_name)
|
|
132
|
+
dlq_queue_name.delete_suffix(Pgbus::DEAD_LETTER_SUFFIX)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def require_msg_id(args)
|
|
136
|
+
msg_id = args.find { |a| a !~ /\A--/ }
|
|
137
|
+
unless msg_id
|
|
138
|
+
warn "A msg_id is required."
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
141
|
+
msg_id
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def abort_missing(msg_id)
|
|
145
|
+
warn "No dead-letter message found with msg_id #{msg_id}."
|
|
146
|
+
exit 1
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def print_help
|
|
150
|
+
puts <<~HELP
|
|
151
|
+
Usage: pgbus dlq <subcommand> [options]
|
|
152
|
+
|
|
153
|
+
Subcommands:
|
|
154
|
+
list [--page N] [--per-page N] List dead-letter messages
|
|
155
|
+
show MSG_ID Show one message (payload filtered)
|
|
156
|
+
retry MSG_ID Re-enqueue a message to its origin queue
|
|
157
|
+
retry-all Re-enqueue every dead-letter message
|
|
158
|
+
purge MSG_ID Discard a single message
|
|
159
|
+
purge --all --yes Discard every dead-letter message
|
|
160
|
+
HELP
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/pgbus/cli.rb
CHANGED
|
@@ -16,8 +16,12 @@ module Pgbus
|
|
|
16
16
|
show_status
|
|
17
17
|
when "queues"
|
|
18
18
|
list_queues
|
|
19
|
+
when "dlq"
|
|
20
|
+
DLQ.start(args[1..] || [])
|
|
19
21
|
when "mcp"
|
|
20
22
|
start_mcp_server
|
|
23
|
+
when "doctor"
|
|
24
|
+
run_doctor
|
|
21
25
|
when "version"
|
|
22
26
|
puts "pgbus #{Pgbus::VERSION}"
|
|
23
27
|
when "help", "--help", "-h"
|
|
@@ -107,7 +111,7 @@ module Pgbus
|
|
|
107
111
|
def apply_capsule_filter(name)
|
|
108
112
|
capsule = Pgbus.configuration.capsule_named(name)
|
|
109
113
|
unless capsule
|
|
110
|
-
available = (Pgbus.configuration.workers || []).filter_map { |c| c[:name]
|
|
114
|
+
available = (Pgbus.configuration.workers || []).filter_map { |c| c[:name] }
|
|
111
115
|
raise ArgumentError,
|
|
112
116
|
"no capsule named #{name.inspect} (available: #{available.join(", ")})"
|
|
113
117
|
end
|
|
@@ -141,6 +145,15 @@ module Pgbus
|
|
|
141
145
|
Pgbus::MCP::Runner.run
|
|
142
146
|
end
|
|
143
147
|
|
|
148
|
+
# Runs the deployment preflight diagnostics and prints the report. Exits 1
|
|
149
|
+
# on any failed check so it can gate a deploy or CI run; exits 0 when all
|
|
150
|
+
# checks pass (warnings do not fail). Doctor itself never raises.
|
|
151
|
+
def run_doctor
|
|
152
|
+
doctor = Pgbus::Doctor.new
|
|
153
|
+
puts doctor.report
|
|
154
|
+
exit 1 unless doctor.success?
|
|
155
|
+
end
|
|
156
|
+
|
|
144
157
|
def list_queues
|
|
145
158
|
Pgbus.client.list_queues
|
|
146
159
|
metrics = Pgbus.client.metrics
|
|
@@ -163,7 +176,11 @@ module Pgbus
|
|
|
163
176
|
start Start the Pgbus supervisor (workers + dispatcher)
|
|
164
177
|
status Show running Pgbus processes
|
|
165
178
|
queues List queues with metrics
|
|
179
|
+
dlq Inspect and drain dead-letter queues
|
|
180
|
+
(list/show/retry/retry-all/purge)
|
|
166
181
|
mcp Start the read-only MCP diagnostic server over stdio
|
|
182
|
+
doctor Run environment diagnostics (config, DB, PGMQ, queues,
|
|
183
|
+
LISTEN/NOTIFY, process liveness); exits 1 on any failure
|
|
167
184
|
version Show version
|
|
168
185
|
help Show this help
|
|
169
186
|
|
|
@@ -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
|