pgmq-ruby 0.3.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/.github/workflows/ci.yml +10 -8
- data/.github/workflows/push.yml +2 -2
- data/.ruby-version +1 -1
- data/.yard-lint.yml +174 -67
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +3 -3
- data/README.md +89 -29
- data/docker-compose.yml +1 -1
- data/lib/pgmq/client/maintenance.rb +48 -0
- data/lib/pgmq/client/message_lifecycle.rb +93 -1
- data/lib/pgmq/client/producer.rb +70 -25
- data/lib/pgmq/client/queue_management.rb +28 -12
- data/lib/pgmq/client.rb +2 -2
- data/lib/pgmq/transaction.rb +4 -17
- data/lib/pgmq/version.rb +1 -1
- data/lib/pgmq.rb +1 -1
- data/renovate.json +2 -9
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6b6b3dcddd3785167a0fd8482fb0900f0d51c9c5cf68ddd23b60e84eb306012
|
|
4
|
+
data.tar.gz: 94604bf7c55a2bd62a63486278641c247b0a9d9cf3b50379d30ee0b7c8c840f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ea99c27e92f96f137c9552985830949cdff6538409646ddf6b10e587c05c2b831c8c083570996ff2c0ffbefd09ae01fa84002e2ca9093f0a27ec6f7387a2b59
|
|
7
|
+
data.tar.gz: 437713bad4d37ff821487493097c6229b8c2069b260b74e587b3452f74508392b1dba7cf37ee3dd91c52143a4c147db373035351a8dc0d86f1d6cd70a4370d4d
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
fail-fast: false
|
|
22
22
|
matrix:
|
|
23
23
|
ruby:
|
|
24
|
-
- '
|
|
24
|
+
- '4.0.0'
|
|
25
25
|
- '3.4'
|
|
26
26
|
- '3.3'
|
|
27
27
|
- '3.2'
|
|
@@ -52,7 +52,7 @@ jobs:
|
|
|
52
52
|
- 5433:5432
|
|
53
53
|
|
|
54
54
|
steps:
|
|
55
|
-
- uses: actions/checkout@
|
|
55
|
+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
56
56
|
with:
|
|
57
57
|
fetch-depth: 0
|
|
58
58
|
|
|
@@ -60,11 +60,11 @@ jobs:
|
|
|
60
60
|
run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends postgresql-client"
|
|
61
61
|
|
|
62
62
|
- name: Remove Gemfile.lock for Ruby previews
|
|
63
|
-
if: contains(matrix.ruby, '
|
|
63
|
+
if: contains(matrix.ruby, '4.0')
|
|
64
64
|
run: rm -f Gemfile.lock
|
|
65
65
|
|
|
66
66
|
- name: Set up Ruby
|
|
67
|
-
uses: ruby/setup-ruby@
|
|
67
|
+
uses: ruby/setup-ruby@ae195bbe749a7cef685ac729197124a48305c1cb # v1.276.0
|
|
68
68
|
with:
|
|
69
69
|
ruby-version: ${{ matrix.ruby }}
|
|
70
70
|
bundler-cache: true
|
|
@@ -108,13 +108,15 @@ jobs:
|
|
|
108
108
|
strategy:
|
|
109
109
|
fail-fast: false
|
|
110
110
|
steps:
|
|
111
|
-
- uses: actions/checkout@
|
|
111
|
+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
112
112
|
with:
|
|
113
113
|
fetch-depth: 0
|
|
114
|
+
- name: Remove Gemfile.lock for Ruby 4.0
|
|
115
|
+
run: rm -f Gemfile.lock
|
|
114
116
|
- name: Set up Ruby
|
|
115
|
-
uses: ruby/setup-ruby@
|
|
117
|
+
uses: ruby/setup-ruby@ae195bbe749a7cef685ac729197124a48305c1cb # v1.276.0
|
|
116
118
|
with:
|
|
117
|
-
ruby-version: '
|
|
119
|
+
ruby-version: '4.0.0'
|
|
118
120
|
bundler-cache: true
|
|
119
121
|
- name: Run yard-lint
|
|
120
122
|
run: bundle exec yard-lint lib/
|
|
@@ -125,7 +127,7 @@ jobs:
|
|
|
125
127
|
strategy:
|
|
126
128
|
fail-fast: false
|
|
127
129
|
steps:
|
|
128
|
-
- uses: actions/checkout@
|
|
130
|
+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
129
131
|
with:
|
|
130
132
|
fetch-depth: 0
|
|
131
133
|
- name: Download Coditsu script
|
data/.github/workflows/push.yml
CHANGED
|
@@ -19,12 +19,12 @@ jobs:
|
|
|
19
19
|
id-token: write
|
|
20
20
|
|
|
21
21
|
steps:
|
|
22
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
23
23
|
with:
|
|
24
24
|
fetch-depth: 0
|
|
25
25
|
|
|
26
26
|
- name: Set up Ruby
|
|
27
|
-
uses: ruby/setup-ruby@
|
|
27
|
+
uses: ruby/setup-ruby@ae195bbe749a7cef685ac729197124a48305c1cb # v1.276.0
|
|
28
28
|
with:
|
|
29
29
|
bundler-cache: false
|
|
30
30
|
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4.0.0
|
data/.yard-lint.yml
CHANGED
|
@@ -3,166 +3,273 @@
|
|
|
3
3
|
|
|
4
4
|
# Global settings for all validators
|
|
5
5
|
AllValidators:
|
|
6
|
-
# YARD command-line options (applied to all validators by default)
|
|
7
6
|
YardOptions:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# Global file exclusion patterns
|
|
7
|
+
- "--private"
|
|
8
|
+
- "--protected"
|
|
12
9
|
Exclude:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
FailOnSeverity: error
|
|
21
|
-
|
|
22
|
-
# Require 100% documentation coverage
|
|
10
|
+
- "\\.git"
|
|
11
|
+
- vendor/**/*
|
|
12
|
+
- node_modules/**/*
|
|
13
|
+
- spec/**/*
|
|
14
|
+
- test/**/*
|
|
15
|
+
- benchmark/**/*
|
|
16
|
+
FailOnSeverity: convention
|
|
23
17
|
RequiredCoverage: 100
|
|
24
18
|
|
|
25
19
|
# Documentation validators
|
|
26
20
|
Documentation/UndocumentedObjects:
|
|
27
|
-
Description:
|
|
21
|
+
Description: Checks for classes, modules, and methods without documentation.
|
|
28
22
|
Enabled: true
|
|
29
23
|
Severity: error
|
|
30
24
|
ExcludedMethods:
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
- initialize/0
|
|
26
|
+
- "/^_/"
|
|
33
27
|
|
|
34
28
|
Documentation/UndocumentedMethodArguments:
|
|
35
|
-
Description:
|
|
29
|
+
Description: Checks for method parameters without @param tags.
|
|
36
30
|
Enabled: true
|
|
37
31
|
Severity: error
|
|
38
32
|
|
|
39
33
|
Documentation/UndocumentedBooleanMethods:
|
|
40
|
-
Description:
|
|
34
|
+
Description: Checks that question mark methods document their boolean return.
|
|
41
35
|
Enabled: true
|
|
42
36
|
Severity: error
|
|
43
37
|
|
|
44
38
|
Documentation/UndocumentedOptions:
|
|
45
|
-
Description:
|
|
39
|
+
Description: Detects methods with options hash parameters but no @option tags.
|
|
46
40
|
Enabled: true
|
|
47
41
|
Severity: error
|
|
48
42
|
|
|
49
43
|
Documentation/MarkdownSyntax:
|
|
50
|
-
Description:
|
|
44
|
+
Description: Detects common markdown syntax errors in documentation.
|
|
51
45
|
Enabled: true
|
|
52
46
|
Severity: error
|
|
53
47
|
|
|
48
|
+
Documentation/EmptyCommentLine:
|
|
49
|
+
Description: Detects empty comment lines at the start or end of documentation blocks.
|
|
50
|
+
Enabled: true
|
|
51
|
+
Severity: convention
|
|
52
|
+
EnabledPatterns:
|
|
53
|
+
Leading: true
|
|
54
|
+
Trailing: true
|
|
55
|
+
|
|
56
|
+
Documentation/BlankLineBeforeDefinition:
|
|
57
|
+
Description: Detects blank lines between YARD documentation and method definition.
|
|
58
|
+
Enabled: true
|
|
59
|
+
Severity: convention
|
|
60
|
+
OrphanedSeverity: convention
|
|
61
|
+
EnabledPatterns:
|
|
62
|
+
SingleBlankLine: true
|
|
63
|
+
OrphanedDocs: true
|
|
64
|
+
|
|
54
65
|
# Tags validators
|
|
55
66
|
Tags/Order:
|
|
56
|
-
Description:
|
|
67
|
+
Description: Enforces consistent ordering of YARD tags.
|
|
57
68
|
Enabled: true
|
|
58
69
|
Severity: error
|
|
59
70
|
EnforcedOrder:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
- param
|
|
72
|
+
- option
|
|
73
|
+
- return
|
|
74
|
+
- raise
|
|
75
|
+
- example
|
|
65
76
|
|
|
66
77
|
Tags/InvalidTypes:
|
|
67
|
-
Description:
|
|
78
|
+
Description: Validates type definitions in @param, @return, @option tags.
|
|
68
79
|
Enabled: true
|
|
69
80
|
Severity: error
|
|
70
81
|
ValidatedTags:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
- param
|
|
83
|
+
- option
|
|
84
|
+
- return
|
|
74
85
|
|
|
75
86
|
Tags/TypeSyntax:
|
|
76
|
-
Description:
|
|
87
|
+
Description: Validates YARD type syntax using YARD parser.
|
|
77
88
|
Enabled: true
|
|
78
89
|
Severity: error
|
|
79
90
|
ValidatedTags:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
- param
|
|
92
|
+
- option
|
|
93
|
+
- return
|
|
94
|
+
- yieldreturn
|
|
84
95
|
|
|
85
96
|
Tags/MeaninglessTag:
|
|
86
|
-
Description:
|
|
97
|
+
Description: Detects @param/@option tags on classes, modules, or constants.
|
|
87
98
|
Enabled: true
|
|
88
99
|
Severity: error
|
|
89
100
|
CheckedTags:
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
- param
|
|
102
|
+
- option
|
|
92
103
|
InvalidObjectTypes:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
- class
|
|
105
|
+
- module
|
|
106
|
+
- constant
|
|
96
107
|
|
|
97
108
|
Tags/CollectionType:
|
|
98
|
-
Description:
|
|
109
|
+
Description: Validates Hash collection syntax consistency.
|
|
99
110
|
Enabled: true
|
|
100
111
|
Severity: error
|
|
101
|
-
EnforcedStyle: long
|
|
112
|
+
EnforcedStyle: long
|
|
102
113
|
ValidatedTags:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
- param
|
|
115
|
+
- option
|
|
116
|
+
- return
|
|
117
|
+
- yieldreturn
|
|
107
118
|
|
|
108
119
|
Tags/TagTypePosition:
|
|
109
|
-
Description:
|
|
120
|
+
Description: Validates type annotation position in tags.
|
|
110
121
|
Enabled: true
|
|
111
122
|
Severity: error
|
|
112
123
|
CheckedTags:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# EnforcedStyle: 'type_after_name' (YARD standard: @param name [Type])
|
|
116
|
-
# or 'type_first' (@param [Type] name)
|
|
124
|
+
- param
|
|
125
|
+
- option
|
|
117
126
|
EnforcedStyle: type_after_name
|
|
118
127
|
|
|
119
128
|
Tags/ApiTags:
|
|
120
|
-
Description:
|
|
121
|
-
Enabled:
|
|
129
|
+
Description: Enforces @api tags on public objects.
|
|
130
|
+
Enabled: false
|
|
122
131
|
Severity: error
|
|
123
132
|
AllowedApis:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
- public
|
|
134
|
+
- private
|
|
135
|
+
- internal
|
|
127
136
|
|
|
128
137
|
Tags/OptionTags:
|
|
129
|
-
Description:
|
|
138
|
+
Description: Requires @option tags for methods with options parameters.
|
|
130
139
|
Enabled: true
|
|
131
140
|
Severity: error
|
|
132
141
|
|
|
142
|
+
Tags/ExampleSyntax:
|
|
143
|
+
Description: Validates Ruby syntax in @example tags.
|
|
144
|
+
Enabled: true
|
|
145
|
+
Severity: warning
|
|
146
|
+
|
|
147
|
+
Tags/RedundantParamDescription:
|
|
148
|
+
Description: Detects meaningless parameter descriptions that add no value.
|
|
149
|
+
Enabled: true
|
|
150
|
+
Severity: convention
|
|
151
|
+
CheckedTags:
|
|
152
|
+
- param
|
|
153
|
+
- option
|
|
154
|
+
Articles:
|
|
155
|
+
- The
|
|
156
|
+
- the
|
|
157
|
+
- A
|
|
158
|
+
- a
|
|
159
|
+
- An
|
|
160
|
+
- an
|
|
161
|
+
MaxRedundantWords: 6
|
|
162
|
+
GenericTerms:
|
|
163
|
+
- object
|
|
164
|
+
- instance
|
|
165
|
+
- value
|
|
166
|
+
- data
|
|
167
|
+
- item
|
|
168
|
+
- element
|
|
169
|
+
EnabledPatterns:
|
|
170
|
+
ArticleParam: true
|
|
171
|
+
PossessiveParam: true
|
|
172
|
+
TypeRestatement: true
|
|
173
|
+
ParamToVerb: true
|
|
174
|
+
IdPattern: true
|
|
175
|
+
DirectionalDate: true
|
|
176
|
+
TypeGeneric: true
|
|
177
|
+
|
|
178
|
+
Tags/InformalNotation:
|
|
179
|
+
Description: Detects informal tag notation patterns like "Note:" instead of @note.
|
|
180
|
+
Enabled: true
|
|
181
|
+
Severity: warning
|
|
182
|
+
CaseSensitive: false
|
|
183
|
+
RequireStartOfLine: true
|
|
184
|
+
Patterns:
|
|
185
|
+
Note: "@note"
|
|
186
|
+
Todo: "@todo"
|
|
187
|
+
TODO: "@todo"
|
|
188
|
+
FIXME: "@todo"
|
|
189
|
+
See: "@see"
|
|
190
|
+
See also: "@see"
|
|
191
|
+
Warning: "@deprecated"
|
|
192
|
+
Deprecated: "@deprecated"
|
|
193
|
+
Author: "@author"
|
|
194
|
+
Version: "@version"
|
|
195
|
+
Since: "@since"
|
|
196
|
+
Returns: "@return"
|
|
197
|
+
Raises: "@raise"
|
|
198
|
+
Example: "@example"
|
|
199
|
+
|
|
200
|
+
Tags/NonAsciiType:
|
|
201
|
+
Description: Detects non-ASCII characters in type annotations.
|
|
202
|
+
Enabled: true
|
|
203
|
+
Severity: warning
|
|
204
|
+
ValidatedTags:
|
|
205
|
+
- param
|
|
206
|
+
- option
|
|
207
|
+
- return
|
|
208
|
+
- yieldreturn
|
|
209
|
+
- yieldparam
|
|
210
|
+
|
|
211
|
+
Tags/TagGroupSeparator:
|
|
212
|
+
Description: Enforces blank line separators between different YARD tag groups.
|
|
213
|
+
Enabled: false
|
|
214
|
+
Severity: convention
|
|
215
|
+
TagGroups:
|
|
216
|
+
param:
|
|
217
|
+
- param
|
|
218
|
+
- option
|
|
219
|
+
return:
|
|
220
|
+
- return
|
|
221
|
+
error:
|
|
222
|
+
- raise
|
|
223
|
+
- throws
|
|
224
|
+
example:
|
|
225
|
+
- example
|
|
226
|
+
meta:
|
|
227
|
+
- see
|
|
228
|
+
- note
|
|
229
|
+
- todo
|
|
230
|
+
- deprecated
|
|
231
|
+
- since
|
|
232
|
+
- version
|
|
233
|
+
- api
|
|
234
|
+
yield:
|
|
235
|
+
- yield
|
|
236
|
+
- yieldparam
|
|
237
|
+
- yieldreturn
|
|
238
|
+
RequireAfterDescription: false
|
|
239
|
+
|
|
133
240
|
# Warnings validators - catches YARD parser errors
|
|
134
241
|
Warnings/UnknownTag:
|
|
135
|
-
Description:
|
|
242
|
+
Description: Detects unknown YARD tags.
|
|
136
243
|
Enabled: true
|
|
137
244
|
Severity: error
|
|
138
245
|
|
|
139
246
|
Warnings/UnknownDirective:
|
|
140
|
-
Description:
|
|
247
|
+
Description: Detects unknown YARD directives.
|
|
141
248
|
Enabled: true
|
|
142
249
|
Severity: error
|
|
143
250
|
|
|
144
251
|
Warnings/InvalidTagFormat:
|
|
145
|
-
Description:
|
|
252
|
+
Description: Detects malformed tag syntax.
|
|
146
253
|
Enabled: true
|
|
147
254
|
Severity: error
|
|
148
255
|
|
|
149
256
|
Warnings/InvalidDirectiveFormat:
|
|
150
|
-
Description:
|
|
257
|
+
Description: Detects malformed directive syntax.
|
|
151
258
|
Enabled: true
|
|
152
259
|
Severity: error
|
|
153
260
|
|
|
154
261
|
Warnings/DuplicatedParameterName:
|
|
155
|
-
Description:
|
|
262
|
+
Description: Detects duplicate @param tags.
|
|
156
263
|
Enabled: true
|
|
157
264
|
Severity: error
|
|
158
265
|
|
|
159
266
|
Warnings/UnknownParameterName:
|
|
160
|
-
Description:
|
|
267
|
+
Description: Detects @param tags for non-existent parameters.
|
|
161
268
|
Enabled: true
|
|
162
269
|
Severity: error
|
|
163
270
|
|
|
164
271
|
# Semantic validators
|
|
165
272
|
Semantic/AbstractMethods:
|
|
166
|
-
Description:
|
|
273
|
+
Description: Ensures @abstract methods do not have real implementations.
|
|
167
274
|
Enabled: true
|
|
168
275
|
Severity: error
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 (2025-12-26)
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
- **[Breaking]** Rename `send` to `produce` and `send_batch` to `produce_batch`. This avoids shadowing Ruby's built-in `Object#send` method which caused confusion and required workarounds (e.g., using `__send__`). The new names also align better with the producer/consumer terminology used in message queue systems.
|
|
7
|
+
|
|
8
|
+
### Queue Management
|
|
9
|
+
- [Enhancement] `create`, `create_partitioned`, and `create_unlogged` now return `true` if the queue was newly created, `false` if it already existed. This provides clearer feedback and aligns with the Rust PGMQ client behavior.
|
|
10
|
+
|
|
11
|
+
### Message Operations
|
|
12
|
+
- **[Feature]** Add `headers:` parameter to `produce(queue_name, message, headers:, delay:)` for message metadata (routing, tracing, correlation IDs).
|
|
13
|
+
- **[Feature]** Add `headers:` parameter to `produce_batch(queue_name, messages, headers:, delay:)` for batch message metadata.
|
|
14
|
+
- **[Feature]** Introduce `pop_batch(queue_name, qty)` for atomic batch pop (read + delete) operations.
|
|
15
|
+
- **[Feature]** Introduce `set_vt_batch(queue_name, msg_ids, vt_offset:)` for batch visibility timeout updates.
|
|
16
|
+
- **[Feature]** Introduce `set_vt_multi(updates_hash, vt_offset:)` for updating visibility timeouts across multiple queues atomically.
|
|
17
|
+
|
|
18
|
+
### Notifications
|
|
19
|
+
- **[Feature]** Introduce `enable_notify_insert(queue_name, throttle_interval_ms:)` for PostgreSQL LISTEN/NOTIFY support.
|
|
20
|
+
- **[Feature]** Introduce `disable_notify_insert(queue_name)` to disable notifications.
|
|
21
|
+
|
|
22
|
+
### Compatibility
|
|
23
|
+
- [Enhancement] Add Ruby 4.0.0 support with full CI testing.
|
|
24
|
+
|
|
3
25
|
## 0.3.0 (2025-11-14)
|
|
4
26
|
|
|
5
27
|
Initial release of pgmq-ruby - a low-level Ruby client for PGMQ (PostgreSQL Message Queue).
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
pgmq-ruby (0.
|
|
4
|
+
pgmq-ruby (0.4.0)
|
|
5
5
|
connection_pool (~> 2.4)
|
|
6
6
|
pg (~> 1.5)
|
|
7
7
|
zeitwerk (~> 2.6)
|
|
@@ -39,8 +39,8 @@ GEM
|
|
|
39
39
|
simplecov_json_formatter (~> 0.1)
|
|
40
40
|
simplecov-html (0.13.2)
|
|
41
41
|
simplecov_json_formatter (0.1.4)
|
|
42
|
-
yard (0.9.
|
|
43
|
-
yard-lint (1.
|
|
42
|
+
yard (0.9.38)
|
|
43
|
+
yard-lint (1.3.0)
|
|
44
44
|
yard (~> 0.9)
|
|
45
45
|
zeitwerk (~> 2.6)
|
|
46
46
|
zeitwerk (2.7.3)
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# PGMQ-Ruby
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/pgmq-ruby)
|
|
4
|
-
[](https://github.com/mensfeld/pgmq-ruby/actions)
|
|
5
5
|
|
|
6
6
|
**Ruby client for [PGMQ](https://github.com/pgmq/pgmq) - PostgreSQL Message Queue**
|
|
7
7
|
|
|
@@ -38,12 +38,13 @@ This gem provides complete support for all core PGMQ SQL functions. Based on the
|
|
|
38
38
|
|
|
39
39
|
| Category | Method | Description | Status |
|
|
40
40
|
|----------|--------|-------------|--------|
|
|
41
|
-
| **
|
|
42
|
-
| | `
|
|
41
|
+
| **Producing** | `produce` | Send single message with optional delay and headers | ✅ |
|
|
42
|
+
| | `produce_batch` | Send multiple messages atomically with headers | ✅ |
|
|
43
43
|
| **Reading** | `read` | Read single message with visibility timeout | ✅ |
|
|
44
44
|
| | `read_batch` | Read multiple messages with visibility timeout | ✅ |
|
|
45
45
|
| | `read_with_poll` | Long-polling for efficient message consumption | ✅ |
|
|
46
46
|
| | `pop` | Atomic read + delete operation | ✅ |
|
|
47
|
+
| | `pop_batch` | Atomic batch read + delete operation | ✅ |
|
|
47
48
|
| **Deleting/Archiving** | `delete` | Delete single message | ✅ |
|
|
48
49
|
| | `delete_batch` | Delete multiple messages | ✅ |
|
|
49
50
|
| | `archive` | Archive single message for long-term storage | ✅ |
|
|
@@ -55,9 +56,13 @@ This gem provides complete support for all core PGMQ SQL functions. Based on the
|
|
|
55
56
|
| | `drop_queue` | Delete queue and all messages | ✅ |
|
|
56
57
|
| | `detach_archive` | Detach archive table from queue | ✅ |
|
|
57
58
|
| **Utilities** | `set_vt` | Update message visibility timeout | ✅ |
|
|
59
|
+
| | `set_vt_batch` | Batch update visibility timeouts | ✅ |
|
|
60
|
+
| | `set_vt_multi` | Update visibility timeouts across multiple queues | ✅ |
|
|
58
61
|
| | `list_queues` | List all queues with metadata | ✅ |
|
|
59
62
|
| | `metrics` | Get queue metrics (length, age, total messages) | ✅ |
|
|
60
63
|
| | `metrics_all` | Get metrics for all queues | ✅ |
|
|
64
|
+
| | `enable_notify_insert` | Enable PostgreSQL NOTIFY on insert | ✅ |
|
|
65
|
+
| | `disable_notify_insert` | Disable notifications | ✅ |
|
|
61
66
|
| **Ruby Enhancements** | Transaction Support | Atomic operations via `client.transaction do \|txn\|` | ✅ |
|
|
62
67
|
| | Conditional Filtering | Server-side JSONB filtering with `conditional:` | ✅ |
|
|
63
68
|
| | Multi-Queue Ops | Read/pop/delete/archive from multiple queues | ✅ |
|
|
@@ -104,7 +109,7 @@ client = PGMQ::Client.new(
|
|
|
104
109
|
client.create('orders')
|
|
105
110
|
|
|
106
111
|
# Send a message (must be JSON string)
|
|
107
|
-
msg_id = client.
|
|
112
|
+
msg_id = client.produce('orders', '{"order_id":123,"total":99.99}')
|
|
108
113
|
|
|
109
114
|
# Read a message (30 second visibility timeout)
|
|
110
115
|
msg = client.read('orders', vt: 30)
|
|
@@ -224,20 +229,21 @@ client = PGMQ::Client.new(
|
|
|
224
229
|
### Queue Management
|
|
225
230
|
|
|
226
231
|
```ruby
|
|
227
|
-
# Create a queue
|
|
228
|
-
client.create("queue_name")
|
|
232
|
+
# Create a queue (returns true if created, false if already exists)
|
|
233
|
+
client.create("queue_name") # => true
|
|
234
|
+
client.create("queue_name") # => false (idempotent)
|
|
229
235
|
|
|
230
236
|
# Create partitioned queue (requires pg_partman)
|
|
231
237
|
client.create_partitioned("queue_name",
|
|
232
238
|
partition_interval: "daily",
|
|
233
239
|
retention_interval: "7 days"
|
|
234
|
-
)
|
|
240
|
+
) # => true/false
|
|
235
241
|
|
|
236
242
|
# Create unlogged queue (faster, no crash recovery)
|
|
237
|
-
client.create_unlogged("queue_name")
|
|
243
|
+
client.create_unlogged("queue_name") # => true/false
|
|
238
244
|
|
|
239
|
-
# Drop queue
|
|
240
|
-
client.drop_queue("queue_name")
|
|
245
|
+
# Drop queue (returns true if dropped, false if didn't exist)
|
|
246
|
+
client.drop_queue("queue_name") # => true/false
|
|
241
247
|
|
|
242
248
|
# List all queues
|
|
243
249
|
queues = client.list_queues
|
|
@@ -277,18 +283,32 @@ client.create("a" * 48) # ✗ Too long (48+ chars)
|
|
|
277
283
|
|
|
278
284
|
```ruby
|
|
279
285
|
# Send single message (must be JSON string)
|
|
280
|
-
msg_id = client.
|
|
286
|
+
msg_id = client.produce("queue_name", '{"data":"value"}')
|
|
281
287
|
|
|
282
288
|
# Send with delay (seconds)
|
|
283
|
-
msg_id = client.
|
|
289
|
+
msg_id = client.produce("queue_name", '{"data":"value"}', delay: 60)
|
|
290
|
+
|
|
291
|
+
# Send with headers (for routing, tracing, correlation)
|
|
292
|
+
msg_id = client.produce("queue_name", '{"data":"value"}',
|
|
293
|
+
headers: '{"trace_id":"abc123","priority":"high"}')
|
|
294
|
+
|
|
295
|
+
# Send with headers and delay
|
|
296
|
+
msg_id = client.produce("queue_name", '{"data":"value"}',
|
|
297
|
+
headers: '{"correlation_id":"req-456"}',
|
|
298
|
+
delay: 60)
|
|
284
299
|
|
|
285
300
|
# Send batch (array of JSON strings)
|
|
286
|
-
msg_ids = client.
|
|
301
|
+
msg_ids = client.produce_batch("queue_name", [
|
|
287
302
|
'{"order":1}',
|
|
288
303
|
'{"order":2}',
|
|
289
304
|
'{"order":3}'
|
|
290
305
|
])
|
|
291
306
|
# => ["101", "102", "103"]
|
|
307
|
+
|
|
308
|
+
# Send batch with headers (one per message)
|
|
309
|
+
msg_ids = client.produce_batch("queue_name",
|
|
310
|
+
['{"order":1}', '{"order":2}'],
|
|
311
|
+
headers: ['{"priority":"high"}', '{"priority":"low"}'])
|
|
292
312
|
```
|
|
293
313
|
|
|
294
314
|
### Reading Messages
|
|
@@ -311,6 +331,9 @@ msg = client.read_with_poll("queue_name",
|
|
|
311
331
|
|
|
312
332
|
# Pop (atomic read + delete)
|
|
313
333
|
msg = client.pop("queue_name")
|
|
334
|
+
|
|
335
|
+
# Pop batch (atomic read + delete for multiple messages)
|
|
336
|
+
messages = client.pop_batch("queue_name", 10)
|
|
314
337
|
```
|
|
315
338
|
|
|
316
339
|
#### Conditional Message Filtering
|
|
@@ -378,8 +401,23 @@ archived_ids = client.archive_batch("queue_name", [101, 102, 103])
|
|
|
378
401
|
# Update visibility timeout
|
|
379
402
|
msg = client.set_vt("queue_name", msg_id, vt_offset: 60)
|
|
380
403
|
|
|
404
|
+
# Batch update visibility timeout
|
|
405
|
+
updated_msgs = client.set_vt_batch("queue_name", [101, 102, 103], vt_offset: 60)
|
|
406
|
+
|
|
407
|
+
# Update visibility timeout across multiple queues
|
|
408
|
+
client.set_vt_multi({
|
|
409
|
+
"orders" => [1, 2, 3],
|
|
410
|
+
"notifications" => [5, 6]
|
|
411
|
+
}, vt_offset: 120)
|
|
412
|
+
|
|
381
413
|
# Purge all messages
|
|
382
414
|
count = client.purge_queue("queue_name")
|
|
415
|
+
|
|
416
|
+
# Enable PostgreSQL NOTIFY for a queue (for LISTEN-based consumers)
|
|
417
|
+
client.enable_notify_insert("queue_name", throttle_interval_ms: 250)
|
|
418
|
+
|
|
419
|
+
# Disable notifications
|
|
420
|
+
client.disable_notify_insert("queue_name")
|
|
383
421
|
```
|
|
384
422
|
|
|
385
423
|
### Monitoring
|
|
@@ -409,9 +447,9 @@ Execute atomic operations across multiple queues or combine queue operations wit
|
|
|
409
447
|
# Atomic operations across multiple queues
|
|
410
448
|
client.transaction do |txn|
|
|
411
449
|
# Send to multiple queues atomically
|
|
412
|
-
txn.
|
|
413
|
-
txn.
|
|
414
|
-
txn.
|
|
450
|
+
txn.produce("orders", '{"order_id":123}')
|
|
451
|
+
txn.produce("notifications", '{"user_id":456,"type":"order_created"}')
|
|
452
|
+
txn.produce("analytics", '{"event":"order_placed"}')
|
|
415
453
|
end
|
|
416
454
|
|
|
417
455
|
# Process message and update application state atomically
|
|
@@ -431,8 +469,8 @@ end
|
|
|
431
469
|
|
|
432
470
|
# Automatic rollback on errors
|
|
433
471
|
client.transaction do |txn|
|
|
434
|
-
txn.
|
|
435
|
-
txn.
|
|
472
|
+
txn.produce("queue1", '{"data":"message1"}')
|
|
473
|
+
txn.produce("queue2", '{"data":"message2"}')
|
|
436
474
|
|
|
437
475
|
raise "Something went wrong!"
|
|
438
476
|
# Both messages are rolled back - neither queue receives anything
|
|
@@ -446,7 +484,7 @@ client.transaction do |txn|
|
|
|
446
484
|
data = JSON.parse(msg.message)
|
|
447
485
|
if data["priority"] == "high"
|
|
448
486
|
# Move to high-priority queue
|
|
449
|
-
txn.
|
|
487
|
+
txn.produce("priority_orders", msg.message)
|
|
450
488
|
txn.delete("pending_orders", msg.msg_id)
|
|
451
489
|
end
|
|
452
490
|
end
|
|
@@ -503,21 +541,43 @@ enqueued = Time.parse(msg.enqueued_at) # => 2025-01-15 10:30:00 UTC
|
|
|
503
541
|
|
|
504
542
|
### Message Headers
|
|
505
543
|
|
|
506
|
-
PGMQ supports optional message headers via the `headers` JSONB column:
|
|
544
|
+
PGMQ supports optional message headers via the `headers` JSONB column. Headers are useful for metadata like routing information, correlation IDs, and distributed tracing:
|
|
507
545
|
|
|
508
546
|
```ruby
|
|
509
|
-
# Sending with headers
|
|
510
|
-
|
|
547
|
+
# Sending a message with headers
|
|
548
|
+
message = '{"order_id":123}'
|
|
549
|
+
headers = '{"trace_id":"abc123","priority":"high","correlation_id":"req-456"}'
|
|
550
|
+
|
|
551
|
+
msg_id = client.produce("orders", message, headers: headers)
|
|
552
|
+
|
|
553
|
+
# Sending with headers and delay
|
|
554
|
+
msg_id = client.produce("orders", message, headers: headers, delay: 60)
|
|
555
|
+
|
|
556
|
+
# Batch produce with headers (one header object per message)
|
|
557
|
+
messages = ['{"id":1}', '{"id":2}', '{"id":3}']
|
|
558
|
+
headers = [
|
|
559
|
+
'{"priority":"high"}',
|
|
560
|
+
'{"priority":"medium"}',
|
|
561
|
+
'{"priority":"low"}'
|
|
562
|
+
]
|
|
563
|
+
msg_ids = client.produce_batch("orders", messages, headers: headers)
|
|
511
564
|
|
|
512
565
|
# Reading messages with headers
|
|
513
|
-
msg = client.read("
|
|
566
|
+
msg = client.read("orders", vt: 30)
|
|
514
567
|
if msg.headers
|
|
515
568
|
metadata = JSON.parse(msg.headers)
|
|
516
569
|
trace_id = metadata["trace_id"]
|
|
570
|
+
priority = metadata["priority"]
|
|
517
571
|
correlation_id = metadata["correlation_id"]
|
|
518
572
|
end
|
|
519
573
|
```
|
|
520
574
|
|
|
575
|
+
Common header use cases:
|
|
576
|
+
- **Distributed tracing**: `trace_id`, `span_id`, `parent_span_id`
|
|
577
|
+
- **Request correlation**: `correlation_id`, `causation_id`
|
|
578
|
+
- **Routing**: `priority`, `region`, `tenant_id`
|
|
579
|
+
- **Content metadata**: `content_type`, `encoding`, `version`
|
|
580
|
+
|
|
521
581
|
### Why Raw Values?
|
|
522
582
|
|
|
523
583
|
This library follows the **rdkafka-ruby philosophy** - provide a thin, performant wrapper around the underlying system:
|
|
@@ -538,14 +598,14 @@ PGMQ stores messages as JSONB in PostgreSQL. You must handle JSON serialization
|
|
|
538
598
|
```ruby
|
|
539
599
|
# Simple hash
|
|
540
600
|
msg = { order_id: 123, status: "pending" }
|
|
541
|
-
client.
|
|
601
|
+
client.produce("orders", msg.to_json)
|
|
542
602
|
|
|
543
603
|
# Using JSON.generate for explicit control
|
|
544
|
-
client.
|
|
604
|
+
client.produce("orders", JSON.generate(order_id: 123, status: "pending"))
|
|
545
605
|
|
|
546
606
|
# Pre-serialized JSON string
|
|
547
607
|
json_str = '{"order_id":123,"status":"pending"}'
|
|
548
|
-
client.
|
|
608
|
+
client.produce("orders", json_str)
|
|
549
609
|
```
|
|
550
610
|
|
|
551
611
|
### Reading Messages
|
|
@@ -577,8 +637,8 @@ class QueueHelper
|
|
|
577
637
|
@client = client
|
|
578
638
|
end
|
|
579
639
|
|
|
580
|
-
def
|
|
581
|
-
@client.
|
|
640
|
+
def produce(queue, data)
|
|
641
|
+
@client.produce(queue, data.to_json)
|
|
582
642
|
end
|
|
583
643
|
|
|
584
644
|
def read(queue, vt:)
|
|
@@ -595,7 +655,7 @@ class QueueHelper
|
|
|
595
655
|
end
|
|
596
656
|
|
|
597
657
|
helper = QueueHelper.new(client)
|
|
598
|
-
helper.
|
|
658
|
+
helper.produce("orders", { order_id: 123 })
|
|
599
659
|
msg = helper.read("orders", vt: 30)
|
|
600
660
|
puts msg.data["order_id"] # => 123
|
|
601
661
|
```
|
data/docker-compose.yml
CHANGED
|
@@ -41,6 +41,54 @@ module PGMQ
|
|
|
41
41
|
|
|
42
42
|
nil
|
|
43
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
|
|
44
92
|
end
|
|
45
93
|
end
|
|
46
94
|
end
|
|
@@ -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
|
|
@@ -212,7 +232,7 @@ module PGMQ
|
|
|
212
232
|
# @param queue_name [String] name of the queue
|
|
213
233
|
# @param msg_id [Integer] message ID
|
|
214
234
|
# @param vt_offset [Integer] visibility timeout offset in seconds
|
|
215
|
-
# @return [PGMQ::Message] updated message
|
|
235
|
+
# @return [PGMQ::Message, nil] updated message or nil if not found
|
|
216
236
|
#
|
|
217
237
|
# @example
|
|
218
238
|
# # Extend processing time by 60 more seconds
|
|
@@ -235,6 +255,78 @@ module PGMQ
|
|
|
235
255
|
|
|
236
256
|
Message.new(result[0])
|
|
237
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
|
|
238
330
|
end
|
|
239
331
|
end
|
|
240
332
|
end
|
data/lib/pgmq/client/producer.rb
CHANGED
|
@@ -2,75 +2,120 @@
|
|
|
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
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
121
|
result.map { |row| row['send_batch'] }
|
|
@@ -10,20 +10,21 @@ module PGMQ
|
|
|
10
10
|
# Creates a new queue
|
|
11
11
|
#
|
|
12
12
|
# @param queue_name [String] name of the queue to create
|
|
13
|
-
# @return [
|
|
13
|
+
# @return [Boolean] true if queue was created, false if it already existed
|
|
14
14
|
# @raise [PGMQ::Errors::InvalidQueueNameError] if queue name is invalid
|
|
15
15
|
# @raise [PGMQ::Errors::ConnectionError] if database operation fails
|
|
16
16
|
#
|
|
17
17
|
# @example
|
|
18
|
-
# client.create("orders")
|
|
18
|
+
# client.create("orders") # => true (created)
|
|
19
|
+
# client.create("orders") # => false (already exists)
|
|
19
20
|
def create(queue_name)
|
|
20
21
|
validate_queue_name!(queue_name)
|
|
21
22
|
|
|
22
23
|
with_connection do |conn|
|
|
24
|
+
existed = queue_exists?(conn, queue_name)
|
|
23
25
|
conn.exec_params('SELECT pgmq.create($1::text)', [queue_name])
|
|
26
|
+
!existed
|
|
24
27
|
end
|
|
25
|
-
|
|
26
|
-
nil
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
# Creates a partitioned queue
|
|
@@ -33,13 +34,13 @@ module PGMQ
|
|
|
33
34
|
# @param queue_name [String] name of the queue
|
|
34
35
|
# @param partition_interval [String] partition interval (e.g., "daily", "10000")
|
|
35
36
|
# @param retention_interval [String] retention interval (e.g., "7 days", "100000")
|
|
36
|
-
# @return [
|
|
37
|
+
# @return [Boolean] true if queue was created, false if it already existed
|
|
37
38
|
#
|
|
38
39
|
# @example
|
|
39
40
|
# client.create_partitioned("big_queue",
|
|
40
41
|
# partition_interval: "daily",
|
|
41
42
|
# retention_interval: "7 days"
|
|
42
|
-
# )
|
|
43
|
+
# ) # => true
|
|
43
44
|
def create_partitioned(
|
|
44
45
|
queue_name,
|
|
45
46
|
partition_interval: '10000',
|
|
@@ -48,30 +49,30 @@ module PGMQ
|
|
|
48
49
|
validate_queue_name!(queue_name)
|
|
49
50
|
|
|
50
51
|
with_connection do |conn|
|
|
52
|
+
existed = queue_exists?(conn, queue_name)
|
|
51
53
|
conn.exec_params(
|
|
52
54
|
'SELECT pgmq.create_partitioned($1::text, $2::text, $3::text)',
|
|
53
55
|
[queue_name, partition_interval, retention_interval]
|
|
54
56
|
)
|
|
57
|
+
!existed
|
|
55
58
|
end
|
|
56
|
-
|
|
57
|
-
nil
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
# Creates an unlogged queue for higher throughput (no crash recovery)
|
|
61
62
|
#
|
|
62
63
|
# @param queue_name [String] name of the queue
|
|
63
|
-
# @return [
|
|
64
|
+
# @return [Boolean] true if queue was created, false if it already existed
|
|
64
65
|
#
|
|
65
66
|
# @example
|
|
66
|
-
# client.create_unlogged("fast_queue")
|
|
67
|
+
# client.create_unlogged("fast_queue") # => true
|
|
67
68
|
def create_unlogged(queue_name)
|
|
68
69
|
validate_queue_name!(queue_name)
|
|
69
70
|
|
|
70
71
|
with_connection do |conn|
|
|
72
|
+
existed = queue_exists?(conn, queue_name)
|
|
71
73
|
conn.exec_params('SELECT pgmq.create_unlogged($1::text)', [queue_name])
|
|
74
|
+
!existed
|
|
72
75
|
end
|
|
73
|
-
|
|
74
|
-
nil
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
# Drops a queue and its archive table
|
|
@@ -107,6 +108,21 @@ module PGMQ
|
|
|
107
108
|
|
|
108
109
|
result.map { |row| QueueMetadata.new(row) }
|
|
109
110
|
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Checks if a queue exists in the pgmq.meta table
|
|
115
|
+
#
|
|
116
|
+
# @param conn [PG::Connection] database connection
|
|
117
|
+
# @param queue_name [String] name of the queue to check
|
|
118
|
+
# @return [Boolean] true if queue exists, false otherwise
|
|
119
|
+
def queue_exists?(conn, queue_name)
|
|
120
|
+
result = conn.exec_params(
|
|
121
|
+
'SELECT 1 FROM pgmq.meta WHERE queue_name = $1 LIMIT 1',
|
|
122
|
+
[queue_name]
|
|
123
|
+
)
|
|
124
|
+
result.ntuples.positive?
|
|
125
|
+
end
|
|
110
126
|
end
|
|
111
127
|
end
|
|
112
128
|
end
|
data/lib/pgmq/client.rb
CHANGED
|
@@ -14,7 +14,7 @@ module PGMQ
|
|
|
14
14
|
# password: 'postgres'
|
|
15
15
|
# )
|
|
16
16
|
# client.create('my_queue')
|
|
17
|
-
# msg_id = client.
|
|
17
|
+
# msg_id = client.produce('my_queue', '{"data":"value"}')
|
|
18
18
|
# msg = client.read('my_queue', vt: 30)
|
|
19
19
|
# client.delete('my_queue', msg.msg_id)
|
|
20
20
|
#
|
|
@@ -27,7 +27,7 @@ module PGMQ
|
|
|
27
27
|
# Include functional modules (order matters for discoverability)
|
|
28
28
|
include Transaction # Transaction support (already existed)
|
|
29
29
|
include QueueManagement # Queue lifecycle (create, drop, list)
|
|
30
|
-
include Producer # Message
|
|
30
|
+
include Producer # Message producing operations
|
|
31
31
|
include Consumer # Single-queue reading operations
|
|
32
32
|
include MultiQueue # Multi-queue operations
|
|
33
33
|
include MessageLifecycle # Message state transitions (pop, delete, archive)
|
data/lib/pgmq/transaction.rb
CHANGED
|
@@ -12,13 +12,13 @@ module PGMQ
|
|
|
12
12
|
#
|
|
13
13
|
# @example Atomic multi-queue operations
|
|
14
14
|
# client.transaction do |txn|
|
|
15
|
-
# txn.
|
|
16
|
-
# txn.
|
|
15
|
+
# txn.produce("orders", '{"order_id":123}')
|
|
16
|
+
# txn.produce("notifications", '{"type":"order_created"}')
|
|
17
17
|
# end
|
|
18
18
|
#
|
|
19
19
|
# @example Automatic rollback on error
|
|
20
20
|
# client.transaction do |txn|
|
|
21
|
-
# txn.
|
|
21
|
+
# txn.produce("orders", '{"order_id":123}')
|
|
22
22
|
# raise "Error" # Both operations rolled back
|
|
23
23
|
# end
|
|
24
24
|
module Transaction
|
|
@@ -33,7 +33,7 @@ module PGMQ
|
|
|
33
33
|
#
|
|
34
34
|
# @example
|
|
35
35
|
# client.transaction do |txn|
|
|
36
|
-
# msg_id = txn.
|
|
36
|
+
# msg_id = txn.produce("queue", '{"data":"test"}')
|
|
37
37
|
# txn.delete("queue", msg_id)
|
|
38
38
|
# end
|
|
39
39
|
def transaction
|
|
@@ -67,19 +67,6 @@ module PGMQ
|
|
|
67
67
|
@parent.respond_to?(method, true) ? @parent.__send__(method, ...) : super
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
# Override Object#send to call parent's send method
|
|
71
|
-
# @param queue_name [String] queue name
|
|
72
|
-
# @param message [String] message as JSON string
|
|
73
|
-
# @param delay [Integer] delay in seconds
|
|
74
|
-
# @return [String] message ID
|
|
75
|
-
def send(
|
|
76
|
-
queue_name,
|
|
77
|
-
message,
|
|
78
|
-
delay: 0
|
|
79
|
-
)
|
|
80
|
-
@parent.send(queue_name, message, delay: delay)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
70
|
# Check if method exists on parent
|
|
84
71
|
# @param method [Symbol] method name
|
|
85
72
|
# @param include_private [Boolean] include private methods
|
data/lib/pgmq/version.rb
CHANGED
data/lib/pgmq.rb
CHANGED
|
@@ -33,7 +33,7 @@ loader.eager_load
|
|
|
33
33
|
#
|
|
34
34
|
# # Basic queue operations
|
|
35
35
|
# client.create('orders')
|
|
36
|
-
# msg_id = client.
|
|
36
|
+
# msg_id = client.produce('orders', '{"order_id":123}')
|
|
37
37
|
# msg = client.read('orders', vt: 30)
|
|
38
38
|
# client.delete('orders', msg.msg_id)
|
|
39
39
|
# client.drop_queue('orders')
|
data/renovate.json
CHANGED
|
@@ -3,16 +3,9 @@
|
|
|
3
3
|
"extends": [
|
|
4
4
|
"config:recommended"
|
|
5
5
|
],
|
|
6
|
+
"minimumReleaseAge": "7 days",
|
|
6
7
|
"github-actions": {
|
|
7
8
|
"enabled": true,
|
|
8
9
|
"pinDigests": true
|
|
9
|
-
}
|
|
10
|
-
"packageRules": [
|
|
11
|
-
{
|
|
12
|
-
"matchManagers": [
|
|
13
|
-
"github-actions"
|
|
14
|
-
],
|
|
15
|
-
"minimumReleaseAge": "7 days"
|
|
16
|
-
}
|
|
17
|
-
]
|
|
10
|
+
}
|
|
18
11
|
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgmq-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maciej Mensfeld
|
|
@@ -115,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
115
115
|
- !ruby/object:Gem::Version
|
|
116
116
|
version: '0'
|
|
117
117
|
requirements: []
|
|
118
|
-
rubygems_version:
|
|
118
|
+
rubygems_version: 4.0.3
|
|
119
119
|
specification_version: 4
|
|
120
120
|
summary: Ruby client for PGMQ (Postgres Message Queue)
|
|
121
121
|
test_files: []
|