pgbus 0.21.1 → 0.21.3
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 +17 -0
- data/README.md +3 -2
- data/lib/generators/pgbus/add_failed_events_source_queue_generator.rb +44 -0
- data/lib/generators/pgbus/templates/add_failed_events_source_queue.rb.erb +27 -0
- data/lib/generators/pgbus/templates/add_failed_events_unique_index.rb.erb +5 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +6 -3
- data/lib/pgbus/active_job/executor.rb +29 -9
- data/lib/pgbus/configuration.rb +38 -0
- data/lib/pgbus/failed_event_recorder.rb +135 -42
- data/lib/pgbus/generators/migration_detector.rb +16 -9
- data/lib/pgbus/mcp/health_analyzer.rb +40 -4
- data/lib/pgbus/mcp/tools/health_tool.rb +4 -2
- data/lib/pgbus/process/consumer.rb +12 -5
- data/lib/pgbus/process/heartbeat.rb +12 -1
- data/lib/pgbus/process/readiness_snapshot.rb +14 -6
- data/lib/pgbus/process/supervisor.rb +178 -48
- data/lib/pgbus/process/worker.rb +4 -3
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/job_list.rb +16 -3
- data/lib/pgbus/web/data_source.rb +44 -25
- data/lib/pgbus/web/health_app.rb +1 -1
- data/sig/pgbus/configuration.rbs +4 -0
- metadata +3 -1
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -296,7 +296,7 @@ module Pgbus
|
|
|
296
296
|
@rate_counter.increment(:dequeued, tagged_messages.size)
|
|
297
297
|
client = Pgbus.client
|
|
298
298
|
tagged_messages.each do |queue_name, message, source_queue|
|
|
299
|
-
detect_zombie(queue_name, message)
|
|
299
|
+
detect_zombie(queue_name, message, source_queue)
|
|
300
300
|
@in_flight.increment
|
|
301
301
|
@claim_buffer.push(queue_name, message, source_queue, client: client)
|
|
302
302
|
end
|
|
@@ -603,11 +603,12 @@ module Pgbus
|
|
|
603
603
|
[RESTORE_COOLDOWN_BASE * (2**@restore_streak), RESTORE_COOLDOWN_MAX].min
|
|
604
604
|
end
|
|
605
605
|
|
|
606
|
-
def detect_zombie(queue_name, message)
|
|
606
|
+
def detect_zombie(queue_name, message, source_queue = nil)
|
|
607
607
|
return unless config.zombie_detection
|
|
608
608
|
return unless message.read_ct.to_i > 1
|
|
609
609
|
|
|
610
|
-
return if FailedEventRecorder.exists?(queue_name: queue_name, msg_id: message.msg_id.to_i
|
|
610
|
+
return if FailedEventRecorder.exists?(queue_name: queue_name, msg_id: message.msg_id.to_i,
|
|
611
|
+
source_queue: source_queue || config.queue_name(queue_name))
|
|
611
612
|
|
|
612
613
|
Pgbus.logger.warn do
|
|
613
614
|
"[Pgbus] Zombie message redelivered: queue=#{queue_name} msg_id=#{message.msg_id} " \
|
data/lib/pgbus/version.rb
CHANGED
|
@@ -10,7 +10,8 @@ module Pgbus
|
|
|
10
10
|
# Sources, one UNION ALL fragment each:
|
|
11
11
|
# - every non-DLQ queue table, LEFT JOINed to its pgbus_failed_events row
|
|
12
12
|
# (failed_events.queue_name is the LOGICAL name; the physical one is
|
|
13
|
-
# matched too for rows older code wrote
|
|
13
|
+
# matched too for rows older code wrote; source_queue, when set, names
|
|
14
|
+
# the one table the row's message was read from — issue #583)
|
|
14
15
|
# - failed rows whose message is no longer in its queue ("orphans")
|
|
15
16
|
# - pgbus_blocked_executions (jobs parked by limits_concurrency)
|
|
16
17
|
#
|
|
@@ -187,7 +188,7 @@ module Pgbus
|
|
|
187
188
|
SELECT f.id, f.error_class::text AS error_class, f.error_message,
|
|
188
189
|
f.failed_at::timestamptz AS failed_at
|
|
189
190
|
FROM pgbus_failed_events f
|
|
190
|
-
WHERE f.queue_name IN ('#{logical}', '#{qtable}') AND f.msg_id = m.msg_id
|
|
191
|
+
WHERE f.queue_name IN ('#{logical}', '#{qtable}') AND f.msg_id = m.msg_id#{failed_in(qtable)}
|
|
191
192
|
ORDER BY f.failed_at DESC LIMIT 1
|
|
192
193
|
) f ON true
|
|
193
194
|
SQL
|
|
@@ -197,7 +198,9 @@ module Pgbus
|
|
|
197
198
|
# queue, plus (unfiltered) one for rows whose queue no longer exists.
|
|
198
199
|
def orphan_fragments(scope, fetch)
|
|
199
200
|
fragments = scope[:by_logical].map do |logical, tables|
|
|
200
|
-
missing = tables.map
|
|
201
|
+
missing = tables.map do |(qtable, _)|
|
|
202
|
+
"NOT EXISTS (SELECT 1 FROM pgmq.q_#{qtable} m WHERE m.msg_id = f.msg_id#{failed_in(qtable)})"
|
|
203
|
+
end
|
|
201
204
|
names = quoted_list([logical, *tables.map(&:first)])
|
|
202
205
|
orphan_fragment("'#{logical}'::text", "f.queue_name IN (#{names}) AND #{missing.join(" AND ")}", fetch)
|
|
203
206
|
end
|
|
@@ -209,6 +212,16 @@ module Pgbus
|
|
|
209
212
|
fragments << orphan_fragment("f.queue_name::text", where, fetch)
|
|
210
213
|
end
|
|
211
214
|
|
|
215
|
+
# Under priority routing each _pN sub-queue numbers its messages from
|
|
216
|
+
# its own sequence: a row that names its table belongs to that table's
|
|
217
|
+
# message only. A row written before source_queue (NULL), or any row
|
|
218
|
+
# before the column is migrated, matches every table of its queue.
|
|
219
|
+
def failed_in(qtable)
|
|
220
|
+
return "" unless FailedEventRecorder.source_queue_column?
|
|
221
|
+
|
|
222
|
+
" AND (f.source_queue IS NULL OR f.source_queue = '#{qtable}')"
|
|
223
|
+
end
|
|
224
|
+
|
|
212
225
|
def orphan_fragment(logical_sql, where, fetch)
|
|
213
226
|
<<~SQL.strip
|
|
214
227
|
(SELECT 'failed'::text AS source, f.id, f.queue_name::text AS queue_name, #{logical_sql} AS logical_queue,
|
|
@@ -1340,7 +1340,8 @@ module Pgbus
|
|
|
1340
1340
|
(SELECT CASE WHEN is_called THEN last_value ELSE 0 END FROM pgmq.#{seq_name}) AS total_messages,
|
|
1341
1341
|
(SELECT max(read_ct) FROM pgmq.#{qtable}) AS max_read_ct,
|
|
1342
1342
|
(SELECT count(*) FROM pgmq.#{qtable} WHERE vt <= NOW() AND read_ct = 0) AS visible_unread_length,
|
|
1343
|
-
(SELECT EXTRACT(epoch FROM (NOW() - min(vt)))::int FROM pgmq.#{qtable} WHERE vt <= NOW()) AS oldest_claimable_age_sec
|
|
1343
|
+
(SELECT EXTRACT(epoch FROM (NOW() - min(vt)))::int FROM pgmq.#{qtable} WHERE vt <= NOW()) AS oldest_claimable_age_sec,
|
|
1344
|
+
(SELECT EXTRACT(epoch FROM (NOW() - min(vt)))::int FROM pgmq.#{qtable} WHERE vt <= NOW() AND read_ct = 0) AS oldest_unread_claimable_age_sec
|
|
1344
1345
|
SQL
|
|
1345
1346
|
rescue StandardError => e
|
|
1346
1347
|
Pgbus.logger.debug { "[Pgbus::Web] Skipping queue metrics for #{name}: #{e.message}" }
|
|
@@ -1362,7 +1363,8 @@ module Pgbus
|
|
|
1362
1363
|
newest_msg_age_sec: row["newest_msg_age_sec"]&.to_i,
|
|
1363
1364
|
total_messages: row["total_messages"].to_i,
|
|
1364
1365
|
max_read_ct: row["max_read_ct"]&.to_i,
|
|
1365
|
-
visible_unread_length: row["visible_unread_length"].to_i
|
|
1366
|
+
visible_unread_length: row["visible_unread_length"].to_i,
|
|
1367
|
+
oldest_unread_claimable_age_sec: row["oldest_unread_claimable_age_sec"]&.to_i
|
|
1366
1368
|
}
|
|
1367
1369
|
end
|
|
1368
1370
|
rescue StandardError => e
|
|
@@ -1382,6 +1384,7 @@ module Pgbus
|
|
|
1382
1384
|
EXTRACT(epoch FROM (NOW() - max(enqueued_at)))::int AS newest_msg_age_sec,
|
|
1383
1385
|
EXTRACT(epoch FROM (NOW() - min(enqueued_at)))::int AS oldest_msg_age_sec,
|
|
1384
1386
|
EXTRACT(epoch FROM (NOW() - min(vt) FILTER (WHERE vt <= NOW())))::int AS oldest_claimable_age_sec,
|
|
1387
|
+
EXTRACT(epoch FROM (NOW() - min(vt) FILTER (WHERE vt <= NOW() AND read_ct = 0)))::int AS oldest_unread_claimable_age_sec,
|
|
1385
1388
|
max(read_ct) AS max_read_ct,
|
|
1386
1389
|
count(CASE WHEN vt <= NOW() AND read_ct = 0 THEN 1 END) AS visible_unread_length
|
|
1387
1390
|
FROM pgmq.#{qtable}
|
|
@@ -1398,6 +1401,7 @@ module Pgbus
|
|
|
1398
1401
|
q_summary.oldest_claimable_age_sec,
|
|
1399
1402
|
q_summary.max_read_ct,
|
|
1400
1403
|
q_summary.visible_unread_length,
|
|
1404
|
+
q_summary.oldest_unread_claimable_age_sec,
|
|
1401
1405
|
all_metrics.total_messages
|
|
1402
1406
|
FROM q_summary, all_metrics
|
|
1403
1407
|
SQL
|
|
@@ -1414,7 +1418,8 @@ module Pgbus
|
|
|
1414
1418
|
newest_msg_age_sec: row["newest_msg_age_sec"]&.to_i,
|
|
1415
1419
|
total_messages: row["total_messages"].to_i,
|
|
1416
1420
|
max_read_ct: row["max_read_ct"]&.to_i,
|
|
1417
|
-
visible_unread_length: row["visible_unread_length"].to_i
|
|
1421
|
+
visible_unread_length: row["visible_unread_length"].to_i,
|
|
1422
|
+
oldest_unread_claimable_age_sec: row["oldest_unread_claimable_age_sec"]&.to_i
|
|
1418
1423
|
}
|
|
1419
1424
|
rescue StandardError => e
|
|
1420
1425
|
Pgbus.logger.error { "[Pgbus::Web] Error fetching metrics for #{queue_name}: #{e.class}: #{e.message}" }
|
|
@@ -1627,20 +1632,22 @@ module Pgbus
|
|
|
1627
1632
|
# lock handed to a retry before the failure would count as the job's.
|
|
1628
1633
|
# A message that is gone has no send time, so only a bound lock goes.
|
|
1629
1634
|
def release_locks_held_by_failed_events(condition, binds)
|
|
1635
|
+
source = FailedEventRecorder.source_queue_column? ? ", source_queue" : ""
|
|
1630
1636
|
rows = connection.select_all(
|
|
1631
|
-
"SELECT queue_name, msg_id, payload FROM pgbus_failed_events WHERE #{condition}",
|
|
1637
|
+
"SELECT queue_name, msg_id, payload#{source} FROM pgbus_failed_events WHERE #{condition}",
|
|
1632
1638
|
"Pgbus Collect Failed Keys", binds
|
|
1633
1639
|
)
|
|
1634
1640
|
|
|
1635
1641
|
# queue_name is the logical name the executor saw: the message is looked
|
|
1636
|
-
# up under it
|
|
1642
|
+
# up under it (or in the table source_queue names), and the lock is
|
|
1643
|
+
# released under its table spelling.
|
|
1637
1644
|
# A key proc may return a number; the queue lookup reads it as text.
|
|
1638
1645
|
holders = rows.to_a.filter_map do |row|
|
|
1639
1646
|
key = extract_uniqueness_key_from_payload_str(row["payload"])&.to_s
|
|
1640
|
-
[key, row["queue_name"], row["msg_id"].to_i] if key && row["msg_id"]
|
|
1647
|
+
[key, row["queue_name"], row["msg_id"].to_i, row["source_queue"]] if key && row["msg_id"]
|
|
1641
1648
|
end
|
|
1642
|
-
holders = holders.group_by { |_key, queue, _msg_id| queue }.flat_map do |queue, entries|
|
|
1643
|
-
failed_queue_lock_holders(queue, entries)
|
|
1649
|
+
holders = holders.group_by { |_key, queue, _msg_id, table| [queue, table] }.flat_map do |(queue, table), entries|
|
|
1650
|
+
failed_queue_lock_holders(queue, table, entries)
|
|
1644
1651
|
end
|
|
1645
1652
|
UniquenessKey.release_all_held_by!(holders) if holders.any?
|
|
1646
1653
|
rescue StandardError => e
|
|
@@ -1650,8 +1657,8 @@ module Pgbus
|
|
|
1650
1657
|
# The release tuples for one queue's failed jobs. A queue that cannot
|
|
1651
1658
|
# be resolved (a stored name no longer valid as a table name) is skipped
|
|
1652
1659
|
# with a warning, so it does not stop the other queues' release.
|
|
1653
|
-
def failed_queue_lock_holders(queue, entries)
|
|
1654
|
-
sent = queued_send_times(queue, entries)
|
|
1660
|
+
def failed_queue_lock_holders(queue, table, entries)
|
|
1661
|
+
sent = queued_send_times(queue, table, entries)
|
|
1655
1662
|
lock_queue = Uniqueness.lock_queue_name(queue)
|
|
1656
1663
|
entries.map { |key, _queue, msg_id| [key, lock_queue, msg_id, sent[[msg_id, key]]] }
|
|
1657
1664
|
rescue ActiveRecord::StatementInvalid
|
|
@@ -1662,13 +1669,15 @@ module Pgbus
|
|
|
1662
1669
|
end
|
|
1663
1670
|
|
|
1664
1671
|
# { [msg_id, uniqueness key] => enqueued_at } for the messages of
|
|
1665
|
-
# +entries+ ([key, queue, msg_id]) still
|
|
1666
|
-
#
|
|
1667
|
-
#
|
|
1672
|
+
# +entries+ ([key, queue, msg_id, table]) still queued. A row that names
|
|
1673
|
+
# its table (issue #583) is that table's message. A row written before
|
|
1674
|
+
# source_queue records the logical queue only, so every priority
|
|
1675
|
+
# sub-queue is searched: msg_ids repeat across them, so the key must
|
|
1668
1676
|
# match too, and the earliest send wins.
|
|
1669
|
-
def queued_send_times(queue, entries)
|
|
1677
|
+
def queued_send_times(queue, table, entries)
|
|
1670
1678
|
msg_ids = "{#{entries.map { |_key, _queue, msg_id| msg_id }.uniq.join(",")}}"
|
|
1671
|
-
@client.physical_queue_names(queue)
|
|
1679
|
+
tables = table ? [table] : @client.physical_queue_names(queue)
|
|
1680
|
+
tables.each_with_object({}) do |physical, found|
|
|
1672
1681
|
queued_rows(physical, msg_ids).each do |row|
|
|
1673
1682
|
pair = [row["msg_id"].to_i, row["lock_key"]]
|
|
1674
1683
|
found[pair] = [found[pair], row["enqueued_at"]].compact.min
|
|
@@ -1722,15 +1731,22 @@ module Pgbus
|
|
|
1722
1731
|
end
|
|
1723
1732
|
|
|
1724
1733
|
# The physical queue holding a failed job's message, or nil when it is
|
|
1725
|
-
# gone. The
|
|
1726
|
-
#
|
|
1727
|
-
#
|
|
1728
|
-
#
|
|
1729
|
-
#
|
|
1734
|
+
# gone. The job's job_id must match too: under priority routing a msg_id
|
|
1735
|
+
# repeats across the _pN sub-queues (issue #577). A row that names the
|
|
1736
|
+
# table its message was read from is looked up there alone (issue
|
|
1737
|
+
# #583); a row written before source_queue, under every table of its
|
|
1738
|
+
# logical queue.
|
|
1730
1739
|
def failed_message_queue(event)
|
|
1731
1740
|
return unless event["queue_name"] && event["msg_id"]
|
|
1732
1741
|
|
|
1733
|
-
|
|
1742
|
+
name, prefixed = failed_message_scope(event)
|
|
1743
|
+
@client.locate_message(name, msg_id: event["msg_id"].to_i, job_id: failed_job_id(event), prefixed: prefixed)
|
|
1744
|
+
end
|
|
1745
|
+
|
|
1746
|
+
# [queue, prefixed] to look a failed row's message up under: the physical
|
|
1747
|
+
# table the row names, else its logical queue.
|
|
1748
|
+
def failed_message_scope(event)
|
|
1749
|
+
event["source_queue"] ? [event["source_queue"], false] : [event["queue_name"], true]
|
|
1734
1750
|
end
|
|
1735
1751
|
|
|
1736
1752
|
def failed_job_id(event)
|
|
@@ -1756,16 +1772,19 @@ module Pgbus
|
|
|
1756
1772
|
# per-row roundtrips. Falls back to per-row archive inside a rescue if
|
|
1757
1773
|
# a batch fails, so one bad queue can't block progress on the others.
|
|
1758
1774
|
def archive_all_failed_messages
|
|
1775
|
+
source = FailedEventRecorder.source_queue_column? ? ", source_queue" : ""
|
|
1759
1776
|
rows = connection.select_all(
|
|
1760
|
-
"SELECT id, queue_name, msg_id, payload FROM pgbus_failed_events
|
|
1777
|
+
"SELECT id, queue_name, msg_id, payload#{source} FROM pgbus_failed_events " \
|
|
1778
|
+
"WHERE msg_id IS NOT NULL AND #{NOT_HANDLER_QUEUE}",
|
|
1761
1779
|
"Pgbus Collect Failed Messages", [handler_queue_names_literal]
|
|
1762
1780
|
)
|
|
1763
1781
|
|
|
1764
|
-
grouped = rows.to_a.group_by { |row| row
|
|
1765
|
-
grouped.each do |queue_name, events|
|
|
1782
|
+
grouped = rows.to_a.group_by { |row| failed_message_scope(row) }
|
|
1783
|
+
grouped.each do |(queue_name, prefixed), events|
|
|
1766
1784
|
# A priority queue's msg_ids repeat across its _pN sub-queues, so
|
|
1767
1785
|
# each message is located first and archived from its own table.
|
|
1768
|
-
located = @client.locate_messages(queue_name, events.map { |row| [row["msg_id"].to_i, failed_job_id(row)] }
|
|
1786
|
+
located = @client.locate_messages(queue_name, events.map { |row| [row["msg_id"].to_i, failed_job_id(row)] },
|
|
1787
|
+
prefixed: prefixed)
|
|
1769
1788
|
located.group_by { |_msg_id, physical| physical }.each do |physical, pairs|
|
|
1770
1789
|
@client.archive_batch(physical, pairs.map(&:first), prefixed: false)
|
|
1771
1790
|
end
|
data/lib/pgbus/web/health_app.rb
CHANGED
|
@@ -98,7 +98,7 @@ module Pgbus
|
|
|
98
98
|
def local_readyz
|
|
99
99
|
snapshot = @local_readiness.call
|
|
100
100
|
status = snapshot.ready? ? 200 : 503
|
|
101
|
-
body = { status: snapshot.status, expected: snapshot.expected, live: snapshot.live }
|
|
101
|
+
body = { status: snapshot.status, expected: snapshot.expected, live: snapshot.live, forked: snapshot.forked }
|
|
102
102
|
[status, JSON_HEADERS.dup, [body.to_json]]
|
|
103
103
|
rescue StandardError => e
|
|
104
104
|
Pgbus.logger.error { "[Pgbus::Web::HealthApp] local readiness check failed: #{e.class}: #{e.message}" }
|
data/sig/pgbus/configuration.rbs
CHANGED
|
@@ -24,6 +24,8 @@ module Pgbus
|
|
|
24
24
|
def batch_stall_threshold=: (untyped value) -> untyped
|
|
25
25
|
def batch_sweep_interval: () -> Numeric?
|
|
26
26
|
def batch_sweep_interval=: (untyped value) -> untyped
|
|
27
|
+
def boot_timeout: () -> Numeric?
|
|
28
|
+
def boot_timeout=: (untyped) -> untyped
|
|
27
29
|
def capsule: (untyped name, queues: untyped, threads: untyped, **untyped options) -> untyped
|
|
28
30
|
def capsule_named: (untyped name) -> untyped
|
|
29
31
|
def circuit_breaker_enabled: () -> bool
|
|
@@ -83,6 +85,8 @@ module Pgbus
|
|
|
83
85
|
def health_bind: () -> String
|
|
84
86
|
def health_bind=: (untyped) -> untyped
|
|
85
87
|
def health_port: () -> untyped
|
|
88
|
+
def health_unclaimed_grace: () -> untyped
|
|
89
|
+
def health_unclaimed_grace=: (untyped value) -> untyped
|
|
86
90
|
def health_port=: (untyped) -> untyped
|
|
87
91
|
def idempotency_ttl: () -> Numeric?
|
|
88
92
|
def idempotency_ttl=: (untyped value) -> untyped
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgbus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.21.
|
|
4
|
+
version: 0.21.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -287,6 +287,7 @@ files:
|
|
|
287
287
|
- lib/generators/pgbus/add_batch_executions_generator.rb
|
|
288
288
|
- lib/generators/pgbus/add_deploy_tasks_generator.rb
|
|
289
289
|
- lib/generators/pgbus/add_failed_events_index_generator.rb
|
|
290
|
+
- lib/generators/pgbus/add_failed_events_source_queue_generator.rb
|
|
290
291
|
- lib/generators/pgbus/add_job_stats_generator.rb
|
|
291
292
|
- lib/generators/pgbus/add_job_stats_latency_generator.rb
|
|
292
293
|
- lib/generators/pgbus/add_job_stats_queue_index_generator.rb
|
|
@@ -307,6 +308,7 @@ files:
|
|
|
307
308
|
- lib/generators/pgbus/templates/add_batch_callback_jobs.rb.erb
|
|
308
309
|
- lib/generators/pgbus/templates/add_batch_executions.rb.erb
|
|
309
310
|
- lib/generators/pgbus/templates/add_deploy_tasks_tables.rb.erb
|
|
311
|
+
- lib/generators/pgbus/templates/add_failed_events_source_queue.rb.erb
|
|
310
312
|
- lib/generators/pgbus/templates/add_failed_events_unique_index.rb.erb
|
|
311
313
|
- lib/generators/pgbus/templates/add_job_stats.rb.erb
|
|
312
314
|
- lib/generators/pgbus/templates/add_job_stats_latency.rb.erb
|