pgbus 0.21.0 → 0.21.1
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 +8 -0
- data/lib/pgbus/batch/sweep.rb +10 -5
- data/lib/pgbus/client.rb +109 -42
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +39 -22
- data/sig/pgbus/client.rbs +7 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59ff2a591041f47789d41f241c5496534dbba5314ebb37a0c2fe41286f611587
|
|
4
|
+
data.tar.gz: 9f8b3c5a4f88330b96cd6ecab5d4dfe18580d48765b3aa50873e3f54150f63b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76c28daff8340ec68e0cf5b0b7f9a69ad48b1d024d65ff9af37fbc734167a8b710afca0b1fce9163783b43e022f0d8e6d4d465219aa91282b52eb69d770e969b
|
|
7
|
+
data.tar.gz: 8f2b9d1f6c4f899c64a9dd0fa1918b64e158799e6172bb0e42cf7beac6ca191675e5df9c31e90eaa62c4f1714c6c55eb317612e8d7b6047ab3e069d5bbab6046
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **The dashboard's Retry, Discard and Discard all reach a failed job's message under priority routing (issue #577).** A failed-job row records the logical queue only, and with `priority_levels > 1` the message sits in a `_pN` sub-queue, never in the base table. Retry reset the visibility timeout on `pgmq.q_pgbus_<queue>`, which priority routing never creates, and answered "Could not retry job."; Discard and Discard all archived from that same table, so they reported the job discarded while its message stayed queued and ran again. Each sub-queue also numbers its messages from its own sequence, so the msg_id alone could name another job's message in a sibling sub-queue. The dashboard now finds the sub-queue whose message has the row's msg_id and carries the job's `job_id` (`Pgbus::Client#locate_message`, which returns that physical table or nil, and `#locate_messages` for many at once, one query per table), resets or archives the message there, and leaves a same-numbered message elsewhere alone. Discard all locates a queue's messages together and archives them in one batch per table. When no sub-queue holds it, Retry enqueues a fresh copy as before. Without priority routing there is one table and nothing changes, except that a message reusing the msg_id under another `job_id` is no longer reset or archived in the job's place. Refs #577.
|
|
6
|
+
|
|
3
7
|
## [0.21.0] - 2026-10-19
|
|
4
8
|
|
|
5
9
|
### Added
|
|
@@ -11,11 +15,15 @@
|
|
|
11
15
|
### Changed
|
|
12
16
|
|
|
13
17
|
- **`actor:` and `reason:` are reserved keywords of `Pgbus::Workflow.start!` (issue #571).** Like `idempotency_key:`, they are taken for the audit trail and no longer reach the run's `arguments`. A workflow that receives an argument literally named `actor` or `reason` through `start!` renames it, e.g. `start!(subject, requested_by: "alice")` read as `arguments[:requested_by]`. Runs already started keep their stored arguments, and `rerun!` copies them as stored. Refs #571.
|
|
18
|
+
- **`Pgbus::Client#message_exists?`, `#message_in_queue?`, `#message_archived?`, `#message_with_job_id?` and `#dead_letter_physical_name` take a logical queue name unless you pass `prefixed: false` (issue #569).** They used to guess that a name starting with `"#{queue_prefix}_"` was already physical. **If you call one of them with a physical table name, add `prefixed: false`;** without it the name is prefixed again. Under priority routing two of them also answer differently for a logical name: `message_archived?` probes every `_pN` archive (it probed a base table that priority routing never creates, and answered nil), and `dead_letter_physical_name` keeps a logical name's own `_pN` suffix (`orders_p2` → `…_orders_p2_dlq`, the queue `move_to_dead_letter` writes), stripping it only from a physical sub-queue name passed with `prefixed: false`.
|
|
19
|
+
- **The batch completion callback is `on_complete`.** `Pgbus::Batch.new(on_complete:)`, the builder's `b.on_complete` and the `#on_complete` reader replace `on_finish`, which keeps working as a deprecated alias until 1.0 and logs one warning per process. The database columns stay `on_finish_class` / `on_finish_job` until the 1.0 schema cleanup, so batches created before upgrading still fire, and the `pgbus.batch_finished` instrumentation event keeps its name. The dashboard's batch page labels the callback "On Complete". Refs #538.
|
|
20
|
+
|
|
14
21
|
|
|
15
22
|
### Fixed
|
|
16
23
|
|
|
17
24
|
- **The workflows one-ongoing index predicate dumps the same `schema.rb` line from a migrated and from a schema-loaded database (issue #573).** `pgbus:add_workflows` and the fresh-install migration declared `idx_pgbus_workflows_one_ongoing` with `where: "status IN ('ready','performing','waiting','paused')"`. `status` is a `varchar`, so Postgres stored the list as `varchar` literals cast to `text[]`; `db:schema:load` parses that dumped string into a different expression, and each later dump flipped the line depending on which kind of database it came from. Both templates now write `status::text IN (…)`, whose stored form replays to itself. The index means the same either way, so existing installs need nothing. An app that wants a stable dump can recreate the index in a migration of its own: `remove_index :pgbus_workflows, name: "idx_pgbus_workflows_one_ongoing"`, then `add_index :pgbus_workflows, [:workflow_class, :subject_type, :subject_id], unique: true, where: "status::text IN ('ready','performing','waiting','paused')", name: "idx_pgbus_workflows_one_ongoing"` (both run in the migration's transaction, so the constraint never lapses; `remove_index` takes an ACCESS EXCLUSIVE lock, so `pgbus_workflows` is locked against reads and writes (workers, workflow starts and the dashboard wait) until the migration commits), and dump the schema again. Refs #573.
|
|
18
25
|
- **A guard that ends a `wait_for:` step after a reattempt consumes the signal the step holds (issue #572).** A woken attempt that called `reattempt!` (or failed into a pause and was resumed) kept its signal `claimed` for the next attempt, whose row carries no `signal_id` until its gate runs. When `cancel_if` or `skip_if` ended that next attempt (or a guard raised and its `on_exception` policy skipped or cancelled, or a deploy had declared the step a `removed_step` meanwhile), the signal stayed `claimed` forever. The attempt now records the held signal before its outcome, so it is consumed when the step ends and kept when it reattempts or pauses, as for any attempt. Refs #572.
|
|
26
|
+
- **A queue whose logical name starts with the prefix is probed in its own table (issue #569).** `Pgbus::Client#message_exists?`, `#message_in_queue?`, `#message_archived?`, `#message_with_job_id?` and `#dead_letter_physical_name` decided whether a name was already physical by `name.start_with?("#{queue_prefix}_")`, so a logical `pgbus_mailers` (table `pgbus_pgbus_mailers`) was probed as `pgmq.q_pgbus_mailers`. The answer was "unknown", or another queue's answer when that table existed. The dispatcher kept an orphaned uniqueness lock under such a queue forever. The dashboard's failed-event Retry enqueued a copy next to the original message. The batch sweep kept an orphan execution row forever, or un-counted a dead-lettered job. The name's spelling no longer decides anything: these methods take a logical name, expanded through `config.queue_name` and the queue strategy, unless the caller passes `prefixed: false` for a physical table name. This is the `prefixed:` convention `delete_message`, `archive_message`, `set_visibility_timeout`, `purge_queue` and `drop_queue` already follow. `message_archived?` now also expands a logical name to every `_pN` archive under priority routing, as `message_exists?` already did; before, it probed a base table that does not exist there. The batch sweep now passes `prefixed: false` for a backfilled execution row (it holds the table its message went to) and the logical name for an orphan row. The caller-facing change is under Changed.
|
|
19
27
|
|
|
20
28
|
## [0.20.1] - 2026-10-18
|
|
21
29
|
|
data/lib/pgbus/batch/sweep.rb
CHANGED
|
@@ -38,14 +38,17 @@ module Pgbus
|
|
|
38
38
|
swept
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# A row with a msg_id was backfilled with the physical table its message
|
|
42
|
+
# was sent to (the `_pN` sub-queue under priority routing), so every
|
|
43
|
+
# probe takes that name as is.
|
|
41
44
|
def classify_stale(row, client)
|
|
42
|
-
in_queue = client.message_in_queue?(row.queue_name, msg_id: row.msg_id)
|
|
45
|
+
in_queue = client.message_in_queue?(row.queue_name, msg_id: row.msg_id, prefixed: false)
|
|
43
46
|
return :still_present if in_queue != false
|
|
44
47
|
|
|
45
|
-
dlq = client.dead_letter_physical_name(row.queue_name)
|
|
46
|
-
return :failed if client.message_with_job_id?(dlq, job_id: row.job_id) == true
|
|
48
|
+
dlq = client.dead_letter_physical_name(row.queue_name, prefixed: false)
|
|
49
|
+
return :failed if client.message_with_job_id?(dlq, job_id: row.job_id, prefixed: false) == true
|
|
47
50
|
|
|
48
|
-
return :completed if client.message_archived?(row.queue_name, msg_id: row.msg_id) == true
|
|
51
|
+
return :completed if client.message_archived?(row.queue_name, msg_id: row.msg_id, prefixed: false) == true
|
|
49
52
|
|
|
50
53
|
Pgbus.logger.warn do
|
|
51
54
|
"[Pgbus] Batch execution #{row.job_id} (batch #{row.batch_id}) has no queue, " \
|
|
@@ -84,6 +87,8 @@ module Pgbus
|
|
|
84
87
|
swept
|
|
85
88
|
end
|
|
86
89
|
|
|
90
|
+
# A row without a msg_id still holds the logical queue name it was
|
|
91
|
+
# inserted with (the job's queue_name); the backfill was never applied.
|
|
87
92
|
def classify_orphan(row, client)
|
|
88
93
|
return :live if row.queue_name.nil?
|
|
89
94
|
|
|
@@ -91,7 +96,7 @@ module Pgbus
|
|
|
91
96
|
return :live if in_queue != false
|
|
92
97
|
|
|
93
98
|
dlq = client.dead_letter_physical_name(row.queue_name)
|
|
94
|
-
return :failed if client.message_with_job_id?(dlq, job_id: row.job_id) == true
|
|
99
|
+
return :failed if client.message_with_job_id?(dlq, job_id: row.job_id, prefixed: false) == true
|
|
95
100
|
|
|
96
101
|
:orphan
|
|
97
102
|
end
|
data/lib/pgbus/client.rb
CHANGED
|
@@ -720,16 +720,24 @@ module Pgbus
|
|
|
720
720
|
# reaper to determine if a uniqueness lock with msg_id=0 (placeholder)
|
|
721
721
|
# still has a corresponding queue message.
|
|
722
722
|
#
|
|
723
|
-
# +queue_name+
|
|
724
|
-
#
|
|
723
|
+
# +queue_name+ is a logical name (e.g. "default"), expanded through the
|
|
724
|
+
# queue strategy, unless the caller passes +prefixed: false+ for a physical
|
|
725
|
+
# table name (e.g. "pgbus_default_p1"), used as is. The name's spelling
|
|
726
|
+
# decides nothing: a logical "pgbus_mailers" lives in pgbus_pgbus_mailers
|
|
727
|
+
# (issue #569).
|
|
725
728
|
#
|
|
726
729
|
# Returns:
|
|
727
730
|
# true — the message definitely exists in the queue
|
|
728
731
|
# false — the message definitely does not exist
|
|
729
732
|
# nil — could not determine (e.g. queue table missing or unknown error).
|
|
730
733
|
# Callers MUST treat nil as "exists" for safety.
|
|
731
|
-
|
|
732
|
-
|
|
734
|
+
#
|
|
735
|
+
# Under priority routing a logical name probes every `_pN` table, and each
|
|
736
|
+
# table has its own msg_id sequence: a bare +msg_id+ may match a different
|
|
737
|
+
# message in a sibling sub-queue. A caller that knows the sub-queue passes
|
|
738
|
+
# its physical name with +prefixed: false+.
|
|
739
|
+
def message_in_queue?(queue_name, msg_id:, prefixed: true)
|
|
740
|
+
message_exists?(queue_name, msg_id: msg_id, prefixed: prefixed)
|
|
733
741
|
end
|
|
734
742
|
|
|
735
743
|
# Look up a queue row by ActiveJob job_id in the JSON payload. Used by the
|
|
@@ -738,10 +746,10 @@ module Pgbus
|
|
|
738
746
|
#
|
|
739
747
|
# Same tri-state as message_exists?: true / false / nil (unknown). A
|
|
740
748
|
# logical name expands to every physical table the strategy owns (the
|
|
741
|
-
# `_pN` sub-queues under priority routing);
|
|
742
|
-
#
|
|
743
|
-
def message_with_job_id?(queue_name, job_id:)
|
|
744
|
-
tables = lookup_physical_queue_names(queue_name).map { |name| QueueNameValidator.sanitize!(name) }
|
|
749
|
+
# `_pN` sub-queues under priority routing); a physical name passed with
|
|
750
|
+
# +prefixed: false+ probes itself only.
|
|
751
|
+
def message_with_job_id?(queue_name, job_id:, prefixed: true)
|
|
752
|
+
tables = lookup_physical_queue_names(queue_name, prefixed: prefixed).map { |name| QueueNameValidator.sanitize!(name) }
|
|
745
753
|
determined = false
|
|
746
754
|
|
|
747
755
|
synchronized do
|
|
@@ -758,43 +766,85 @@ module Pgbus
|
|
|
758
766
|
determined ? false : nil # rubocop:disable Style/ReturnNilInPredicateMethodDefinition -- tri-state: nil means unknown
|
|
759
767
|
end
|
|
760
768
|
|
|
761
|
-
#
|
|
762
|
-
#
|
|
763
|
-
def
|
|
764
|
-
|
|
769
|
+
# The physical table that holds message +msg_id+, or nil when none does.
|
|
770
|
+
# See locate_messages.
|
|
771
|
+
def locate_message(queue_name, msg_id:, job_id: nil, prefixed: true)
|
|
772
|
+
locate_messages(queue_name, [[msg_id, job_id]], prefixed: prefixed)[msg_id.to_i]
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# { msg_id => physical table } for each [msg_id, job_id] pair whose
|
|
776
|
+
# message is queued, one query per table on one connection. A logical
|
|
777
|
+
# name expands to every table the strategy owns, and under priority
|
|
778
|
+
# routing each `_pN` sub-queue numbers its messages from its own
|
|
779
|
+
# sequence: only a message carrying the pair's job_id matches, or any
|
|
780
|
+
# message with that msg_id when job_id is nil, the first table winning
|
|
781
|
+
# (issue #577). A physical name with +prefixed: false+ is probed alone.
|
|
782
|
+
# A table that does not exist holds nothing; other errors raise.
|
|
783
|
+
def locate_messages(queue_name, messages, prefixed: true)
|
|
784
|
+
wanted = messages.to_h { |msg_id, job_id| [msg_id.to_i, job_id&.to_s] }
|
|
785
|
+
tables = lookup_physical_queue_names(queue_name, prefixed: prefixed)
|
|
786
|
+
located = {}
|
|
787
|
+
|
|
788
|
+
synchronized do
|
|
789
|
+
with_raw_connection do |conn|
|
|
790
|
+
tables.each do |name|
|
|
791
|
+
pending = wanted.keys - located.keys
|
|
792
|
+
break if pending.empty?
|
|
793
|
+
|
|
794
|
+
queued_job_ids(conn, QueueNameValidator.sanitize!(name), pending).each do |row|
|
|
795
|
+
msg_id = row["msg_id"].to_i
|
|
796
|
+
job_id = wanted[msg_id]
|
|
797
|
+
located[msg_id] ||= name if job_id.nil? || row["job_id"] == job_id
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
located
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# DLQ companion of a logical queue name, or of a physical table name passed
|
|
807
|
+
# with +prefixed: false+.
|
|
808
|
+
def dead_letter_physical_name(queue_name, prefixed: true)
|
|
809
|
+
return config.dead_letter_queue_name(queue_name) if prefixed
|
|
810
|
+
|
|
765
811
|
# Priority sub-queues share the logical queue's DLQ (`pgbus_default_dlq`),
|
|
766
|
-
# not a per-priority `…_pN_dlq`. Only
|
|
767
|
-
#
|
|
768
|
-
|
|
812
|
+
# not a per-priority `…_pN_dlq`. Only a physical name can be a sub-queue,
|
|
813
|
+
# and only when the strategy actually creates `_pN` tables: a logical
|
|
814
|
+
# queue named `orders_p0` keeps its suffix.
|
|
815
|
+
name = queue_name.to_s
|
|
816
|
+
base = @queue_strategy.priority? ? name.sub(/_p\d+\z/, "") : name
|
|
769
817
|
"#{base}#{Pgbus::DEAD_LETTER_SUFFIX}"
|
|
770
818
|
end
|
|
771
819
|
|
|
772
820
|
# Same tri-state as message_exists?: true / false / nil (unknown).
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
821
|
+
# A logical name probes the archive of every physical table the strategy
|
|
822
|
+
# owns, like message_exists?; a physical name (prefixed: false) its own.
|
|
823
|
+
# A bare msg_id is unique per table only (see message_in_queue?).
|
|
824
|
+
def message_archived?(queue_name, msg_id:, prefixed: true)
|
|
825
|
+
tables = lookup_physical_queue_names(queue_name, prefixed: prefixed).map { |name| QueueNameValidator.sanitize!(name) }
|
|
826
|
+
determined = false
|
|
776
827
|
|
|
777
828
|
synchronized do
|
|
778
829
|
with_raw_connection do |conn|
|
|
779
|
-
|
|
830
|
+
tables.each do |sanitized|
|
|
831
|
+
archived = probe_archive_presence(conn, sanitized, msg_id.to_i)
|
|
832
|
+
return true if archived == true
|
|
833
|
+
|
|
834
|
+
determined = true unless archived == :missing
|
|
835
|
+
end
|
|
780
836
|
end
|
|
781
837
|
end
|
|
782
|
-
rescue ActiveRecord::StatementInvalid => e
|
|
783
|
-
raise unless undefined_table_error?(e)
|
|
784
|
-
|
|
785
|
-
nil
|
|
786
|
-
rescue StandardError => e
|
|
787
|
-
raise unless defined?(PG::UndefinedTable) && e.is_a?(PG::UndefinedTable)
|
|
788
838
|
|
|
789
|
-
nil
|
|
839
|
+
determined ? false : nil # rubocop:disable Style/ReturnNilInPredicateMethodDefinition -- tri-state: nil means unknown
|
|
790
840
|
end
|
|
791
841
|
|
|
792
|
-
def message_exists?(queue_name, msg_id: nil, uniqueness_key: nil)
|
|
842
|
+
def message_exists?(queue_name, msg_id: nil, uniqueness_key: nil, prefixed: true)
|
|
793
843
|
has_msg_id = !msg_id.nil?
|
|
794
844
|
has_uniqueness_key = !uniqueness_key.nil?
|
|
795
845
|
raise ArgumentError, "pass msg_id, uniqueness_key, or both" unless has_msg_id || has_uniqueness_key
|
|
796
846
|
|
|
797
|
-
tables = lookup_physical_queue_names(queue_name).map { |name| QueueNameValidator.sanitize!(name) }
|
|
847
|
+
tables = lookup_physical_queue_names(queue_name, prefixed: prefixed).map { |name| QueueNameValidator.sanitize!(name) }
|
|
798
848
|
determined = false
|
|
799
849
|
|
|
800
850
|
synchronized do
|
|
@@ -1092,21 +1142,10 @@ module Pgbus
|
|
|
1092
1142
|
end
|
|
1093
1143
|
end
|
|
1094
1144
|
|
|
1095
|
-
# Accept either a logical name ("default") or an already-prefixed
|
|
1096
|
-
# physical name ("pgbus_default") and return the physical name.
|
|
1097
|
-
# Coerces symbols to strings so callers can pass either form.
|
|
1098
|
-
def resolve_full_queue_name(queue_name)
|
|
1099
|
-
name = queue_name.to_s
|
|
1100
|
-
prefix = "#{config.queue_prefix}_"
|
|
1101
|
-
name.start_with?(prefix) ? name : config.queue_name(name)
|
|
1102
|
-
end
|
|
1103
|
-
|
|
1104
1145
|
# Logical names expand through QueueFactory so a priority queue's _pN
|
|
1105
|
-
# tables are included;
|
|
1106
|
-
def lookup_physical_queue_names(queue_name)
|
|
1107
|
-
|
|
1108
|
-
prefix = "#{config.queue_prefix}_"
|
|
1109
|
-
name.start_with?(prefix) ? [name] : @queue_strategy.physical_queue_names(name)
|
|
1146
|
+
# tables are included; a physical name (prefixed: false) stays as is.
|
|
1147
|
+
def lookup_physical_queue_names(queue_name, prefixed:)
|
|
1148
|
+
prefixed ? @queue_strategy.physical_queue_names(queue_name.to_s) : [queue_name.to_s]
|
|
1110
1149
|
end
|
|
1111
1150
|
|
|
1112
1151
|
def probe_queue_presence(conn, sanitized, msg_id:, uniqueness_key:)
|
|
@@ -1125,6 +1164,18 @@ module Pgbus
|
|
|
1125
1164
|
:missing
|
|
1126
1165
|
end
|
|
1127
1166
|
|
|
1167
|
+
def probe_archive_presence(conn, sanitized, msg_id)
|
|
1168
|
+
msg_id_archived?(conn, sanitized, msg_id)
|
|
1169
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
1170
|
+
raise unless undefined_table_error?(e)
|
|
1171
|
+
|
|
1172
|
+
:missing
|
|
1173
|
+
rescue StandardError => e
|
|
1174
|
+
raise unless defined?(PG::UndefinedTable) && e.is_a?(PG::UndefinedTable)
|
|
1175
|
+
|
|
1176
|
+
:missing
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1128
1179
|
def probe_job_id_presence(conn, sanitized, job_id)
|
|
1129
1180
|
job_id_present?(conn, sanitized, job_id)
|
|
1130
1181
|
rescue ActiveRecord::StatementInvalid => e
|
|
@@ -1137,6 +1188,22 @@ module Pgbus
|
|
|
1137
1188
|
:missing
|
|
1138
1189
|
end
|
|
1139
1190
|
|
|
1191
|
+
# msg_ids are integers, so the array literal is safe.
|
|
1192
|
+
def queued_job_ids(conn, sanitized, msg_ids)
|
|
1193
|
+
conn.exec_params(
|
|
1194
|
+
"SELECT msg_id, message::jsonb ->> 'job_id' AS job_id FROM pgmq.q_#{sanitized} WHERE msg_id = ANY($1::bigint[])",
|
|
1195
|
+
["{#{msg_ids.join(",")}}"]
|
|
1196
|
+
)
|
|
1197
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
1198
|
+
raise unless undefined_table_error?(e)
|
|
1199
|
+
|
|
1200
|
+
[]
|
|
1201
|
+
rescue StandardError => e
|
|
1202
|
+
raise unless defined?(PG::UndefinedTable) && e.is_a?(PG::UndefinedTable)
|
|
1203
|
+
|
|
1204
|
+
[]
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1140
1207
|
def scan_uniqueness_keys(conn, sanitized, keys, found)
|
|
1141
1208
|
placeholders = keys.each_index.map { |i| "$#{i + 1}" }.join(", ")
|
|
1142
1209
|
result = conn.exec_params(
|
data/lib/pgbus/version.rb
CHANGED
|
@@ -299,9 +299,9 @@ module Pgbus
|
|
|
299
299
|
# duplicate (the original is still in the queue waiting for retry).
|
|
300
300
|
# Falls back to enqueueing a fresh copy only if the original is gone
|
|
301
301
|
# (e.g., already moved to DLQ).
|
|
302
|
-
|
|
303
|
-
if
|
|
304
|
-
@client.set_visibility_timeout(event["
|
|
302
|
+
physical = failed_message_queue(event)
|
|
303
|
+
if physical
|
|
304
|
+
@client.set_visibility_timeout(physical, event["msg_id"].to_i, vt: 0, prefixed: false)
|
|
305
305
|
else
|
|
306
306
|
payload = JSON.parse(event["payload"])
|
|
307
307
|
headers = event["headers"]
|
|
@@ -1721,12 +1721,29 @@ module Pgbus
|
|
|
1721
1721
|
nil
|
|
1722
1722
|
end
|
|
1723
1723
|
|
|
1724
|
+
# 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).
|
|
1730
|
+
def failed_message_queue(event)
|
|
1731
|
+
return unless event["queue_name"] && event["msg_id"]
|
|
1732
|
+
|
|
1733
|
+
@client.locate_message(event["queue_name"], msg_id: event["msg_id"].to_i, job_id: failed_job_id(event))
|
|
1734
|
+
end
|
|
1735
|
+
|
|
1736
|
+
def failed_job_id(event)
|
|
1737
|
+
payload = event["payload"]
|
|
1738
|
+
payload = JSON.parse(payload) if payload.is_a?(String)
|
|
1739
|
+
payload["job_id"] if payload.is_a?(Hash)
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1724
1742
|
# Archive the queue message a failed_event row points to. Idempotent —
|
|
1725
1743
|
# silently no-ops if the message no longer exists in the queue.
|
|
1726
1744
|
def archive_failed_message(event)
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
@client.archive_message(event["queue_name"], event["msg_id"].to_i)
|
|
1745
|
+
physical = failed_message_queue(event)
|
|
1746
|
+
@client.archive_message(physical, event["msg_id"].to_i, prefixed: false) if physical
|
|
1730
1747
|
rescue StandardError => e
|
|
1731
1748
|
Pgbus.logger.debug do
|
|
1732
1749
|
"[Pgbus::Web] Error archiving message for failed event #{event["id"]}: #{e.message}"
|
|
@@ -1734,30 +1751,30 @@ module Pgbus
|
|
|
1734
1751
|
end
|
|
1735
1752
|
|
|
1736
1753
|
# Archive every queue message referenced by a failed_event row.
|
|
1737
|
-
# Groups by queue
|
|
1738
|
-
#
|
|
1739
|
-
# Falls back to per-row archive inside a rescue if
|
|
1740
|
-
# so one bad queue can't block progress on the others.
|
|
1754
|
+
# Groups by queue, then locates and archive_batches per physical table,
|
|
1755
|
+
# so an N-event discard is two SQL statements per table instead of N
|
|
1756
|
+
# per-row roundtrips. Falls back to per-row archive inside a rescue if
|
|
1757
|
+
# a batch fails, so one bad queue can't block progress on the others.
|
|
1741
1758
|
def archive_all_failed_messages
|
|
1742
1759
|
rows = connection.select_all(
|
|
1743
|
-
"SELECT id, queue_name, msg_id FROM pgbus_failed_events WHERE msg_id IS NOT NULL AND #{NOT_HANDLER_QUEUE}",
|
|
1760
|
+
"SELECT id, queue_name, msg_id, payload FROM pgbus_failed_events WHERE msg_id IS NOT NULL AND #{NOT_HANDLER_QUEUE}",
|
|
1744
1761
|
"Pgbus Collect Failed Messages", [handler_queue_names_literal]
|
|
1745
1762
|
)
|
|
1746
1763
|
|
|
1747
1764
|
grouped = rows.to_a.group_by { |row| row["queue_name"] }
|
|
1748
1765
|
grouped.each do |queue_name, events|
|
|
1749
|
-
msg_ids
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
@client.archive_batch(
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
events.each { |row| archive_failed_message(row) }
|
|
1766
|
+
# A priority queue's msg_ids repeat across its _pN sub-queues, so
|
|
1767
|
+
# 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)] })
|
|
1769
|
+
located.group_by { |_msg_id, physical| physical }.each do |physical, pairs|
|
|
1770
|
+
@client.archive_batch(physical, pairs.map(&:first), prefixed: false)
|
|
1771
|
+
end
|
|
1772
|
+
rescue StandardError => e
|
|
1773
|
+
Pgbus.logger.debug do
|
|
1774
|
+
"[Pgbus::Web] archive_batch failed for #{queue_name} (#{e.message}); " \
|
|
1775
|
+
"falling back to per-row archive"
|
|
1760
1776
|
end
|
|
1777
|
+
events.each { |row| archive_failed_message(row) }
|
|
1761
1778
|
end
|
|
1762
1779
|
rescue StandardError => e
|
|
1763
1780
|
Pgbus.logger.debug { "[Pgbus::Web] Error archiving failed messages: #{e.message}" }
|
data/sig/pgbus/client.rbs
CHANGED
|
@@ -13,7 +13,7 @@ module Pgbus
|
|
|
13
13
|
def convert_archive_partitioned: (untyped queue_name, ?partition_interval: untyped, ?retention_interval: untyped, ?leading_partition: untyped) -> untyped
|
|
14
14
|
def create_fifo_index: (untyped queue_name) -> untyped
|
|
15
15
|
def create_fifo_indexes_all: () -> untyped
|
|
16
|
-
def dead_letter_physical_name: (untyped queue_name) -> untyped
|
|
16
|
+
def dead_letter_physical_name: (untyped queue_name, ?prefixed: untyped) -> untyped
|
|
17
17
|
def delete_batch: (untyped queue_name, untyped msg_ids, ?prefixed: untyped) -> untyped
|
|
18
18
|
def delete_message: (untyped queue_name, untyped msg_id, ?prefixed: untyped) -> untyped
|
|
19
19
|
def drop_queue: (untyped queue_name, ?prefixed: untyped) -> untyped
|
|
@@ -25,10 +25,12 @@ module Pgbus
|
|
|
25
25
|
def in_recovery?: () -> bool
|
|
26
26
|
def list_notify_insert_throttles: () -> untyped
|
|
27
27
|
def list_queues: () -> untyped
|
|
28
|
-
def
|
|
29
|
-
def
|
|
30
|
-
def
|
|
31
|
-
def
|
|
28
|
+
def locate_message: (untyped queue_name, msg_id: untyped, ?job_id: untyped, ?prefixed: untyped) -> String?
|
|
29
|
+
def locate_messages: (untyped queue_name, Array[[untyped, untyped]] messages, ?prefixed: untyped) -> Hash[Integer, String]
|
|
30
|
+
def message_archived?: (untyped queue_name, msg_id: untyped, ?prefixed: untyped) -> bool?
|
|
31
|
+
def message_exists?: (untyped queue_name, ?msg_id: untyped, ?uniqueness_key: untyped, ?prefixed: untyped) -> bool?
|
|
32
|
+
def message_in_queue?: (untyped queue_name, msg_id: untyped, ?prefixed: untyped) -> bool?
|
|
33
|
+
def message_with_job_id?: (untyped queue_name, job_id: untyped, ?prefixed: untyped) -> bool?
|
|
32
34
|
def metrics: (?untyped queue_name) -> untyped
|
|
33
35
|
def move_to_dead_letter: (untyped queue_name, untyped message, ?headers: untyped) -> untyped
|
|
34
36
|
def notify_enabled?: (untyped queue_name) -> bool
|