pgmq-ruby 0.1.0 → 0.4.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/.coditsu/ci.yml +3 -0
- data/.github/workflows/ci.yml +163 -0
- data/.github/workflows/push.yml +35 -0
- data/.gitignore +67 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/.yard-lint.yml +275 -0
- data/CHANGELOG.md +125 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +65 -0
- data/LICENSE +165 -0
- data/README.md +687 -0
- data/Rakefile +4 -0
- data/docker-compose.yml +22 -0
- data/lib/pgmq/client/consumer.rb +155 -0
- data/lib/pgmq/client/maintenance.rb +94 -0
- data/lib/pgmq/client/message_lifecycle.rb +332 -0
- data/lib/pgmq/client/metrics.rb +49 -0
- data/lib/pgmq/client/multi_queue.rb +193 -0
- data/lib/pgmq/client/producer.rb +125 -0
- data/lib/pgmq/client/queue_management.rb +128 -0
- data/lib/pgmq/client.rb +138 -0
- data/lib/pgmq/connection.rb +196 -0
- data/lib/pgmq/errors.rb +30 -0
- data/lib/pgmq/message.rb +45 -0
- data/lib/pgmq/metrics.rb +37 -0
- data/lib/pgmq/queue_metadata.rb +37 -0
- data/lib/pgmq/transaction.rb +92 -0
- data/lib/pgmq/version.rb +6 -0
- data/lib/pgmq.rb +53 -0
- data/pgmq-ruby.gemspec +32 -0
- data/renovate.json +11 -0
- metadata +67 -5
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGMQ
|
|
4
|
+
class Client
|
|
5
|
+
# Single-queue message reading operations
|
|
6
|
+
#
|
|
7
|
+
# This module handles reading messages from a single queue, including basic reads,
|
|
8
|
+
# batch reads, and long-polling for efficient message consumption.
|
|
9
|
+
module Consumer
|
|
10
|
+
# Reads a message from the queue
|
|
11
|
+
#
|
|
12
|
+
# @param queue_name [String] name of the queue
|
|
13
|
+
# @param vt [Integer] visibility timeout in seconds
|
|
14
|
+
# @param conditional [Hash] optional JSONB filter for message payload
|
|
15
|
+
# @return [PGMQ::Message, nil] message object or nil if queue is empty
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# msg = client.read("orders", vt: 30)
|
|
19
|
+
# if msg
|
|
20
|
+
# process(msg.payload)
|
|
21
|
+
# client.delete("orders", msg.msg_id)
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @example With conditional filtering
|
|
25
|
+
# msg = client.read("orders", vt: 30, conditional: { type: "priority", status: "pending" })
|
|
26
|
+
def read(
|
|
27
|
+
queue_name,
|
|
28
|
+
vt: DEFAULT_VT,
|
|
29
|
+
conditional: {}
|
|
30
|
+
)
|
|
31
|
+
validate_queue_name!(queue_name)
|
|
32
|
+
|
|
33
|
+
result = with_connection do |conn|
|
|
34
|
+
if conditional.empty?
|
|
35
|
+
conn.exec_params(
|
|
36
|
+
'SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer)',
|
|
37
|
+
[queue_name, vt, 1]
|
|
38
|
+
)
|
|
39
|
+
else
|
|
40
|
+
conn.exec_params(
|
|
41
|
+
'SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer, $4::jsonb)',
|
|
42
|
+
[queue_name, vt, 1, conditional.to_json]
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
return nil if result.ntuples.zero?
|
|
48
|
+
|
|
49
|
+
Message.new(result[0])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Reads multiple messages from the queue
|
|
53
|
+
#
|
|
54
|
+
# @param queue_name [String] name of the queue
|
|
55
|
+
# @param vt [Integer] visibility timeout in seconds
|
|
56
|
+
# @param qty [Integer] number of messages to read
|
|
57
|
+
# @param conditional [Hash] optional JSONB filter for message payload
|
|
58
|
+
# @return [Array<PGMQ::Message>] array of messages
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# messages = client.read_batch("orders", vt: 30, qty: 10)
|
|
62
|
+
# messages.each do |msg|
|
|
63
|
+
# process(msg.payload)
|
|
64
|
+
# client.delete("orders", msg.msg_id)
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# @example With conditional filtering
|
|
68
|
+
# messages = client.read_batch(
|
|
69
|
+
# "orders",
|
|
70
|
+
# vt: 30,
|
|
71
|
+
# qty: 10,
|
|
72
|
+
# conditional: { priority: "high" }
|
|
73
|
+
# )
|
|
74
|
+
def read_batch(
|
|
75
|
+
queue_name,
|
|
76
|
+
vt: DEFAULT_VT,
|
|
77
|
+
qty: 1,
|
|
78
|
+
conditional: {}
|
|
79
|
+
)
|
|
80
|
+
validate_queue_name!(queue_name)
|
|
81
|
+
|
|
82
|
+
result = with_connection do |conn|
|
|
83
|
+
if conditional.empty?
|
|
84
|
+
conn.exec_params(
|
|
85
|
+
'SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer)',
|
|
86
|
+
[queue_name, vt, qty]
|
|
87
|
+
)
|
|
88
|
+
else
|
|
89
|
+
conn.exec_params(
|
|
90
|
+
'SELECT * FROM pgmq.read($1::text, $2::integer, $3::integer, $4::jsonb)',
|
|
91
|
+
[queue_name, vt, qty, conditional.to_json]
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
result.map { |row| Message.new(row) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Reads messages with long-polling support
|
|
100
|
+
#
|
|
101
|
+
# Polls the queue for messages, waiting up to max_poll_seconds if queue is empty
|
|
102
|
+
#
|
|
103
|
+
# @param queue_name [String] name of the queue
|
|
104
|
+
# @param vt [Integer] visibility timeout in seconds
|
|
105
|
+
# @param qty [Integer] number of messages to read
|
|
106
|
+
# @param max_poll_seconds [Integer] maximum time to poll in seconds
|
|
107
|
+
# @param poll_interval_ms [Integer] interval between polls in milliseconds
|
|
108
|
+
# @param conditional [Hash] optional JSONB filter for message payload
|
|
109
|
+
# @return [Array<PGMQ::Message>] array of messages
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# messages = client.read_with_poll("orders",
|
|
113
|
+
# vt: 30,
|
|
114
|
+
# qty: 5,
|
|
115
|
+
# max_poll_seconds: 10,
|
|
116
|
+
# poll_interval_ms: 250
|
|
117
|
+
# )
|
|
118
|
+
#
|
|
119
|
+
# @example With conditional filtering
|
|
120
|
+
# messages = client.read_with_poll("orders",
|
|
121
|
+
# vt: 30,
|
|
122
|
+
# qty: 5,
|
|
123
|
+
# conditional: { status: "pending" }
|
|
124
|
+
# )
|
|
125
|
+
def read_with_poll(
|
|
126
|
+
queue_name,
|
|
127
|
+
vt: DEFAULT_VT,
|
|
128
|
+
qty: 1,
|
|
129
|
+
max_poll_seconds: 5,
|
|
130
|
+
poll_interval_ms: 100,
|
|
131
|
+
conditional: {}
|
|
132
|
+
)
|
|
133
|
+
validate_queue_name!(queue_name)
|
|
134
|
+
|
|
135
|
+
result = with_connection do |conn|
|
|
136
|
+
if conditional.empty?
|
|
137
|
+
conn.exec_params(
|
|
138
|
+
'SELECT * FROM pgmq.read_with_poll($1::text, $2::integer, $3::integer, $4::integer, $5::integer)',
|
|
139
|
+
[queue_name, vt, qty, max_poll_seconds, poll_interval_ms]
|
|
140
|
+
)
|
|
141
|
+
else
|
|
142
|
+
sql = 'SELECT * FROM pgmq.read_with_poll($1::text, $2::integer, $3::integer, ' \
|
|
143
|
+
'$4::integer, $5::integer, $6::jsonb)'
|
|
144
|
+
conn.exec_params(
|
|
145
|
+
sql,
|
|
146
|
+
[queue_name, vt, qty, max_poll_seconds, poll_interval_ms, conditional.to_json]
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
result.map { |row| Message.new(row) }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGMQ
|
|
4
|
+
class Client
|
|
5
|
+
# Queue maintenance operations
|
|
6
|
+
#
|
|
7
|
+
# This module handles queue maintenance tasks such as purging messages
|
|
8
|
+
# and detaching archive tables.
|
|
9
|
+
module Maintenance
|
|
10
|
+
# Purges all messages from a queue
|
|
11
|
+
#
|
|
12
|
+
# @param queue_name [String] name of the queue
|
|
13
|
+
# @return [Integer] number of messages purged
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# count = client.purge_queue("old_queue")
|
|
17
|
+
# puts "Purged #{count} messages"
|
|
18
|
+
def purge_queue(queue_name)
|
|
19
|
+
validate_queue_name!(queue_name)
|
|
20
|
+
|
|
21
|
+
result = with_connection do |conn|
|
|
22
|
+
conn.exec_params('SELECT pgmq.purge_queue($1::text)', [queue_name])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
result[0]['purge_queue']
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Detaches the archive table from PGMQ management
|
|
29
|
+
#
|
|
30
|
+
# @param queue_name [String] name of the queue
|
|
31
|
+
# @return [void]
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# client.detach_archive("orders")
|
|
35
|
+
def detach_archive(queue_name)
|
|
36
|
+
validate_queue_name!(queue_name)
|
|
37
|
+
|
|
38
|
+
with_connection do |conn|
|
|
39
|
+
conn.exec_params('SELECT pgmq.detach_archive($1::text)', [queue_name])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Enables PostgreSQL NOTIFY when messages are inserted into a queue
|
|
46
|
+
#
|
|
47
|
+
# When enabled, PostgreSQL will send a NOTIFY event on message insert,
|
|
48
|
+
# allowing clients to use LISTEN instead of polling. The throttle interval
|
|
49
|
+
# prevents notification storms during high-volume inserts.
|
|
50
|
+
#
|
|
51
|
+
# @param queue_name [String] name of the queue
|
|
52
|
+
# @param throttle_interval_ms [Integer] minimum ms between notifications (default: 250)
|
|
53
|
+
# @return [void]
|
|
54
|
+
#
|
|
55
|
+
# @example Enable with default throttle (250ms)
|
|
56
|
+
# client.enable_notify_insert("orders")
|
|
57
|
+
#
|
|
58
|
+
# @example Enable with custom throttle (1 second)
|
|
59
|
+
# client.enable_notify_insert("orders", throttle_interval_ms: 1000)
|
|
60
|
+
#
|
|
61
|
+
# @example Disable throttling (notify on every insert)
|
|
62
|
+
# client.enable_notify_insert("orders", throttle_interval_ms: 0)
|
|
63
|
+
def enable_notify_insert(queue_name, throttle_interval_ms: 250)
|
|
64
|
+
validate_queue_name!(queue_name)
|
|
65
|
+
|
|
66
|
+
with_connection do |conn|
|
|
67
|
+
conn.exec_params(
|
|
68
|
+
'SELECT pgmq.enable_notify_insert($1::text, $2::integer)',
|
|
69
|
+
[queue_name, throttle_interval_ms]
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Disables PostgreSQL NOTIFY for a queue
|
|
77
|
+
#
|
|
78
|
+
# @param queue_name [String] name of the queue
|
|
79
|
+
# @return [void]
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# client.disable_notify_insert("orders")
|
|
83
|
+
def disable_notify_insert(queue_name)
|
|
84
|
+
validate_queue_name!(queue_name)
|
|
85
|
+
|
|
86
|
+
with_connection do |conn|
|
|
87
|
+
conn.exec_params('SELECT pgmq.disable_notify_insert($1::text)', [queue_name])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGMQ
|
|
4
|
+
class Client
|
|
5
|
+
# Message lifecycle operations (pop, delete, archive, visibility timeout)
|
|
6
|
+
#
|
|
7
|
+
# This module handles message state transitions including popping (atomic read+delete),
|
|
8
|
+
# deleting, archiving, and updating visibility timeouts.
|
|
9
|
+
module MessageLifecycle
|
|
10
|
+
# Pops a message (atomic read + delete)
|
|
11
|
+
#
|
|
12
|
+
# @param queue_name [String] name of the queue
|
|
13
|
+
# @return [PGMQ::Message, nil] message object or nil if queue is empty
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# msg = client.pop("orders")
|
|
17
|
+
# process(msg.payload) if msg
|
|
18
|
+
def pop(queue_name)
|
|
19
|
+
validate_queue_name!(queue_name)
|
|
20
|
+
|
|
21
|
+
result = with_connection do |conn|
|
|
22
|
+
conn.exec_params('SELECT * FROM pgmq.pop($1::text)', [queue_name])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
return nil if result.ntuples.zero?
|
|
26
|
+
|
|
27
|
+
Message.new(result[0])
|
|
28
|
+
end
|
|
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
|
+
|
|
50
|
+
# Deletes a message from the queue
|
|
51
|
+
#
|
|
52
|
+
# @param queue_name [String] name of the queue
|
|
53
|
+
# @param msg_id [Integer] message ID to delete
|
|
54
|
+
# @return [Boolean] true if message was deleted
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# client.delete("orders", 123)
|
|
58
|
+
def delete(
|
|
59
|
+
queue_name,
|
|
60
|
+
msg_id
|
|
61
|
+
)
|
|
62
|
+
validate_queue_name!(queue_name)
|
|
63
|
+
|
|
64
|
+
result = with_connection do |conn|
|
|
65
|
+
conn.exec_params(
|
|
66
|
+
'SELECT pgmq.delete($1::text, $2::bigint)',
|
|
67
|
+
[queue_name, msg_id]
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
return false if result.ntuples.zero?
|
|
72
|
+
|
|
73
|
+
result[0]['delete'] == 't'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Deletes multiple messages from the queue
|
|
77
|
+
#
|
|
78
|
+
# @param queue_name [String] name of the queue
|
|
79
|
+
# @param msg_ids [Array<Integer>] array of message IDs to delete
|
|
80
|
+
# @return [Array<Integer>] array of successfully deleted message IDs
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# deleted = client.delete_batch("orders", [101, 102, 103])
|
|
84
|
+
def delete_batch(
|
|
85
|
+
queue_name,
|
|
86
|
+
msg_ids
|
|
87
|
+
)
|
|
88
|
+
validate_queue_name!(queue_name)
|
|
89
|
+
return [] if msg_ids.empty?
|
|
90
|
+
|
|
91
|
+
# Use PostgreSQL array parameter binding
|
|
92
|
+
result = with_connection do |conn|
|
|
93
|
+
encoder = PG::TextEncoder::Array.new
|
|
94
|
+
encoded_array = encoder.encode(msg_ids)
|
|
95
|
+
|
|
96
|
+
conn.exec_params(
|
|
97
|
+
'SELECT * FROM pgmq.delete($1::text, $2::bigint[])',
|
|
98
|
+
[queue_name, encoded_array]
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
result.map { |row| row['delete'] }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Deletes specific messages from multiple queues in a single transaction
|
|
106
|
+
#
|
|
107
|
+
# Efficiently deletes messages across different queues atomically.
|
|
108
|
+
# Useful when processing related messages from different queues.
|
|
109
|
+
#
|
|
110
|
+
# @param deletions [Hash] hash of queue_name => array of msg_ids
|
|
111
|
+
# @return [Hash] hash of queue_name => array of deleted msg_ids
|
|
112
|
+
#
|
|
113
|
+
# @example Delete messages from multiple queues
|
|
114
|
+
# client.delete_multi({
|
|
115
|
+
# 'orders' => [1, 2, 3],
|
|
116
|
+
# 'notifications' => [5, 6],
|
|
117
|
+
# 'emails' => [10]
|
|
118
|
+
# })
|
|
119
|
+
# # => { 'orders' => [1, 2, 3], 'notifications' => [5, 6], 'emails' => [10] }
|
|
120
|
+
#
|
|
121
|
+
# @example Clean up after batch processing across queues
|
|
122
|
+
# messages = client.read_multi(['q1', 'q2', 'q3'], qty: 10)
|
|
123
|
+
# deletions = messages.group_by(&:queue_name).transform_values { |mss| mss.map(&:msg_id) }
|
|
124
|
+
# client.delete_multi(deletions)
|
|
125
|
+
def delete_multi(deletions)
|
|
126
|
+
raise ArgumentError, 'deletions must be a hash' unless deletions.is_a?(Hash)
|
|
127
|
+
return {} if deletions.empty?
|
|
128
|
+
|
|
129
|
+
# Validate all queue names
|
|
130
|
+
deletions.each_key { |qn| validate_queue_name!(qn) }
|
|
131
|
+
|
|
132
|
+
transaction do |txn|
|
|
133
|
+
result = {}
|
|
134
|
+
deletions.each do |queue_name, msg_ids|
|
|
135
|
+
next if msg_ids.empty?
|
|
136
|
+
|
|
137
|
+
deleted_ids = txn.delete_batch(queue_name, msg_ids)
|
|
138
|
+
result[queue_name] = deleted_ids
|
|
139
|
+
end
|
|
140
|
+
result
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Archives a message
|
|
145
|
+
#
|
|
146
|
+
# @param queue_name [String] name of the queue
|
|
147
|
+
# @param msg_id [Integer] message ID to archive
|
|
148
|
+
# @return [Boolean] true if message was archived
|
|
149
|
+
#
|
|
150
|
+
# @example
|
|
151
|
+
# client.archive("orders", 123)
|
|
152
|
+
def archive(
|
|
153
|
+
queue_name,
|
|
154
|
+
msg_id
|
|
155
|
+
)
|
|
156
|
+
validate_queue_name!(queue_name)
|
|
157
|
+
|
|
158
|
+
result = with_connection do |conn|
|
|
159
|
+
conn.exec_params(
|
|
160
|
+
'SELECT pgmq.archive($1::text, $2::bigint)',
|
|
161
|
+
[queue_name, msg_id]
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
return false if result.ntuples.zero?
|
|
166
|
+
|
|
167
|
+
result[0]['archive'] == 't'
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Archives multiple messages
|
|
171
|
+
#
|
|
172
|
+
# @param queue_name [String] name of the queue
|
|
173
|
+
# @param msg_ids [Array<Integer>] array of message IDs to archive
|
|
174
|
+
# @return [Array<Integer>] array of successfully archived message IDs
|
|
175
|
+
#
|
|
176
|
+
# @example
|
|
177
|
+
# archived = client.archive_batch("orders", [101, 102, 103])
|
|
178
|
+
def archive_batch(
|
|
179
|
+
queue_name,
|
|
180
|
+
msg_ids
|
|
181
|
+
)
|
|
182
|
+
validate_queue_name!(queue_name)
|
|
183
|
+
return [] if msg_ids.empty?
|
|
184
|
+
|
|
185
|
+
# Use PostgreSQL array parameter binding
|
|
186
|
+
result = with_connection do |conn|
|
|
187
|
+
encoder = PG::TextEncoder::Array.new
|
|
188
|
+
encoded_array = encoder.encode(msg_ids)
|
|
189
|
+
|
|
190
|
+
conn.exec_params(
|
|
191
|
+
'SELECT * FROM pgmq.archive($1::text, $2::bigint[])',
|
|
192
|
+
[queue_name, encoded_array]
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
result.map { |row| row['archive'] }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Archives specific messages from multiple queues in a single transaction
|
|
200
|
+
#
|
|
201
|
+
# Efficiently archives messages across different queues atomically.
|
|
202
|
+
#
|
|
203
|
+
# @param archives [Hash] hash of queue_name => array of msg_ids
|
|
204
|
+
# @return [Hash] hash of queue_name => array of archived msg_ids
|
|
205
|
+
#
|
|
206
|
+
# @example Archive messages from multiple queues
|
|
207
|
+
# client.archive_multi({
|
|
208
|
+
# 'orders' => [1, 2],
|
|
209
|
+
# 'notifications' => [5]
|
|
210
|
+
# })
|
|
211
|
+
def archive_multi(archives)
|
|
212
|
+
raise ArgumentError, 'archives must be a hash' unless archives.is_a?(Hash)
|
|
213
|
+
return {} if archives.empty?
|
|
214
|
+
|
|
215
|
+
# Validate all queue names
|
|
216
|
+
archives.each_key { |qn| validate_queue_name!(qn) }
|
|
217
|
+
|
|
218
|
+
transaction do |txn|
|
|
219
|
+
result = {}
|
|
220
|
+
archives.each do |queue_name, msg_ids|
|
|
221
|
+
next if msg_ids.empty?
|
|
222
|
+
|
|
223
|
+
archived_ids = txn.archive_batch(queue_name, msg_ids)
|
|
224
|
+
result[queue_name] = archived_ids
|
|
225
|
+
end
|
|
226
|
+
result
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Updates the visibility timeout for a message
|
|
231
|
+
#
|
|
232
|
+
# @param queue_name [String] name of the queue
|
|
233
|
+
# @param msg_id [Integer] message ID
|
|
234
|
+
# @param vt_offset [Integer] visibility timeout offset in seconds
|
|
235
|
+
# @return [PGMQ::Message, nil] updated message or nil if not found
|
|
236
|
+
#
|
|
237
|
+
# @example
|
|
238
|
+
# # Extend processing time by 60 more seconds
|
|
239
|
+
# msg = client.set_vt("orders", 123, vt_offset: 60)
|
|
240
|
+
def set_vt(
|
|
241
|
+
queue_name,
|
|
242
|
+
msg_id,
|
|
243
|
+
vt_offset:
|
|
244
|
+
)
|
|
245
|
+
validate_queue_name!(queue_name)
|
|
246
|
+
|
|
247
|
+
result = with_connection do |conn|
|
|
248
|
+
conn.exec_params(
|
|
249
|
+
'SELECT * FROM pgmq.set_vt($1::text, $2::bigint, $3::integer)',
|
|
250
|
+
[queue_name, msg_id, vt_offset]
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
return nil if result.ntuples.zero?
|
|
255
|
+
|
|
256
|
+
Message.new(result[0])
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Updates visibility timeout for multiple messages
|
|
260
|
+
#
|
|
261
|
+
# @param queue_name [String] name of the queue
|
|
262
|
+
# @param msg_ids [Array<Integer>] array of message IDs
|
|
263
|
+
# @param vt_offset [Integer] visibility timeout offset in seconds
|
|
264
|
+
# @return [Array<PGMQ::Message>] array of updated messages
|
|
265
|
+
#
|
|
266
|
+
# @example
|
|
267
|
+
# # Extend processing time for multiple messages
|
|
268
|
+
# messages = client.set_vt_batch("orders", [101, 102, 103], vt_offset: 60)
|
|
269
|
+
def set_vt_batch(
|
|
270
|
+
queue_name,
|
|
271
|
+
msg_ids,
|
|
272
|
+
vt_offset:
|
|
273
|
+
)
|
|
274
|
+
validate_queue_name!(queue_name)
|
|
275
|
+
return [] if msg_ids.empty?
|
|
276
|
+
|
|
277
|
+
result = with_connection do |conn|
|
|
278
|
+
encoder = PG::TextEncoder::Array.new
|
|
279
|
+
encoded_array = encoder.encode(msg_ids)
|
|
280
|
+
|
|
281
|
+
conn.exec_params(
|
|
282
|
+
'SELECT * FROM pgmq.set_vt($1::text, $2::bigint[], $3::integer)',
|
|
283
|
+
[queue_name, encoded_array, vt_offset]
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
result.map { |row| Message.new(row) }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Updates visibility timeout for messages across multiple queues in a single transaction
|
|
291
|
+
#
|
|
292
|
+
# Efficiently updates visibility timeouts across different queues atomically.
|
|
293
|
+
# Useful when processing related messages from different queues and needing
|
|
294
|
+
# to extend their visibility timeouts together.
|
|
295
|
+
#
|
|
296
|
+
# @param updates [Hash] hash of queue_name => array of msg_ids
|
|
297
|
+
# @param vt_offset [Integer] visibility timeout offset in seconds (applied to all)
|
|
298
|
+
# @return [Hash] hash of queue_name => array of updated PGMQ::Message objects
|
|
299
|
+
#
|
|
300
|
+
# @example Extend visibility timeout for messages from multiple queues
|
|
301
|
+
# client.set_vt_multi({
|
|
302
|
+
# 'orders' => [1, 2, 3],
|
|
303
|
+
# 'notifications' => [5, 6],
|
|
304
|
+
# 'emails' => [10]
|
|
305
|
+
# }, vt_offset: 60)
|
|
306
|
+
# # => { 'orders' => [<Message>, ...], 'notifications' => [...], 'emails' => [...] }
|
|
307
|
+
#
|
|
308
|
+
# @example Extend timeout after batch reading from multiple queues
|
|
309
|
+
# messages = client.read_multi(['q1', 'q2', 'q3'], qty: 10)
|
|
310
|
+
# updates = messages.group_by(&:queue_name).transform_values { |msgs| msgs.map(&:msg_id) }
|
|
311
|
+
# client.set_vt_multi(updates, vt_offset: 120)
|
|
312
|
+
def set_vt_multi(updates, vt_offset:)
|
|
313
|
+
raise ArgumentError, 'updates must be a hash' unless updates.is_a?(Hash)
|
|
314
|
+
return {} if updates.empty?
|
|
315
|
+
|
|
316
|
+
# Validate all queue names
|
|
317
|
+
updates.each_key { |qn| validate_queue_name!(qn) }
|
|
318
|
+
|
|
319
|
+
transaction do |txn|
|
|
320
|
+
result = {}
|
|
321
|
+
updates.each do |queue_name, msg_ids|
|
|
322
|
+
next if msg_ids.empty?
|
|
323
|
+
|
|
324
|
+
updated_messages = txn.set_vt_batch(queue_name, msg_ids, vt_offset: vt_offset)
|
|
325
|
+
result[queue_name] = updated_messages
|
|
326
|
+
end
|
|
327
|
+
result
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGMQ
|
|
4
|
+
class Client
|
|
5
|
+
# Queue metrics and monitoring
|
|
6
|
+
#
|
|
7
|
+
# This module handles retrieving queue metrics such as queue length,
|
|
8
|
+
# message age, and total message counts.
|
|
9
|
+
module Metrics
|
|
10
|
+
# Gets metrics for a specific queue
|
|
11
|
+
#
|
|
12
|
+
# @param queue_name [String] name of the queue
|
|
13
|
+
# @return [PGMQ::Metrics] metrics object
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# metrics = client.metrics("orders")
|
|
17
|
+
# puts "Queue length: #{metrics.queue_length}"
|
|
18
|
+
# puts "Oldest message: #{metrics.oldest_msg_age_sec}s"
|
|
19
|
+
def metrics(queue_name)
|
|
20
|
+
validate_queue_name!(queue_name)
|
|
21
|
+
|
|
22
|
+
result = with_connection do |conn|
|
|
23
|
+
conn.exec_params('SELECT * FROM pgmq.metrics($1::text)', [queue_name])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return nil if result.ntuples.zero?
|
|
27
|
+
|
|
28
|
+
PGMQ::Metrics.new(result[0])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Gets metrics for all queues
|
|
32
|
+
#
|
|
33
|
+
# @return [Array<PGMQ::Metrics>] array of metrics objects
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# all_metrics = client.metrics_all
|
|
37
|
+
# all_metrics.each do |m|
|
|
38
|
+
# puts "#{m.queue_name}: #{m.queue_length} messages"
|
|
39
|
+
# end
|
|
40
|
+
def metrics_all
|
|
41
|
+
result = with_connection do |conn|
|
|
42
|
+
conn.exec('SELECT * FROM pgmq.metrics_all()')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
result.map { |row| PGMQ::Metrics.new(row) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|