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
data/Rakefile
CHANGED
|
@@ -8,7 +8,16 @@ end
|
|
|
8
8
|
|
|
9
9
|
require "rubocop/rake_task"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Lint only the gem's own source — NOT the nested docs/ site. docs/ is a separate
|
|
12
|
+
# consuming app with its own bundle and .rubocop.yml (it inherit_gems
|
|
13
|
+
# rubocop-rails-omakase and requires docs_kit/rubocop, both absent from the gem's
|
|
14
|
+
# bundle). RuboCop loads a directory's .rubocop.yml while scanning it — before
|
|
15
|
+
# AllCops/Exclude applies — so a bare run discovers docs/.rubocop.yml and crashes
|
|
16
|
+
# on the unresolvable gem inheritance. Passing explicit paths stops the
|
|
17
|
+
# discovery. docs/ lints itself in the Docs site workflow. (Same fix as docs-kit.)
|
|
18
|
+
RuboCop::RakeTask.new do |task|
|
|
19
|
+
task.patterns = %w[app benchmarks config lib spec Gemfile Rakefile pgbus.gemspec]
|
|
20
|
+
end
|
|
12
21
|
|
|
13
22
|
namespace :bench do
|
|
14
23
|
bench_dir = "benchmarks"
|
|
@@ -29,6 +29,43 @@ module Pgbus
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Order the rate keys are rendered in, and their i18n label suffixes.
|
|
33
|
+
WORKER_RATE_KEYS = %w[processed failed dequeued].freeze
|
|
34
|
+
|
|
35
|
+
# Metadata keys rendered specially elsewhere (throughput badge, health
|
|
36
|
+
# status) and therefore excluded from the generic key/value dump.
|
|
37
|
+
WORKER_INTERNAL_METADATA_KEYS = %w[rates jobs_processed jobs_failed in_flight loop_tick_at].freeze
|
|
38
|
+
|
|
39
|
+
# The subset of a process's metadata to render as generic key/value badges:
|
|
40
|
+
# everything except the throughput/health keys that have dedicated rendering.
|
|
41
|
+
def pgbus_display_metadata(metadata)
|
|
42
|
+
return {} unless metadata.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
metadata.reject { |k, _| WORKER_INTERNAL_METADATA_KEYS.include?(k.to_s) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Render a worker's per-second throughput rates (from heartbeat metadata)
|
|
48
|
+
# as a compact human-readable string, e.g. "12.4/s processed · 0.2/s failed".
|
|
49
|
+
# Zero rates are omitted; returns nil when there are no non-zero rates so
|
|
50
|
+
# callers can fall back to the raw metadata rendering.
|
|
51
|
+
def pgbus_worker_rates(metadata)
|
|
52
|
+
return nil unless metadata.is_a?(Hash)
|
|
53
|
+
|
|
54
|
+
rates = metadata["rates"]
|
|
55
|
+
return nil unless rates.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
parts = WORKER_RATE_KEYS.filter_map do |key|
|
|
58
|
+
value = rates[key].to_f
|
|
59
|
+
next if value.zero?
|
|
60
|
+
|
|
61
|
+
label = I18n.t("pgbus.processes.processes_table.rates.#{key}", default: key)
|
|
62
|
+
"#{value}/s #{label}"
|
|
63
|
+
end
|
|
64
|
+
return nil if parts.empty?
|
|
65
|
+
|
|
66
|
+
parts.join(" · ")
|
|
67
|
+
end
|
|
68
|
+
|
|
32
69
|
def pgbus_status_badge(healthy_or_status)
|
|
33
70
|
status = case healthy_or_status
|
|
34
71
|
when true then :healthy
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
<td data-label="Last Heartbeat" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(p[:last_heartbeat_at]) %></td>
|
|
22
22
|
<td data-label="Metadata" class="px-4 py-3 text-sm text-gray-500 font-mono text-xs max-w-xs truncate">
|
|
23
23
|
<% if p[:metadata].is_a?(Hash) %>
|
|
24
|
-
<% p[:metadata]
|
|
24
|
+
<% if (rates = pgbus_worker_rates(p[:metadata])) %>
|
|
25
|
+
<span class="inline-flex items-center rounded bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 px-1.5 py-0.5 text-xs mr-1 font-medium"><%= rates %></span>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% pgbus_display_metadata(p[:metadata]).each do |k, v| %>
|
|
25
28
|
<span class="inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 text-xs mr-1"><%= k %>: <%= v %></span>
|
|
26
29
|
<% end %>
|
|
27
30
|
<% end %>
|
data/config/locales/da.yml
CHANGED
data/config/locales/de.yml
CHANGED
data/config/locales/en.yml
CHANGED
data/config/locales/es.yml
CHANGED
data/config/locales/fi.yml
CHANGED
data/config/locales/fr.yml
CHANGED
data/config/locales/it.yml
CHANGED
data/config/locales/ja.yml
CHANGED
data/config/locales/nb.yml
CHANGED
data/config/locales/nl.yml
CHANGED
data/config/locales/pt.yml
CHANGED
data/config/locales/sv.yml
CHANGED
|
@@ -6,13 +6,13 @@ require_relative "migration_path"
|
|
|
6
6
|
|
|
7
7
|
module Pgbus
|
|
8
8
|
module Generators
|
|
9
|
-
class
|
|
9
|
+
class AddUniquenessKeysGenerator < Rails::Generators::Base
|
|
10
10
|
include ActiveRecord::Generators::Migration
|
|
11
11
|
include MigrationPath
|
|
12
12
|
|
|
13
13
|
source_root File.expand_path("templates", __dir__)
|
|
14
14
|
|
|
15
|
-
desc "Add
|
|
15
|
+
desc "Add uniqueness keys table for ensures_uniqueness support"
|
|
16
16
|
|
|
17
17
|
class_option :database,
|
|
18
18
|
type: :string,
|
|
@@ -20,13 +20,13 @@ module Pgbus
|
|
|
20
20
|
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
|
|
21
21
|
|
|
22
22
|
def create_migration_file
|
|
23
|
-
migration_template "
|
|
24
|
-
File.join(pgbus_migrate_path, "
|
|
23
|
+
migration_template "add_uniqueness_keys.rb.erb",
|
|
24
|
+
File.join(pgbus_migrate_path, "add_pgbus_uniqueness_keys.rb")
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def display_post_install
|
|
28
28
|
say ""
|
|
29
|
-
say "Pgbus
|
|
29
|
+
say "Pgbus uniqueness keys table installed!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
32
|
say " 1. Run: rails db:migrate#{":#{options[:database]}" if separate_database?}"
|
|
@@ -151,7 +151,7 @@ module Pgbus
|
|
|
151
151
|
msg_ids = Pgbus.client.send_batch(queue, payloads)
|
|
152
152
|
|
|
153
153
|
unless msg_ids.is_a?(Array) && msg_ids.size == jobs.size
|
|
154
|
-
raise "Pgbus batch enqueue failed: expected #{jobs.size} ids, got #{msg_ids&.size || 0}"
|
|
154
|
+
raise Pgbus::EnqueueError, "Pgbus batch enqueue failed: expected #{jobs.size} ids, got #{msg_ids&.size || 0}"
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
jobs.zip(msg_ids).each { |job, id| job.provider_job_id = id }
|
|
@@ -296,14 +296,35 @@ module Pgbus
|
|
|
296
296
|
Pgbus.logger.warn { "[Pgbus] Batch discard signal failed: #{e.message}" }
|
|
297
297
|
end
|
|
298
298
|
|
|
299
|
+
# Archiving is idempotent — archiving an already-archived message is a
|
|
300
|
+
# no-op — so a connection error here is safe to retry once, unlike sends
|
|
301
|
+
# where a retry could duplicate the message. Without the retry, a job
|
|
302
|
+
# that succeeded but failed to archive redelivers after VT expiry and
|
|
303
|
+
# runs twice. If the retry also fails, fall through to the normal
|
|
304
|
+
# failure path (recorded failure + VT-based redelivery).
|
|
299
305
|
def archive_from(queue_name, msg_id, source_queue: nil)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
attempts = 0
|
|
307
|
+
begin
|
|
308
|
+
if source_queue
|
|
309
|
+
client.archive_message(source_queue, msg_id, prefixed: false)
|
|
310
|
+
else
|
|
311
|
+
client.archive_message(queue_name, msg_id)
|
|
312
|
+
end
|
|
313
|
+
rescue StandardError => e
|
|
314
|
+
attempts += 1
|
|
315
|
+
raise unless attempts == 1 && connection_error?(e)
|
|
316
|
+
|
|
317
|
+
Pgbus.logger.warn do
|
|
318
|
+
"[Pgbus::Executor] Archive failed on connection error, retrying once: #{e.message}"
|
|
319
|
+
end
|
|
320
|
+
retry
|
|
304
321
|
end
|
|
305
322
|
end
|
|
306
323
|
|
|
324
|
+
def connection_error?(error)
|
|
325
|
+
defined?(PGMQ::Errors::ConnectionError) && error.is_a?(PGMQ::Errors::ConnectionError)
|
|
326
|
+
end
|
|
327
|
+
|
|
307
328
|
def handle_dead_letter(message, queue_name, payload, source_queue: nil)
|
|
308
329
|
Pgbus.logger.warn do
|
|
309
330
|
job_class = payload["job_class"] || "unknown"
|
|
@@ -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
|
|