pgbus 0.2.0 → 0.2.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/lib/pgbus/active_job/executor.rb +1 -1
- data/lib/pgbus/client.rb +60 -32
- data/lib/pgbus/event_bus/publisher.rb +1 -7
- data/lib/pgbus/outbox/poller.rb +58 -20
- data/lib/pgbus/process/consumer.rb +16 -3
- data/lib/pgbus/process/worker.rb +15 -5
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +11 -9
- 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: 35bd03aa1d30dde2aea609829f027d9a1355282fb049e22d4f3a20119039828a
|
|
4
|
+
data.tar.gz: 341150d6777d9747f19b8446692ffd262403eee0b2e687461176cce820af8177
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dafeed2ede684fedb7a95b76ac3504c0987624ba49bdd5b3c9ea18c67a51f241ce626069b3d62f0dff617d9582da83490e8b527e600f396a334785d56010c405
|
|
7
|
+
data.tar.gz: 55a9207ec4e8bbe56ce2e8d7322f0bf242daf6b30f89ac87f4cd37dc4dad2565704503076e24b994a3c52aec31e822abfbdafe72ee11ef239227a703d823f875
|
|
@@ -156,7 +156,7 @@ module Pgbus
|
|
|
156
156
|
|
|
157
157
|
def archive_from(queue_name, msg_id, source_queue: nil)
|
|
158
158
|
if source_queue
|
|
159
|
-
client.
|
|
159
|
+
client.archive_message(source_queue, msg_id, prefixed: false)
|
|
160
160
|
else
|
|
161
161
|
client.archive_message(queue_name, msg_id)
|
|
162
162
|
end
|
data/lib/pgbus/client.rb
CHANGED
|
@@ -19,18 +19,26 @@ module Pgbus
|
|
|
19
19
|
require "pgmq"
|
|
20
20
|
end
|
|
21
21
|
@config = config
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
pool_timeout: config.pool_timeout
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
conn_opts = config.connection_options
|
|
23
|
+
@shared_connection = conn_opts.is_a?(Proc)
|
|
24
|
+
|
|
25
|
+
if @shared_connection
|
|
26
|
+
# When using the Rails lambda path (-> { AR::Base.connection.raw_connection }),
|
|
27
|
+
# the Proc returns the same underlying PG::Connection that ActiveRecord uses.
|
|
28
|
+
# PG::Connection (libpq) is not thread-safe — concurrent access causes
|
|
29
|
+
# segfaults and result corruption. Force pool_size=1 and serialize all
|
|
30
|
+
# operations through a mutex.
|
|
31
|
+
@pgmq = PGMQ::Client.new(conn_opts, pool_size: 1, pool_timeout: config.pool_timeout)
|
|
32
|
+
@pgmq_mutex = Mutex.new
|
|
33
|
+
else
|
|
34
|
+
# With a String URL or Hash params, pgmq-ruby creates its own dedicated
|
|
35
|
+
# PG::Connection per pool slot — no shared state with ActiveRecord.
|
|
36
|
+
# Use the configured pool_size and let pgmq-ruby's connection_pool handle
|
|
37
|
+
# concurrency internally (no mutex needed).
|
|
38
|
+
@pgmq = PGMQ::Client.new(conn_opts, pool_size: config.pool_size, pool_timeout: config.pool_timeout)
|
|
39
|
+
@pgmq_mutex = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
@queues_created = Concurrent::Map.new
|
|
35
43
|
@queue_strategy = QueueFactory.for(config)
|
|
36
44
|
@schema_ensured = false
|
|
@@ -69,7 +77,7 @@ module Pgbus
|
|
|
69
77
|
full_name = config.queue_name(queue_name)
|
|
70
78
|
ensure_queue(queue_name)
|
|
71
79
|
serialized = payloads.map { |p| serialize(p) }
|
|
72
|
-
serialized_headers = headers&.map { |h| serialize(h) }
|
|
80
|
+
serialized_headers = headers&.map { |h| h.nil? ? nil : serialize(h) }
|
|
73
81
|
Instrumentation.instrument("pgbus.client.send_batch", queue: full_name, size: payloads.size) do
|
|
74
82
|
synchronized { @pgmq.produce_batch(full_name, serialized, headers: serialized_headers, delay: delay) }
|
|
75
83
|
end
|
|
@@ -128,31 +136,47 @@ module Pgbus
|
|
|
128
136
|
end
|
|
129
137
|
end
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
# Read from multiple queues in a single SQL query (UNION ALL).
|
|
140
|
+
# Each returned message includes a queue_name field identifying its source.
|
|
141
|
+
# queue_names should be logical names (prefix is added automatically).
|
|
142
|
+
def read_multi(queue_names, qty:, vt: nil)
|
|
143
|
+
full_names = queue_names.map { |q| config.queue_name(q) }
|
|
144
|
+
Instrumentation.instrument("pgbus.client.read_multi", queues: full_names, qty: qty) do
|
|
145
|
+
synchronized { @pgmq.read_multi(full_names, vt: vt || config.visibility_timeout, qty: qty) }
|
|
146
|
+
end
|
|
134
147
|
end
|
|
135
148
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
149
|
+
# Delete a message. Pass prefixed: false when queue_name is already
|
|
150
|
+
# the full PGMQ queue name (e.g. from priority sub-queues or dashboard).
|
|
151
|
+
def delete_message(queue_name, msg_id, prefixed: true)
|
|
152
|
+
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
153
|
+
synchronized { @pgmq.delete(name, msg_id) }
|
|
139
154
|
end
|
|
140
155
|
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
# Archive a message. Pass prefixed: false when queue_name is already
|
|
157
|
+
# the full PGMQ queue name.
|
|
158
|
+
def archive_message(queue_name, msg_id, prefixed: true)
|
|
159
|
+
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
160
|
+
synchronized { @pgmq.archive(name, msg_id) }
|
|
143
161
|
end
|
|
144
162
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
163
|
+
# Batch archive — moves multiple messages to the archive table in one call.
|
|
164
|
+
def archive_batch(queue_name, msg_ids, prefixed: true)
|
|
165
|
+
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
166
|
+
synchronized { @pgmq.archive_batch(name, msg_ids) }
|
|
148
167
|
end
|
|
149
168
|
|
|
150
|
-
|
|
151
|
-
|
|
169
|
+
# Batch delete — permanently removes multiple messages in one call.
|
|
170
|
+
def delete_batch(queue_name, msg_ids, prefixed: true)
|
|
171
|
+
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
172
|
+
synchronized { @pgmq.delete_batch(name, msg_ids) }
|
|
152
173
|
end
|
|
153
174
|
|
|
154
|
-
|
|
155
|
-
|
|
175
|
+
# Set visibility timeout. Pass prefixed: false when queue_name is already
|
|
176
|
+
# the full PGMQ queue name.
|
|
177
|
+
def set_visibility_timeout(queue_name, msg_id, vt:, prefixed: true)
|
|
178
|
+
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
179
|
+
synchronized { @pgmq.set_vt(name, msg_id, vt: vt) }
|
|
156
180
|
end
|
|
157
181
|
|
|
158
182
|
def transaction(&block)
|
|
@@ -327,11 +351,15 @@ module Pgbus
|
|
|
327
351
|
end
|
|
328
352
|
end
|
|
329
353
|
|
|
330
|
-
# Serialize
|
|
331
|
-
#
|
|
332
|
-
#
|
|
354
|
+
# Serialize PGMQ operations through a mutex when sharing a connection
|
|
355
|
+
# with ActiveRecord (Proc path). When pgmq-ruby owns its own connections
|
|
356
|
+
# (String/Hash path), the internal connection_pool handles concurrency.
|
|
333
357
|
def synchronized(&)
|
|
334
|
-
@pgmq_mutex
|
|
358
|
+
if @pgmq_mutex
|
|
359
|
+
@pgmq_mutex.synchronize(&)
|
|
360
|
+
else
|
|
361
|
+
yield
|
|
362
|
+
end
|
|
335
363
|
end
|
|
336
364
|
|
|
337
365
|
def serialize(data)
|
|
@@ -7,13 +7,7 @@ module Pgbus
|
|
|
7
7
|
|
|
8
8
|
def publish(routing_key, payload, headers: nil, delay: 0)
|
|
9
9
|
event_data = build_event_data(payload)
|
|
10
|
-
|
|
11
|
-
if Pgbus.client.pgmq.respond_to?(:produce_topic)
|
|
12
|
-
Pgbus.client.publish_to_topic(routing_key, event_data, headers: headers, delay: delay)
|
|
13
|
-
else
|
|
14
|
-
# Fallback: send directly to queues matching the routing key
|
|
15
|
-
Pgbus.client.send_message(routing_key, event_data, headers: headers, delay: delay)
|
|
16
|
-
end
|
|
10
|
+
Pgbus.client.publish_to_topic(routing_key, event_data, headers: headers, delay: delay)
|
|
17
11
|
end
|
|
18
12
|
|
|
19
13
|
def publish_later(routing_key, payload, delay:, headers: nil)
|
data/lib/pgbus/outbox/poller.rb
CHANGED
|
@@ -54,10 +54,7 @@ module Pgbus
|
|
|
54
54
|
.to_a
|
|
55
55
|
break if entries.empty?
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
succeeded += 1 if publish_entry(entry)
|
|
59
|
-
end
|
|
60
|
-
|
|
57
|
+
succeeded = publish_entries(entries)
|
|
61
58
|
published += succeeded
|
|
62
59
|
break if succeeded.zero? || entries.size < config.outbox_batch_size
|
|
63
60
|
end
|
|
@@ -74,24 +71,65 @@ module Pgbus
|
|
|
74
71
|
|
|
75
72
|
private
|
|
76
73
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
74
|
+
def publish_entries(entries)
|
|
75
|
+
# Partition: topic-routed entries must be published individually
|
|
76
|
+
# (different routing keys), direct-queue entries can be batched.
|
|
77
|
+
topic_entries, queue_entries = entries.partition { |e| e.routing_key.present? }
|
|
78
|
+
|
|
79
|
+
succeeded = 0
|
|
80
|
+
topic_entries.each { |e| succeeded += 1 if publish_single(e) }
|
|
81
|
+
succeeded += publish_queue_batch(queue_entries)
|
|
82
|
+
succeeded
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def publish_single(entry)
|
|
86
|
+
Pgbus.client.publish_to_topic(
|
|
87
|
+
entry.routing_key,
|
|
88
|
+
entry.payload,
|
|
89
|
+
headers: entry.headers,
|
|
90
|
+
delay: entry.delay || 0
|
|
91
|
+
)
|
|
92
|
+
entry.update!(published_at: Time.current)
|
|
93
|
+
true
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
Pgbus.logger.error { "[Pgbus] Failed to publish outbox entry #{entry.id}: #{e.message}" }
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Group direct-queue entries by (queue_name, priority, delay) and
|
|
100
|
+
# use send_batch for each group to reduce round-trips.
|
|
101
|
+
def publish_queue_batch(entries)
|
|
102
|
+
return 0 if entries.empty?
|
|
103
|
+
|
|
104
|
+
succeeded = 0
|
|
105
|
+
entries.group_by { |e| [e.queue_name, e.priority, e.delay || 0] }.each do |(queue, _priority, delay), group|
|
|
106
|
+
payloads = group.map(&:payload)
|
|
107
|
+
headers = group.map(&:headers)
|
|
108
|
+
headers = nil if headers.all?(&:blank?)
|
|
109
|
+
|
|
110
|
+
Pgbus.client.send_batch(queue, payloads, headers: headers, delay: delay)
|
|
111
|
+
now = Time.current
|
|
112
|
+
group.each { |e| e.update!(published_at: now) }
|
|
113
|
+
succeeded += group.size
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
Pgbus.logger.error { "[Pgbus] Failed to batch-publish #{group.size} outbox entries: #{e.message}" }
|
|
116
|
+
# Fall back to individual publishing for this group
|
|
117
|
+
group.each { |entry| succeeded += 1 if publish_single_queue(entry) }
|
|
93
118
|
end
|
|
119
|
+
succeeded
|
|
120
|
+
end
|
|
94
121
|
|
|
122
|
+
# Fallback for individual publishing when a batch fails.
|
|
123
|
+
# Intentionally omits priority to match send_batch routing behavior
|
|
124
|
+
# (base queue only), ensuring consistent queue placement regardless
|
|
125
|
+
# of whether the batch or fallback path is used.
|
|
126
|
+
def publish_single_queue(entry)
|
|
127
|
+
Pgbus.client.send_message(
|
|
128
|
+
entry.queue_name,
|
|
129
|
+
entry.payload,
|
|
130
|
+
headers: entry.headers,
|
|
131
|
+
delay: entry.delay || 0
|
|
132
|
+
)
|
|
95
133
|
entry.update!(published_at: Time.current)
|
|
96
134
|
true
|
|
97
135
|
rescue StandardError => e
|
|
@@ -56,9 +56,12 @@ module Pgbus
|
|
|
56
56
|
idle = @pool.max_length - @pool.queue_length
|
|
57
57
|
return interruptible_sleep(config.polling_interval) if idle <= 0
|
|
58
58
|
|
|
59
|
-
tagged_messages = @queue_names.
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
tagged_messages = if @queue_names.size == 1
|
|
60
|
+
queue = @queue_names.first
|
|
61
|
+
(Pgbus.client.read_batch(queue, qty: idle) || []).map { |m| [queue, m] }
|
|
62
|
+
else
|
|
63
|
+
fetch_multi_consumer(idle)
|
|
64
|
+
end
|
|
62
65
|
|
|
63
66
|
if tagged_messages.empty?
|
|
64
67
|
interruptible_sleep(config.polling_interval)
|
|
@@ -94,6 +97,16 @@ module Pgbus
|
|
|
94
97
|
# the next read will route to DLQ above.
|
|
95
98
|
end
|
|
96
99
|
|
|
100
|
+
def fetch_multi_consumer(qty)
|
|
101
|
+
messages = Pgbus.client.read_multi(@queue_names, qty: qty) || []
|
|
102
|
+
prefix = "#{config.queue_prefix}_"
|
|
103
|
+
|
|
104
|
+
messages.map do |m|
|
|
105
|
+
logical = m.queue_name&.delete_prefix(prefix) || @queue_names.first
|
|
106
|
+
[logical, m]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
97
110
|
def pattern_overlaps?(topic_filter, subscription_pattern)
|
|
98
111
|
# Simple check: if either is a subset of the other
|
|
99
112
|
topic_filter == subscription_pattern ||
|
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -107,7 +107,7 @@ module Pgbus
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Returns an array of [queue_name, message] pairs so we always know
|
|
110
|
-
# which queue each message came from
|
|
110
|
+
# which queue each message came from.
|
|
111
111
|
def fetch_messages(qty)
|
|
112
112
|
active_queues = queues.reject { |q| @circuit_breaker.paused?(q) }
|
|
113
113
|
active_queues = active_queues.select { |q| @queue_lock.try_lock(q) } if @single_active_consumer
|
|
@@ -120,10 +120,7 @@ module Pgbus
|
|
|
120
120
|
messages = Pgbus.client.read_batch(queue, qty: qty) || []
|
|
121
121
|
messages.map { |m| [queue, m] }
|
|
122
122
|
else
|
|
123
|
-
|
|
124
|
-
active_queues.flat_map do |q|
|
|
125
|
-
(Pgbus.client.read_batch(q, qty: per_queue) || []).map { |m| [q, m] }
|
|
126
|
-
end.first(qty)
|
|
123
|
+
fetch_multi(active_queues, qty)
|
|
127
124
|
end
|
|
128
125
|
rescue StandardError => e
|
|
129
126
|
Pgbus.logger.error { "[Pgbus] Error fetching messages: #{e.message}" }
|
|
@@ -147,6 +144,19 @@ module Pgbus
|
|
|
147
144
|
results
|
|
148
145
|
end
|
|
149
146
|
|
|
147
|
+
# Use pgmq-ruby's read_multi to read from all queues in a single
|
|
148
|
+
# SQL query (UNION ALL). Each returned message carries a queue_name
|
|
149
|
+
# field so we can map it back to the logical queue.
|
|
150
|
+
def fetch_multi(active_queues, qty)
|
|
151
|
+
messages = Pgbus.client.read_multi(active_queues, qty: qty) || []
|
|
152
|
+
prefix = "#{config.queue_prefix}_"
|
|
153
|
+
|
|
154
|
+
messages.map do |m|
|
|
155
|
+
logical = m.queue_name&.delete_prefix(prefix) || active_queues.first
|
|
156
|
+
[logical, m]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
150
160
|
def priority_enabled?
|
|
151
161
|
config.priority_levels && config.priority_levels > 1
|
|
152
162
|
end
|
data/lib/pgbus/version.rb
CHANGED
|
@@ -105,11 +105,11 @@ module Pgbus
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def retry_job(queue_name, msg_id)
|
|
108
|
-
@client.set_visibility_timeout(queue_name, msg_id.to_i, vt: 0)
|
|
108
|
+
@client.set_visibility_timeout(queue_name, msg_id.to_i, vt: 0, prefixed: false)
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
def discard_job(queue_name, msg_id)
|
|
112
|
-
@client.archive_message(queue_name, msg_id.to_i)
|
|
112
|
+
@client.archive_message(queue_name, msg_id.to_i, prefixed: false)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
# Failed events
|
|
@@ -269,7 +269,7 @@ module Pgbus
|
|
|
269
269
|
|
|
270
270
|
def discard_dlq_message(queue_name, msg_id)
|
|
271
271
|
# queue_name here is the full DLQ name (already prefixed)
|
|
272
|
-
@client.
|
|
272
|
+
@client.delete_message(queue_name, msg_id.to_i, prefixed: false)
|
|
273
273
|
true
|
|
274
274
|
rescue StandardError => e
|
|
275
275
|
Pgbus.logger.debug { "[Pgbus::Web] Error discarding DLQ message #{msg_id}: #{e.message}" }
|
|
@@ -290,14 +290,16 @@ module Pgbus
|
|
|
290
290
|
|
|
291
291
|
def discard_all_dlq
|
|
292
292
|
messages = dlq_messages(page: 1, per_page: 1000)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
return 0 if messages.empty?
|
|
294
|
+
|
|
295
|
+
# Group by queue for batch delete — one call per DLQ instead of N calls
|
|
296
|
+
messages.group_by { |m| m[:queue_name] }.sum do |queue_name, msgs|
|
|
297
|
+
ids = msgs.map { |m| m[:msg_id].to_i }
|
|
298
|
+
@client.delete_batch(queue_name, ids, prefixed: false).size
|
|
296
299
|
rescue StandardError => e
|
|
297
|
-
Pgbus.logger.debug { "[Pgbus::Web] Error discarding DLQ
|
|
298
|
-
|
|
300
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error batch-discarding DLQ messages from #{queue_name}: #{e.message}" }
|
|
301
|
+
0
|
|
299
302
|
end
|
|
300
|
-
count
|
|
301
303
|
end
|
|
302
304
|
|
|
303
305
|
# Processes
|