pgmq-ruby 0.3.0 → 0.5.0
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/.github/workflows/ci.yml +48 -26
- data/.github/workflows/push.yml +2 -2
- data/.rspec +1 -0
- data/.rubocop.yml +66 -0
- data/.ruby-version +1 -1
- data/.yard-lint.yml +172 -67
- data/CHANGELOG.md +57 -0
- data/CLAUDE.md +310 -0
- data/Gemfile +5 -5
- data/Gemfile.lint +16 -0
- data/Gemfile.lint.lock +120 -0
- data/Gemfile.lock +20 -6
- data/README.md +300 -37
- data/Rakefile +71 -2
- data/docker-compose.yml +2 -2
- data/lib/pgmq/client/consumer.rb +80 -7
- data/lib/pgmq/client/maintenance.rb +37 -6
- data/lib/pgmq/client/message_lifecycle.rb +142 -25
- data/lib/pgmq/client/metrics.rb +2 -2
- data/lib/pgmq/client/multi_queue.rb +9 -9
- data/lib/pgmq/client/producer.rb +72 -27
- data/lib/pgmq/client/queue_management.rb +36 -20
- data/lib/pgmq/client/topics.rb +268 -0
- data/lib/pgmq/client.rb +15 -14
- data/lib/pgmq/connection.rb +11 -11
- data/lib/pgmq/message.rb +11 -9
- data/lib/pgmq/metrics.rb +7 -7
- data/lib/pgmq/queue_metadata.rb +7 -7
- data/lib/pgmq/transaction.rb +4 -17
- data/lib/pgmq/version.rb +1 -1
- data/lib/pgmq.rb +4 -4
- data/package-lock.json +331 -0
- data/package.json +9 -0
- data/pgmq-ruby.gemspec +20 -20
- data/renovate.json +15 -3
- metadata +9 -3
- data/.coditsu/ci.yml +0 -3
data/lib/pgmq/client/consumer.rb
CHANGED
|
@@ -33,12 +33,12 @@ module PGMQ
|
|
|
33
33
|
result = with_connection do |conn|
|
|
34
34
|
if conditional.empty?
|
|
35
35
|
conn.exec_params(
|
|
36
|
-
|
|
36
|
+
"SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer)",
|
|
37
37
|
[queue_name, vt, 1]
|
|
38
38
|
)
|
|
39
39
|
else
|
|
40
40
|
conn.exec_params(
|
|
41
|
-
|
|
41
|
+
"SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer, $4::jsonb)",
|
|
42
42
|
[queue_name, vt, 1, conditional.to_json]
|
|
43
43
|
)
|
|
44
44
|
end
|
|
@@ -82,12 +82,12 @@ module PGMQ
|
|
|
82
82
|
result = with_connection do |conn|
|
|
83
83
|
if conditional.empty?
|
|
84
84
|
conn.exec_params(
|
|
85
|
-
|
|
85
|
+
"SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer)",
|
|
86
86
|
[queue_name, vt, qty]
|
|
87
87
|
)
|
|
88
88
|
else
|
|
89
89
|
conn.exec_params(
|
|
90
|
-
|
|
90
|
+
"SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer, $4::jsonb)",
|
|
91
91
|
[queue_name, vt, qty, conditional.to_json]
|
|
92
92
|
)
|
|
93
93
|
end
|
|
@@ -135,12 +135,12 @@ module PGMQ
|
|
|
135
135
|
result = with_connection do |conn|
|
|
136
136
|
if conditional.empty?
|
|
137
137
|
conn.exec_params(
|
|
138
|
-
|
|
138
|
+
"SELECT * FROM pgmq.read_with_poll($1::text, $2::integer, $3::integer, $4::integer, $5::integer)",
|
|
139
139
|
[queue_name, vt, qty, max_poll_seconds, poll_interval_ms]
|
|
140
140
|
)
|
|
141
141
|
else
|
|
142
|
-
sql =
|
|
143
|
-
|
|
142
|
+
sql = "SELECT * FROM pgmq.read_with_poll($1::text, $2::integer, $3::integer, " \
|
|
143
|
+
"$4::integer, $5::integer, $6::jsonb)"
|
|
144
144
|
conn.exec_params(
|
|
145
145
|
sql,
|
|
146
146
|
[queue_name, vt, qty, max_poll_seconds, poll_interval_ms, conditional.to_json]
|
|
@@ -150,6 +150,79 @@ module PGMQ
|
|
|
150
150
|
|
|
151
151
|
result.map { |row| Message.new(row) }
|
|
152
152
|
end
|
|
153
|
+
|
|
154
|
+
# Reads messages using grouped round-robin ordering
|
|
155
|
+
#
|
|
156
|
+
# Messages are grouped by the first key in their JSON payload and returned
|
|
157
|
+
# in round-robin order across groups. This ensures fair processing when
|
|
158
|
+
# messages from different entities (users, orders, etc.) are in the queue.
|
|
159
|
+
#
|
|
160
|
+
# @param queue_name [String] name of the queue
|
|
161
|
+
# @param vt [Integer] visibility timeout in seconds
|
|
162
|
+
# @param qty [Integer] number of messages to read
|
|
163
|
+
# @return [Array<PGMQ::Message>] array of messages in round-robin order
|
|
164
|
+
#
|
|
165
|
+
# @example Fair processing across users
|
|
166
|
+
# # Queue contains: user1_msg1, user1_msg2, user2_msg1, user3_msg1
|
|
167
|
+
# messages = client.read_grouped_rr("tasks", vt: 30, qty: 4)
|
|
168
|
+
# # Returns in round-robin: user1_msg1, user2_msg1, user3_msg1, user1_msg2
|
|
169
|
+
#
|
|
170
|
+
# @example Prevent single entity from monopolizing worker
|
|
171
|
+
# loop do
|
|
172
|
+
# messages = client.read_grouped_rr("orders", vt: 30, qty: 10)
|
|
173
|
+
# break if messages.empty?
|
|
174
|
+
# messages.each { |msg| process(msg) }
|
|
175
|
+
# end
|
|
176
|
+
def read_grouped_rr(queue_name, vt: DEFAULT_VT, qty: 1)
|
|
177
|
+
validate_queue_name!(queue_name)
|
|
178
|
+
|
|
179
|
+
result = with_connection do |conn|
|
|
180
|
+
conn.exec_params(
|
|
181
|
+
"SELECT * FROM pgmq.read_grouped_rr($1::text, $2::integer, $3::integer)",
|
|
182
|
+
[queue_name, vt, qty]
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
result.map { |row| Message.new(row) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Reads messages using grouped round-robin with long-polling support
|
|
190
|
+
#
|
|
191
|
+
# Combines grouped round-robin ordering with long-polling for efficient
|
|
192
|
+
# and fair message consumption.
|
|
193
|
+
#
|
|
194
|
+
# @param queue_name [String] name of the queue
|
|
195
|
+
# @param vt [Integer] visibility timeout in seconds
|
|
196
|
+
# @param qty [Integer] number of messages to read
|
|
197
|
+
# @param max_poll_seconds [Integer] maximum time to poll in seconds
|
|
198
|
+
# @param poll_interval_ms [Integer] interval between polls in milliseconds
|
|
199
|
+
# @return [Array<PGMQ::Message>] array of messages in round-robin order
|
|
200
|
+
#
|
|
201
|
+
# @example Long-polling with fair ordering
|
|
202
|
+
# messages = client.read_grouped_rr_with_poll("tasks",
|
|
203
|
+
# vt: 30,
|
|
204
|
+
# qty: 10,
|
|
205
|
+
# max_poll_seconds: 5,
|
|
206
|
+
# poll_interval_ms: 100
|
|
207
|
+
# )
|
|
208
|
+
def read_grouped_rr_with_poll(
|
|
209
|
+
queue_name,
|
|
210
|
+
vt: DEFAULT_VT,
|
|
211
|
+
qty: 1,
|
|
212
|
+
max_poll_seconds: 5,
|
|
213
|
+
poll_interval_ms: 100
|
|
214
|
+
)
|
|
215
|
+
validate_queue_name!(queue_name)
|
|
216
|
+
|
|
217
|
+
result = with_connection do |conn|
|
|
218
|
+
conn.exec_params(
|
|
219
|
+
"SELECT * FROM pgmq.read_grouped_rr_with_poll($1::text, $2::integer, $3::integer, $4::integer, $5::integer)",
|
|
220
|
+
[queue_name, vt, qty, max_poll_seconds, poll_interval_ms]
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
result.map { |row| Message.new(row) }
|
|
225
|
+
end
|
|
153
226
|
end
|
|
154
227
|
end
|
|
155
228
|
end
|
|
@@ -19,24 +19,55 @@ module PGMQ
|
|
|
19
19
|
validate_queue_name!(queue_name)
|
|
20
20
|
|
|
21
21
|
result = with_connection do |conn|
|
|
22
|
-
conn.exec_params(
|
|
22
|
+
conn.exec_params("SELECT pgmq.purge_queue($1::text)", [queue_name])
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
result[0][
|
|
25
|
+
result[0]["purge_queue"]
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
#
|
|
28
|
+
# Enables PostgreSQL NOTIFY when messages are inserted into a queue
|
|
29
|
+
#
|
|
30
|
+
# When enabled, PostgreSQL will send a NOTIFY event on message insert,
|
|
31
|
+
# allowing clients to use LISTEN instead of polling. The throttle interval
|
|
32
|
+
# prevents notification storms during high-volume inserts.
|
|
33
|
+
#
|
|
34
|
+
# @param queue_name [String] name of the queue
|
|
35
|
+
# @param throttle_interval_ms [Integer] minimum ms between notifications (default: 250)
|
|
36
|
+
# @return [void]
|
|
37
|
+
#
|
|
38
|
+
# @example Enable with default throttle (250ms)
|
|
39
|
+
# client.enable_notify_insert("orders")
|
|
40
|
+
#
|
|
41
|
+
# @example Enable with custom throttle (1 second)
|
|
42
|
+
# client.enable_notify_insert("orders", throttle_interval_ms: 1000)
|
|
43
|
+
#
|
|
44
|
+
# @example Disable throttling (notify on every insert)
|
|
45
|
+
# client.enable_notify_insert("orders", throttle_interval_ms: 0)
|
|
46
|
+
def enable_notify_insert(queue_name, throttle_interval_ms: 250)
|
|
47
|
+
validate_queue_name!(queue_name)
|
|
48
|
+
|
|
49
|
+
with_connection do |conn|
|
|
50
|
+
conn.exec_params(
|
|
51
|
+
"SELECT pgmq.enable_notify_insert($1::text, $2::integer)",
|
|
52
|
+
[queue_name, throttle_interval_ms]
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Disables PostgreSQL NOTIFY for a queue
|
|
29
60
|
#
|
|
30
61
|
# @param queue_name [String] name of the queue
|
|
31
62
|
# @return [void]
|
|
32
63
|
#
|
|
33
64
|
# @example
|
|
34
|
-
# client.
|
|
35
|
-
def
|
|
65
|
+
# client.disable_notify_insert("orders")
|
|
66
|
+
def disable_notify_insert(queue_name)
|
|
36
67
|
validate_queue_name!(queue_name)
|
|
37
68
|
|
|
38
69
|
with_connection do |conn|
|
|
39
|
-
conn.exec_params(
|
|
70
|
+
conn.exec_params("SELECT pgmq.disable_notify_insert($1::text)", [queue_name])
|
|
40
71
|
end
|
|
41
72
|
|
|
42
73
|
nil
|
|
@@ -19,7 +19,7 @@ module PGMQ
|
|
|
19
19
|
validate_queue_name!(queue_name)
|
|
20
20
|
|
|
21
21
|
result = with_connection do |conn|
|
|
22
|
-
conn.exec_params(
|
|
22
|
+
conn.exec_params("SELECT * FROM pgmq.pop($1::text)", [queue_name])
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
return nil if result.ntuples.zero?
|
|
@@ -27,6 +27,26 @@ module PGMQ
|
|
|
27
27
|
Message.new(result[0])
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Pops multiple messages atomically (atomic read + delete for batch)
|
|
31
|
+
#
|
|
32
|
+
# @param queue_name [String] name of the queue
|
|
33
|
+
# @param qty [Integer] maximum number of messages to pop
|
|
34
|
+
# @return [Array<PGMQ::Message>] array of message objects (empty if queue is empty)
|
|
35
|
+
#
|
|
36
|
+
# @example Pop up to 10 messages
|
|
37
|
+
# messages = client.pop_batch("orders", 10)
|
|
38
|
+
# messages.each { |msg| process(msg.payload) }
|
|
39
|
+
def pop_batch(queue_name, qty)
|
|
40
|
+
validate_queue_name!(queue_name)
|
|
41
|
+
return [] if qty <= 0
|
|
42
|
+
|
|
43
|
+
result = with_connection do |conn|
|
|
44
|
+
conn.exec_params("SELECT * FROM pgmq.pop($1::text, $2::integer)", [queue_name, qty])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
result.map { |row| Message.new(row) }
|
|
48
|
+
end
|
|
49
|
+
|
|
30
50
|
# Deletes a message from the queue
|
|
31
51
|
#
|
|
32
52
|
# @param queue_name [String] name of the queue
|
|
@@ -43,14 +63,14 @@ module PGMQ
|
|
|
43
63
|
|
|
44
64
|
result = with_connection do |conn|
|
|
45
65
|
conn.exec_params(
|
|
46
|
-
|
|
66
|
+
"SELECT pgmq.delete($1::text, $2::bigint)",
|
|
47
67
|
[queue_name, msg_id]
|
|
48
68
|
)
|
|
49
69
|
end
|
|
50
70
|
|
|
51
71
|
return false if result.ntuples.zero?
|
|
52
72
|
|
|
53
|
-
result[0][
|
|
73
|
+
result[0]["delete"] == "t"
|
|
54
74
|
end
|
|
55
75
|
|
|
56
76
|
# Deletes multiple messages from the queue
|
|
@@ -74,12 +94,12 @@ module PGMQ
|
|
|
74
94
|
encoded_array = encoder.encode(msg_ids)
|
|
75
95
|
|
|
76
96
|
conn.exec_params(
|
|
77
|
-
|
|
97
|
+
"SELECT * FROM pgmq.delete($1::text, $2::bigint[])",
|
|
78
98
|
[queue_name, encoded_array]
|
|
79
99
|
)
|
|
80
100
|
end
|
|
81
101
|
|
|
82
|
-
result.map { |row| row[
|
|
102
|
+
result.map { |row| row["delete"] }
|
|
83
103
|
end
|
|
84
104
|
|
|
85
105
|
# Deletes specific messages from multiple queues in a single transaction
|
|
@@ -103,7 +123,7 @@ module PGMQ
|
|
|
103
123
|
# deletions = messages.group_by(&:queue_name).transform_values { |mss| mss.map(&:msg_id) }
|
|
104
124
|
# client.delete_multi(deletions)
|
|
105
125
|
def delete_multi(deletions)
|
|
106
|
-
raise ArgumentError,
|
|
126
|
+
raise ArgumentError, "deletions must be a hash" unless deletions.is_a?(Hash)
|
|
107
127
|
return {} if deletions.empty?
|
|
108
128
|
|
|
109
129
|
# Validate all queue names
|
|
@@ -137,14 +157,14 @@ module PGMQ
|
|
|
137
157
|
|
|
138
158
|
result = with_connection do |conn|
|
|
139
159
|
conn.exec_params(
|
|
140
|
-
|
|
160
|
+
"SELECT pgmq.archive($1::text, $2::bigint)",
|
|
141
161
|
[queue_name, msg_id]
|
|
142
162
|
)
|
|
143
163
|
end
|
|
144
164
|
|
|
145
165
|
return false if result.ntuples.zero?
|
|
146
166
|
|
|
147
|
-
result[0][
|
|
167
|
+
result[0]["archive"] == "t"
|
|
148
168
|
end
|
|
149
169
|
|
|
150
170
|
# Archives multiple messages
|
|
@@ -168,12 +188,12 @@ module PGMQ
|
|
|
168
188
|
encoded_array = encoder.encode(msg_ids)
|
|
169
189
|
|
|
170
190
|
conn.exec_params(
|
|
171
|
-
|
|
191
|
+
"SELECT * FROM pgmq.archive($1::text, $2::bigint[])",
|
|
172
192
|
[queue_name, encoded_array]
|
|
173
193
|
)
|
|
174
194
|
end
|
|
175
195
|
|
|
176
|
-
result.map { |row| row[
|
|
196
|
+
result.map { |row| row["archive"] }
|
|
177
197
|
end
|
|
178
198
|
|
|
179
199
|
# Archives specific messages from multiple queues in a single transaction
|
|
@@ -189,7 +209,7 @@ module PGMQ
|
|
|
189
209
|
# 'notifications' => [5]
|
|
190
210
|
# })
|
|
191
211
|
def archive_multi(archives)
|
|
192
|
-
raise ArgumentError,
|
|
212
|
+
raise ArgumentError, "archives must be a hash" unless archives.is_a?(Hash)
|
|
193
213
|
return {} if archives.empty?
|
|
194
214
|
|
|
195
215
|
# Validate all queue names
|
|
@@ -209,32 +229,129 @@ module PGMQ
|
|
|
209
229
|
|
|
210
230
|
# Updates the visibility timeout for a message
|
|
211
231
|
#
|
|
232
|
+
# Supports two modes:
|
|
233
|
+
# - Integer offset (seconds from now): `vt: 60` - message visible in 60 seconds
|
|
234
|
+
# - Absolute timestamp: `vt: Time.now + 300` - message visible at specific time
|
|
235
|
+
#
|
|
212
236
|
# @param queue_name [String] name of the queue
|
|
213
237
|
# @param msg_id [Integer] message ID
|
|
214
|
-
# @param
|
|
215
|
-
# @return [PGMQ::Message] updated message
|
|
238
|
+
# @param vt [Integer, Time] visibility timeout as seconds offset or absolute timestamp
|
|
239
|
+
# @return [PGMQ::Message, nil] updated message or nil if not found
|
|
216
240
|
#
|
|
217
|
-
# @example
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
vt_offset:
|
|
224
|
-
)
|
|
241
|
+
# @example Extend processing time by 60 more seconds (offset)
|
|
242
|
+
# msg = client.set_vt("orders", 123, vt: 60)
|
|
243
|
+
#
|
|
244
|
+
# @example Set absolute visibility time (timestamp)
|
|
245
|
+
# msg = client.set_vt("orders", 123, vt: Time.now + 300)
|
|
246
|
+
def set_vt(queue_name, msg_id, vt:)
|
|
225
247
|
validate_queue_name!(queue_name)
|
|
226
248
|
|
|
227
249
|
result = with_connection do |conn|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
250
|
+
if vt.is_a?(Time)
|
|
251
|
+
conn.exec_params(
|
|
252
|
+
"SELECT * FROM pgmq.set_vt($1::text, $2::bigint, $3::timestamptz)",
|
|
253
|
+
[queue_name, msg_id, vt.utc.iso8601(6)]
|
|
254
|
+
)
|
|
255
|
+
else
|
|
256
|
+
conn.exec_params(
|
|
257
|
+
"SELECT * FROM pgmq.set_vt($1::text, $2::bigint, $3::integer)",
|
|
258
|
+
[queue_name, msg_id, vt]
|
|
259
|
+
)
|
|
260
|
+
end
|
|
232
261
|
end
|
|
233
262
|
|
|
234
263
|
return nil if result.ntuples.zero?
|
|
235
264
|
|
|
236
265
|
Message.new(result[0])
|
|
237
266
|
end
|
|
267
|
+
|
|
268
|
+
# Updates visibility timeout for multiple messages
|
|
269
|
+
#
|
|
270
|
+
# Supports two modes:
|
|
271
|
+
# - Integer offset (seconds from now): `vt: 60` - messages visible in 60 seconds
|
|
272
|
+
# - Absolute timestamp: `vt: Time.now + 300` - messages visible at specific time
|
|
273
|
+
#
|
|
274
|
+
# @param queue_name [String] name of the queue
|
|
275
|
+
# @param msg_ids [Array<Integer>] array of message IDs
|
|
276
|
+
# @param vt [Integer, Time] visibility timeout as seconds offset or absolute timestamp
|
|
277
|
+
# @return [Array<PGMQ::Message>] array of updated messages
|
|
278
|
+
#
|
|
279
|
+
# @example Extend processing time for multiple messages (offset)
|
|
280
|
+
# messages = client.set_vt_batch("orders", [101, 102, 103], vt: 60)
|
|
281
|
+
#
|
|
282
|
+
# @example Set absolute visibility time (timestamp)
|
|
283
|
+
# messages = client.set_vt_batch("orders", [101, 102], vt: Time.now + 300)
|
|
284
|
+
def set_vt_batch(queue_name, msg_ids, vt:)
|
|
285
|
+
validate_queue_name!(queue_name)
|
|
286
|
+
return [] if msg_ids.empty?
|
|
287
|
+
|
|
288
|
+
result = with_connection do |conn|
|
|
289
|
+
encoder = PG::TextEncoder::Array.new
|
|
290
|
+
encoded_array = encoder.encode(msg_ids)
|
|
291
|
+
|
|
292
|
+
if vt.is_a?(Time)
|
|
293
|
+
conn.exec_params(
|
|
294
|
+
"SELECT * FROM pgmq.set_vt($1::text, $2::bigint[], $3::timestamptz)",
|
|
295
|
+
[queue_name, encoded_array, vt.utc.iso8601(6)]
|
|
296
|
+
)
|
|
297
|
+
else
|
|
298
|
+
conn.exec_params(
|
|
299
|
+
"SELECT * FROM pgmq.set_vt($1::text, $2::bigint[], $3::integer)",
|
|
300
|
+
[queue_name, encoded_array, vt]
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
result.map { |row| Message.new(row) }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Updates visibility timeout for messages across multiple queues in a single transaction
|
|
309
|
+
#
|
|
310
|
+
# Efficiently updates visibility timeouts across different queues atomically.
|
|
311
|
+
# Useful when processing related messages from different queues and needing
|
|
312
|
+
# to extend their visibility timeouts together.
|
|
313
|
+
#
|
|
314
|
+
# Supports two modes:
|
|
315
|
+
# - Integer offset (seconds from now): `vt: 60` - messages visible in 60 seconds
|
|
316
|
+
# - Absolute timestamp: `vt: Time.now + 300` - messages visible at specific time
|
|
317
|
+
#
|
|
318
|
+
# @param updates [Hash] hash of queue_name => array of msg_ids
|
|
319
|
+
# @param vt [Integer, Time] visibility timeout as seconds offset or absolute timestamp
|
|
320
|
+
# @return [Hash] hash of queue_name => array of updated PGMQ::Message objects
|
|
321
|
+
#
|
|
322
|
+
# @example Extend visibility timeout for messages from multiple queues
|
|
323
|
+
# client.set_vt_multi({
|
|
324
|
+
# 'orders' => [1, 2, 3],
|
|
325
|
+
# 'notifications' => [5, 6],
|
|
326
|
+
# 'emails' => [10]
|
|
327
|
+
# }, vt: 60)
|
|
328
|
+
# # => { 'orders' => [<Message>, ...], 'notifications' => [...], 'emails' => [...] }
|
|
329
|
+
#
|
|
330
|
+
# @example Set absolute visibility time
|
|
331
|
+
# client.set_vt_multi(updates, vt: Time.now + 300)
|
|
332
|
+
#
|
|
333
|
+
# @example Extend timeout after batch reading from multiple queues
|
|
334
|
+
# messages = client.read_multi(['q1', 'q2', 'q3'], qty: 10)
|
|
335
|
+
# updates = messages.group_by(&:queue_name).transform_values { |msgs| msgs.map(&:msg_id) }
|
|
336
|
+
# client.set_vt_multi(updates, vt: 120)
|
|
337
|
+
def set_vt_multi(updates, vt:)
|
|
338
|
+
raise ArgumentError, "updates must be a hash" unless updates.is_a?(Hash)
|
|
339
|
+
return {} if updates.empty?
|
|
340
|
+
|
|
341
|
+
# Validate all queue names
|
|
342
|
+
updates.each_key { |qn| validate_queue_name!(qn) }
|
|
343
|
+
|
|
344
|
+
transaction do |txn|
|
|
345
|
+
result = {}
|
|
346
|
+
updates.each do |queue_name, msg_ids|
|
|
347
|
+
next if msg_ids.empty?
|
|
348
|
+
|
|
349
|
+
updated_messages = txn.set_vt_batch(queue_name, msg_ids, vt: vt)
|
|
350
|
+
result[queue_name] = updated_messages
|
|
351
|
+
end
|
|
352
|
+
result
|
|
353
|
+
end
|
|
354
|
+
end
|
|
238
355
|
end
|
|
239
356
|
end
|
|
240
357
|
end
|
data/lib/pgmq/client/metrics.rb
CHANGED
|
@@ -20,7 +20,7 @@ module PGMQ
|
|
|
20
20
|
validate_queue_name!(queue_name)
|
|
21
21
|
|
|
22
22
|
result = with_connection do |conn|
|
|
23
|
-
conn.exec_params(
|
|
23
|
+
conn.exec_params("SELECT * FROM pgmq.metrics($1::text)", [queue_name])
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
return nil if result.ntuples.zero?
|
|
@@ -39,7 +39,7 @@ module PGMQ
|
|
|
39
39
|
# end
|
|
40
40
|
def metrics_all
|
|
41
41
|
result = with_connection do |conn|
|
|
42
|
-
conn.exec(
|
|
42
|
+
conn.exec("SELECT * FROM pgmq.metrics_all()")
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
result.map { |row| PGMQ::Metrics.new(row) }
|
|
@@ -46,9 +46,9 @@ module PGMQ
|
|
|
46
46
|
qty: 1,
|
|
47
47
|
limit: nil
|
|
48
48
|
)
|
|
49
|
-
raise ArgumentError,
|
|
50
|
-
raise ArgumentError,
|
|
51
|
-
raise ArgumentError,
|
|
49
|
+
raise ArgumentError, "queue_names must be an array" unless queue_names.is_a?(Array)
|
|
50
|
+
raise ArgumentError, "queue_names cannot be empty" if queue_names.empty?
|
|
51
|
+
raise ArgumentError, "queue_names cannot exceed 50 queues" if queue_names.size > 50
|
|
52
52
|
|
|
53
53
|
# Validate all queue names (prevents SQL injection)
|
|
54
54
|
queue_names.each { |qn| validate_queue_name!(qn) }
|
|
@@ -118,9 +118,9 @@ module PGMQ
|
|
|
118
118
|
max_poll_seconds: 5,
|
|
119
119
|
poll_interval_ms: 100
|
|
120
120
|
)
|
|
121
|
-
raise ArgumentError,
|
|
122
|
-
raise ArgumentError,
|
|
123
|
-
raise ArgumentError,
|
|
121
|
+
raise ArgumentError, "queue_names must be an array" unless queue_names.is_a?(Array)
|
|
122
|
+
raise ArgumentError, "queue_names cannot be empty" if queue_names.empty?
|
|
123
|
+
raise ArgumentError, "queue_names cannot exceed 50 queues" if queue_names.size > 50
|
|
124
124
|
|
|
125
125
|
start_time = Time.now
|
|
126
126
|
poll_interval_seconds = poll_interval_ms / 1000.0
|
|
@@ -165,9 +165,9 @@ module PGMQ
|
|
|
165
165
|
# process(msg.queue_name, msg.payload)
|
|
166
166
|
# end
|
|
167
167
|
def pop_multi(queue_names)
|
|
168
|
-
raise ArgumentError,
|
|
169
|
-
raise ArgumentError,
|
|
170
|
-
raise ArgumentError,
|
|
168
|
+
raise ArgumentError, "queue_names must be an array" unless queue_names.is_a?(Array)
|
|
169
|
+
raise ArgumentError, "queue_names cannot be empty" if queue_names.empty?
|
|
170
|
+
raise ArgumentError, "queue_names cannot exceed 50 queues" if queue_names.size > 50
|
|
171
171
|
|
|
172
172
|
# Validate all queue names
|
|
173
173
|
queue_names.each { |qn| validate_queue_name!(qn) }
|
data/lib/pgmq/client/producer.rb
CHANGED
|
@@ -2,78 +2,123 @@
|
|
|
2
2
|
|
|
3
3
|
module PGMQ
|
|
4
4
|
class Client
|
|
5
|
-
# Message
|
|
5
|
+
# Message producing operations
|
|
6
6
|
#
|
|
7
|
-
# This module handles
|
|
7
|
+
# This module handles producing messages to queues, both individual messages
|
|
8
8
|
# and batches. Users must serialize messages to JSON strings themselves.
|
|
9
9
|
module Producer
|
|
10
|
-
#
|
|
10
|
+
# Produces a message to a queue
|
|
11
11
|
#
|
|
12
12
|
# @param queue_name [String] name of the queue
|
|
13
13
|
# @param message [String] message as JSON string (for PostgreSQL JSONB)
|
|
14
|
+
# @param headers [String, nil] optional headers as JSON string (for metadata, routing, tracing)
|
|
14
15
|
# @param delay [Integer] delay in seconds before message becomes visible
|
|
15
16
|
# @return [String] message ID as string
|
|
16
17
|
#
|
|
17
|
-
# @example
|
|
18
|
-
# msg_id = client.
|
|
18
|
+
# @example Basic produce
|
|
19
|
+
# msg_id = client.produce("orders", '{"order_id":123,"total":99.99}')
|
|
19
20
|
#
|
|
20
21
|
# @example With delay
|
|
21
|
-
# msg_id = client.
|
|
22
|
+
# msg_id = client.produce("orders", '{"data":"value"}', delay: 60)
|
|
23
|
+
#
|
|
24
|
+
# @example With headers for routing/tracing
|
|
25
|
+
# msg_id = client.produce("orders", '{"order_id":123}',
|
|
26
|
+
# headers: '{"trace_id":"abc123","priority":"high"}')
|
|
27
|
+
#
|
|
28
|
+
# @example With headers and delay
|
|
29
|
+
# msg_id = client.produce("orders", '{"order_id":123}',
|
|
30
|
+
# headers: '{"correlation_id":"req-456"}',
|
|
31
|
+
# delay: 30)
|
|
22
32
|
#
|
|
23
33
|
# @note Users must serialize to JSON themselves. Higher-level frameworks
|
|
24
34
|
# should handle serialization.
|
|
25
|
-
def
|
|
35
|
+
def produce(
|
|
26
36
|
queue_name,
|
|
27
37
|
message,
|
|
38
|
+
headers: nil,
|
|
28
39
|
delay: 0
|
|
29
40
|
)
|
|
30
41
|
validate_queue_name!(queue_name)
|
|
31
42
|
|
|
32
43
|
result = with_connection do |conn|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
if headers
|
|
45
|
+
conn.exec_params(
|
|
46
|
+
"SELECT * FROM pgmq.send($1::text, $2::jsonb, $3::jsonb, $4::integer)",
|
|
47
|
+
[queue_name, message, headers, delay]
|
|
48
|
+
)
|
|
49
|
+
else
|
|
50
|
+
conn.exec_params(
|
|
51
|
+
"SELECT * FROM pgmq.send($1::text, $2::jsonb, $3::integer)",
|
|
52
|
+
[queue_name, message, delay]
|
|
53
|
+
)
|
|
54
|
+
end
|
|
37
55
|
end
|
|
38
56
|
|
|
39
|
-
result[0][
|
|
57
|
+
result[0]["send"]
|
|
40
58
|
end
|
|
41
59
|
|
|
42
|
-
#
|
|
60
|
+
# Produces multiple messages to a queue in a batch
|
|
43
61
|
#
|
|
44
62
|
# @param queue_name [String] name of the queue
|
|
45
|
-
# @param messages [Array<
|
|
63
|
+
# @param messages [Array<String>] array of message payloads as JSON strings
|
|
64
|
+
# @param headers [Array<String>, nil] optional array of headers as JSON strings (must match messages length)
|
|
46
65
|
# @param delay [Integer] delay in seconds before messages become visible
|
|
47
|
-
# @return [Array<
|
|
66
|
+
# @return [Array<String>] array of message IDs
|
|
67
|
+
# @raise [ArgumentError] if headers array length doesn't match messages length
|
|
48
68
|
#
|
|
49
|
-
# @example
|
|
50
|
-
# ids = client.
|
|
51
|
-
# {
|
|
52
|
-
# {
|
|
53
|
-
# {
|
|
69
|
+
# @example Basic batch produce
|
|
70
|
+
# ids = client.produce_batch("orders", [
|
|
71
|
+
# '{"order_id":1}',
|
|
72
|
+
# '{"order_id":2}',
|
|
73
|
+
# '{"order_id":3}'
|
|
54
74
|
# ])
|
|
55
|
-
|
|
75
|
+
#
|
|
76
|
+
# @example With headers (one per message)
|
|
77
|
+
# ids = client.produce_batch("orders",
|
|
78
|
+
# ['{"order_id":1}', '{"order_id":2}'],
|
|
79
|
+
# headers: ['{"priority":"high"}', '{"priority":"low"}'])
|
|
80
|
+
#
|
|
81
|
+
# @example With headers and delay
|
|
82
|
+
# ids = client.produce_batch("orders",
|
|
83
|
+
# ['{"order_id":1}', '{"order_id":2}'],
|
|
84
|
+
# headers: ['{"trace_id":"a"}', '{"trace_id":"b"}'],
|
|
85
|
+
# delay: 60)
|
|
86
|
+
def produce_batch(
|
|
56
87
|
queue_name,
|
|
57
88
|
messages,
|
|
89
|
+
headers: nil,
|
|
58
90
|
delay: 0
|
|
59
91
|
)
|
|
60
92
|
validate_queue_name!(queue_name)
|
|
61
93
|
return [] if messages.empty?
|
|
62
94
|
|
|
95
|
+
if headers && headers.length != messages.length
|
|
96
|
+
raise ArgumentError,
|
|
97
|
+
"headers array length (#{headers.length}) must match messages array length (#{messages.length})"
|
|
98
|
+
end
|
|
99
|
+
|
|
63
100
|
# Use PostgreSQL array parameter binding for security
|
|
64
101
|
# PG gem will properly encode the array values
|
|
65
102
|
result = with_connection do |conn|
|
|
66
103
|
# Create array encoder for proper PostgreSQL array formatting
|
|
67
104
|
encoder = PG::TextEncoder::Array.new
|
|
68
|
-
|
|
105
|
+
encoded_messages = encoder.encode(messages)
|
|
69
106
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
107
|
+
if headers
|
|
108
|
+
encoded_headers = encoder.encode(headers)
|
|
109
|
+
conn.exec_params(
|
|
110
|
+
"SELECT * FROM pgmq.send_batch($1::text, $2::jsonb[], $3::jsonb[], $4::integer)",
|
|
111
|
+
[queue_name, encoded_messages, encoded_headers, delay]
|
|
112
|
+
)
|
|
113
|
+
else
|
|
114
|
+
conn.exec_params(
|
|
115
|
+
"SELECT * FROM pgmq.send_batch($1::text, $2::jsonb[], $3::integer)",
|
|
116
|
+
[queue_name, encoded_messages, delay]
|
|
117
|
+
)
|
|
118
|
+
end
|
|
74
119
|
end
|
|
75
120
|
|
|
76
|
-
result.map { |row| row[
|
|
121
|
+
result.map { |row| row["send_batch"] }
|
|
77
122
|
end
|
|
78
123
|
end
|
|
79
124
|
end
|