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.
@@ -33,12 +33,12 @@ module PGMQ
33
33
  result = with_connection do |conn|
34
34
  if conditional.empty?
35
35
  conn.exec_params(
36
- 'SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer)',
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
- 'SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer, $4::jsonb)',
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
- 'SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer)',
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
- 'SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer, $4::jsonb)',
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
- 'SELECT * FROM pgmq.read_with_poll($1::text, $2::integer, $3::integer, $4::integer, $5::integer)',
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 = 'SELECT * FROM pgmq.read_with_poll($1::text, $2::integer, $3::integer, ' \
143
- '$4::integer, $5::integer, $6::jsonb)'
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('SELECT pgmq.purge_queue($1::text)', [queue_name])
22
+ conn.exec_params("SELECT pgmq.purge_queue($1::text)", [queue_name])
23
23
  end
24
24
 
25
- result[0]['purge_queue']
25
+ result[0]["purge_queue"]
26
26
  end
27
27
 
28
- # Detaches the archive table from PGMQ management
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.detach_archive("orders")
35
- def detach_archive(queue_name)
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('SELECT pgmq.detach_archive($1::text)', [queue_name])
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('SELECT * FROM pgmq.pop($1::text)', [queue_name])
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
- 'SELECT pgmq.delete($1::text, $2::bigint)',
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]['delete'] == 't'
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
- 'SELECT * FROM pgmq.delete($1::text, $2::bigint[])',
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['delete'] }
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, 'deletions must be a hash' unless deletions.is_a?(Hash)
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
- 'SELECT pgmq.archive($1::text, $2::bigint)',
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]['archive'] == 't'
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
- 'SELECT * FROM pgmq.archive($1::text, $2::bigint[])',
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['archive'] }
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, 'archives must be a hash' unless archives.is_a?(Hash)
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 vt_offset [Integer] visibility timeout offset in seconds
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
- # # Extend processing time by 60 more seconds
219
- # msg = client.set_vt("orders", 123, vt_offset: 60)
220
- def set_vt(
221
- queue_name,
222
- msg_id,
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
- conn.exec_params(
229
- 'SELECT * FROM pgmq.set_vt($1::text, $2::bigint, $3::integer)',
230
- [queue_name, msg_id, vt_offset]
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
@@ -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('SELECT * FROM pgmq.metrics($1::text)', [queue_name])
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('SELECT * FROM pgmq.metrics_all()')
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, '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
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, '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
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, '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
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) }
@@ -2,78 +2,123 @@
2
2
 
3
3
  module PGMQ
4
4
  class Client
5
- # Message sending operations
5
+ # Message producing operations
6
6
  #
7
- # This module handles sending messages to queues, both individual messages
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
- # Sends a message to a queue
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.send("orders", '{"order_id":123,"total":99.99}')
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.send("orders", '{"data":"value"}', delay: 60)
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 send(
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
- conn.exec_params(
34
- 'SELECT * FROM pgmq.send($1::text, $2::jsonb, $3::integer)',
35
- [queue_name, message, delay]
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]['send']
57
+ result[0]["send"]
40
58
  end
41
59
 
42
- # Sends multiple messages to a queue in a batch
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<Hash>] array of message payloads
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<Integer>] array of message IDs
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.send_batch("orders", [
51
- # { order_id: 1 },
52
- # { order_id: 2 },
53
- # { order_id: 3 }
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
- def send_batch(
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
- encoded_array = encoder.encode(messages)
105
+ encoded_messages = encoder.encode(messages)
69
106
 
70
- conn.exec_params(
71
- 'SELECT * FROM pgmq.send_batch($1::text, $2::jsonb[], $3::integer)',
72
- [queue_name, encoded_array, delay]
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['send_batch'] }
121
+ result.map { |row| row["send_batch"] }
77
122
  end
78
123
  end
79
124
  end