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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.21.1"
4
+ VERSION = "0.21.3"
5
5
  end
@@ -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 { |(qtable, _)| "NOT EXISTS (SELECT 1 FROM pgmq.q_#{qtable} m WHERE m.msg_id = f.msg_id)" }
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, and the lock is released under its table spelling.
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 in the logical queue. Every
1666
- # priority sub-queue is searched: a failed event records the logical
1667
- # queue only, and msg_ids repeat across sub-queues, so the key must
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).each_with_object({}) do |physical, found|
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 row records the logical queue only, and under priority
1726
- # routing a msg_id repeats across the _pN sub-queues, so the job's
1727
- # job_id must match too (issue #577). Two sub-queues' failures with the
1728
- # same msg_id share one row, which keeps the first one's payload: that
1729
- # is the message found (issue #583).
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
- @client.locate_message(event["queue_name"], msg_id: event["msg_id"].to_i, job_id: failed_job_id(event))
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 WHERE msg_id IS NOT NULL AND #{NOT_HANDLER_QUEUE}",
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["queue_name"] }
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
@@ -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}" }
@@ -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.1
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