pgmq-ruby 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +42 -22
- data/.github/workflows/push.yml +1 -1
- data/.rspec +1 -0
- data/.rubocop.yml +66 -0
- data/.yard-lint.yml +1 -3
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +310 -0
- data/Gemfile +5 -5
- data/Gemfile.lint +16 -0
- data/Gemfile.lint.lock +120 -0
- data/Gemfile.lock +20 -6
- data/README.md +213 -10
- data/Rakefile +71 -2
- data/docker-compose.yml +2 -2
- data/lib/pgmq/client/consumer.rb +80 -7
- data/lib/pgmq/client/maintenance.rb +4 -21
- data/lib/pgmq/client/message_lifecycle.rb +69 -44
- data/lib/pgmq/client/metrics.rb +2 -2
- data/lib/pgmq/client/multi_queue.rb +9 -9
- data/lib/pgmq/client/producer.rb +7 -7
- data/lib/pgmq/client/queue_management.rb +9 -9
- data/lib/pgmq/client/topics.rb +268 -0
- data/lib/pgmq/client.rb +13 -12
- data/lib/pgmq/connection.rb +11 -11
- data/lib/pgmq/message.rb +11 -9
- data/lib/pgmq/metrics.rb +7 -7
- data/lib/pgmq/queue_metadata.rb +7 -7
- data/lib/pgmq/version.rb +1 -1
- data/lib/pgmq.rb +3 -3
- data/package-lock.json +331 -0
- data/package.json +9 -0
- data/pgmq-ruby.gemspec +20 -20
- data/renovate.json +20 -1
- metadata +8 -2
- data/.coditsu/ci.yml +0 -3
data/Gemfile.lint.lock
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
GEM
|
|
2
|
+
remote: https://rubygems.org/
|
|
3
|
+
specs:
|
|
4
|
+
ast (2.4.3)
|
|
5
|
+
json (2.18.1)
|
|
6
|
+
language_server-protocol (3.17.0.5)
|
|
7
|
+
lint_roller (1.1.0)
|
|
8
|
+
parallel (1.27.0)
|
|
9
|
+
parser (3.3.10.2)
|
|
10
|
+
ast (~> 2.4.1)
|
|
11
|
+
racc
|
|
12
|
+
prism (1.9.0)
|
|
13
|
+
racc (1.8.1)
|
|
14
|
+
rainbow (3.1.1)
|
|
15
|
+
regexp_parser (2.11.3)
|
|
16
|
+
rubocop (1.84.2)
|
|
17
|
+
json (~> 2.3)
|
|
18
|
+
language_server-protocol (~> 3.17.0.2)
|
|
19
|
+
lint_roller (~> 1.1.0)
|
|
20
|
+
parallel (~> 1.10)
|
|
21
|
+
parser (>= 3.3.0.2)
|
|
22
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
23
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
24
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
25
|
+
ruby-progressbar (~> 1.7)
|
|
26
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
27
|
+
rubocop-ast (1.49.0)
|
|
28
|
+
parser (>= 3.3.7.2)
|
|
29
|
+
prism (~> 1.7)
|
|
30
|
+
rubocop-capybara (2.22.1)
|
|
31
|
+
lint_roller (~> 1.1)
|
|
32
|
+
rubocop (~> 1.72, >= 1.72.1)
|
|
33
|
+
rubocop-factory_bot (2.28.0)
|
|
34
|
+
lint_roller (~> 1.1)
|
|
35
|
+
rubocop (~> 1.72, >= 1.72.1)
|
|
36
|
+
rubocop-performance (1.26.1)
|
|
37
|
+
lint_roller (~> 1.1)
|
|
38
|
+
rubocop (>= 1.75.0, < 2.0)
|
|
39
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
40
|
+
rubocop-rspec (3.9.0)
|
|
41
|
+
lint_roller (~> 1.1)
|
|
42
|
+
rubocop (~> 1.81)
|
|
43
|
+
rubocop-rspec_rails (2.32.0)
|
|
44
|
+
lint_roller (~> 1.1)
|
|
45
|
+
rubocop (~> 1.72, >= 1.72.1)
|
|
46
|
+
rubocop-rspec (~> 3.5)
|
|
47
|
+
ruby-progressbar (1.13.0)
|
|
48
|
+
standard (1.54.0)
|
|
49
|
+
language_server-protocol (~> 3.17.0.2)
|
|
50
|
+
lint_roller (~> 1.0)
|
|
51
|
+
rubocop (~> 1.84.0)
|
|
52
|
+
standard-custom (~> 1.0.0)
|
|
53
|
+
standard-performance (~> 1.8)
|
|
54
|
+
standard-custom (1.0.2)
|
|
55
|
+
lint_roller (~> 1.0)
|
|
56
|
+
rubocop (~> 1.50)
|
|
57
|
+
standard-performance (1.9.0)
|
|
58
|
+
lint_roller (~> 1.1)
|
|
59
|
+
rubocop-performance (~> 1.26.0)
|
|
60
|
+
standard-rspec (0.4.0)
|
|
61
|
+
lint_roller (>= 1.0)
|
|
62
|
+
rubocop-capybara (~> 2.22)
|
|
63
|
+
rubocop-factory_bot (~> 2.27)
|
|
64
|
+
rubocop-rspec (~> 3.9)
|
|
65
|
+
rubocop-rspec_rails (~> 2.31)
|
|
66
|
+
unicode-display_width (3.2.0)
|
|
67
|
+
unicode-emoji (~> 4.1)
|
|
68
|
+
unicode-emoji (4.2.0)
|
|
69
|
+
yard (0.9.38)
|
|
70
|
+
yard-lint (1.4.0)
|
|
71
|
+
yard (~> 0.9)
|
|
72
|
+
zeitwerk (~> 2.6)
|
|
73
|
+
zeitwerk (2.7.4)
|
|
74
|
+
|
|
75
|
+
PLATFORMS
|
|
76
|
+
ruby
|
|
77
|
+
x86_64-linux
|
|
78
|
+
|
|
79
|
+
DEPENDENCIES
|
|
80
|
+
rubocop-capybara
|
|
81
|
+
rubocop-factory_bot
|
|
82
|
+
rubocop-performance
|
|
83
|
+
rubocop-rspec
|
|
84
|
+
rubocop-rspec_rails
|
|
85
|
+
standard
|
|
86
|
+
standard-performance
|
|
87
|
+
standard-rspec
|
|
88
|
+
yard-lint
|
|
89
|
+
|
|
90
|
+
CHECKSUMS
|
|
91
|
+
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
92
|
+
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
|
|
93
|
+
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
94
|
+
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
95
|
+
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
|
|
96
|
+
parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
|
|
97
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
98
|
+
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
99
|
+
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
100
|
+
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
|
101
|
+
rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f
|
|
102
|
+
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
|
|
103
|
+
rubocop-capybara (2.22.1) sha256=ced88caef23efea53f46e098ff352f8fc1068c649606ca75cb74650970f51c0c
|
|
104
|
+
rubocop-factory_bot (2.28.0) sha256=4b17fc02124444173317e131759d195b0d762844a71a29fe8139c1105d92f0cb
|
|
105
|
+
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
|
|
106
|
+
rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2
|
|
107
|
+
rubocop-rspec_rails (2.32.0) sha256=4a0d641c72f6ebb957534f539d9d0a62c47abd8ce0d0aeee1ef4701e892a9100
|
|
108
|
+
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
|
109
|
+
standard (1.54.0) sha256=7a4b08f83d9893083c8f03bc486f0feeb6a84d48233b40829c03ef4767ea0100
|
|
110
|
+
standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b
|
|
111
|
+
standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2
|
|
112
|
+
standard-rspec (0.4.0) sha256=0fdf64c887cd6404f1c3a1435b14ba6fde2e9e80c0f4dafe4b04a67f673db262
|
|
113
|
+
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
|
|
114
|
+
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
|
|
115
|
+
yard (0.9.38) sha256=721fb82afb10532aa49860655f6cc2eaa7130889df291b052e1e6b268283010f
|
|
116
|
+
yard-lint (1.4.0) sha256=7dd88fbb08fd77cb840bea899d58812817b36d92291b5693dd0eeb3af9f91f0f
|
|
117
|
+
zeitwerk (2.7.4) sha256=2bef90f356bdafe9a6c2bd32bcd804f83a4f9b8bc27f3600fff051eb3edcec8b
|
|
118
|
+
|
|
119
|
+
BUNDLED WITH
|
|
120
|
+
4.0.3
|
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.5.0)
|
|
5
5
|
connection_pool (~> 2.4)
|
|
6
6
|
pg (~> 1.5)
|
|
7
7
|
zeitwerk (~> 2.6)
|
|
@@ -9,9 +9,26 @@ PATH
|
|
|
9
9
|
GEM
|
|
10
10
|
remote: https://rubygems.org/
|
|
11
11
|
specs:
|
|
12
|
+
async (2.36.0)
|
|
13
|
+
console (~> 1.29)
|
|
14
|
+
fiber-annotation
|
|
15
|
+
io-event (~> 1.11)
|
|
16
|
+
metrics (~> 0.12)
|
|
17
|
+
traces (~> 0.18)
|
|
12
18
|
connection_pool (2.5.4)
|
|
19
|
+
console (1.34.3)
|
|
20
|
+
fiber-annotation
|
|
21
|
+
fiber-local (~> 1.1)
|
|
22
|
+
json
|
|
13
23
|
diff-lcs (1.6.2)
|
|
14
24
|
docile (1.4.1)
|
|
25
|
+
fiber-annotation (0.2.0)
|
|
26
|
+
fiber-local (1.1.0)
|
|
27
|
+
fiber-storage
|
|
28
|
+
fiber-storage (1.0.1)
|
|
29
|
+
io-event (1.14.2)
|
|
30
|
+
json (2.18.1)
|
|
31
|
+
metrics (0.15.0)
|
|
15
32
|
pg (1.6.2)
|
|
16
33
|
pg (1.6.2-aarch64-linux)
|
|
17
34
|
pg (1.6.2-aarch64-linux-musl)
|
|
@@ -39,10 +56,7 @@ GEM
|
|
|
39
56
|
simplecov_json_formatter (~> 0.1)
|
|
40
57
|
simplecov-html (0.13.2)
|
|
41
58
|
simplecov_json_formatter (0.1.4)
|
|
42
|
-
|
|
43
|
-
yard-lint (1.3.0)
|
|
44
|
-
yard (~> 0.9)
|
|
45
|
-
zeitwerk (~> 2.6)
|
|
59
|
+
traces (0.18.2)
|
|
46
60
|
zeitwerk (2.7.3)
|
|
47
61
|
|
|
48
62
|
PLATFORMS
|
|
@@ -55,11 +69,11 @@ PLATFORMS
|
|
|
55
69
|
x86_64-linux-musl
|
|
56
70
|
|
|
57
71
|
DEPENDENCIES
|
|
72
|
+
async (~> 2.6)
|
|
58
73
|
pgmq-ruby!
|
|
59
74
|
rake
|
|
60
75
|
rspec
|
|
61
76
|
simplecov
|
|
62
|
-
yard-lint
|
|
63
77
|
|
|
64
78
|
BUNDLED WITH
|
|
65
79
|
2.7.2
|
data/README.md
CHANGED
|
@@ -25,11 +25,17 @@ PGMQ-Ruby is a Ruby client for PGMQ (PostgreSQL Message Queue). It provides dire
|
|
|
25
25
|
- [Quick Start](#quick-start)
|
|
26
26
|
- [Configuration](#configuration)
|
|
27
27
|
- [API Reference](#api-reference)
|
|
28
|
+
- [Queue Management](#queue-management)
|
|
29
|
+
- [Sending Messages](#sending-messages)
|
|
30
|
+
- [Reading Messages](#reading-messages)
|
|
31
|
+
- [Grouped Round-Robin Reading](#grouped-round-robin-reading)
|
|
32
|
+
- [Message Lifecycle](#message-lifecycle)
|
|
33
|
+
- [Monitoring](#monitoring)
|
|
34
|
+
- [Transaction Support](#transaction-support)
|
|
35
|
+
- [Topic Routing](#topic-routing-amqp-like-patterns)
|
|
28
36
|
- [Message Object](#message-object)
|
|
29
|
-
- [
|
|
30
|
-
- [Rails Integration](#rails-integration)
|
|
37
|
+
- [Working with JSON](#working-with-json)
|
|
31
38
|
- [Development](#development)
|
|
32
|
-
- [License](#license)
|
|
33
39
|
- [Author](#author)
|
|
34
40
|
|
|
35
41
|
## PGMQ Feature Support
|
|
@@ -43,6 +49,8 @@ This gem provides complete support for all core PGMQ SQL functions. Based on the
|
|
|
43
49
|
| **Reading** | `read` | Read single message with visibility timeout | ✅ |
|
|
44
50
|
| | `read_batch` | Read multiple messages with visibility timeout | ✅ |
|
|
45
51
|
| | `read_with_poll` | Long-polling for efficient message consumption | ✅ |
|
|
52
|
+
| | `read_grouped_rr` | Round-robin reading across message groups | ✅ |
|
|
53
|
+
| | `read_grouped_rr_with_poll` | Round-robin with long-polling | ✅ |
|
|
46
54
|
| | `pop` | Atomic read + delete operation | ✅ |
|
|
47
55
|
| | `pop_batch` | Atomic batch read + delete operation | ✅ |
|
|
48
56
|
| **Deleting/Archiving** | `delete` | Delete single message | ✅ |
|
|
@@ -54,8 +62,13 @@ This gem provides complete support for all core PGMQ SQL functions. Based on the
|
|
|
54
62
|
| | `create_partitioned` | Create partitioned queue (requires pg_partman) | ✅ |
|
|
55
63
|
| | `create_unlogged` | Create unlogged queue (faster, no crash recovery) | ✅ |
|
|
56
64
|
| | `drop_queue` | Delete queue and all messages | ✅ |
|
|
57
|
-
| | `
|
|
58
|
-
|
|
|
65
|
+
| **Topic Routing** | `bind_topic` | Bind topic pattern to queue (AMQP-like) | ✅ |
|
|
66
|
+
| | `unbind_topic` | Remove topic binding | ✅ |
|
|
67
|
+
| | `produce_topic` | Send message via routing key | ✅ |
|
|
68
|
+
| | `produce_batch_topic` | Batch send via routing key | ✅ |
|
|
69
|
+
| | `list_topic_bindings` | List all topic bindings | ✅ |
|
|
70
|
+
| | `test_routing` | Test which queues match a routing key | ✅ |
|
|
71
|
+
| **Utilities** | `set_vt` | Update visibility timeout (integer or Time) | ✅ |
|
|
59
72
|
| | `set_vt_batch` | Batch update visibility timeouts | ✅ |
|
|
60
73
|
| | `set_vt_multi` | Update visibility timeouts across multiple queues | ✅ |
|
|
61
74
|
| | `list_queues` | List all queues with metadata | ✅ |
|
|
@@ -75,6 +88,67 @@ This gem provides complete support for all core PGMQ SQL functions. Based on the
|
|
|
75
88
|
- Ruby 3.2+
|
|
76
89
|
- PostgreSQL 14-18 with PGMQ extension installed
|
|
77
90
|
|
|
91
|
+
### Installing PGMQ Extension
|
|
92
|
+
|
|
93
|
+
PGMQ can be installed on your PostgreSQL instance in several ways:
|
|
94
|
+
|
|
95
|
+
#### Standard Installation (Self-hosted PostgreSQL)
|
|
96
|
+
|
|
97
|
+
For self-hosted PostgreSQL instances with filesystem access, install via [PGXN](https://pgxn.org/dist/pgmq/):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pgxn install pgmq
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or build from source:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/pgmq/pgmq.git
|
|
107
|
+
cd pgmq/pgmq-extension
|
|
108
|
+
make && make install
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Then enable the extension:
|
|
112
|
+
|
|
113
|
+
```sql
|
|
114
|
+
CREATE EXTENSION pgmq;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### Managed PostgreSQL Services (AWS RDS, Aurora, etc.)
|
|
118
|
+
|
|
119
|
+
For managed PostgreSQL services that don't allow native extension installation, PGMQ provides a **SQL-only installation** that works without filesystem access:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
git clone https://github.com/pgmq/pgmq.git
|
|
123
|
+
cd pgmq
|
|
124
|
+
psql -f pgmq-extension/sql/pgmq.sql postgres://user:pass@your-rds-host:5432/database
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This creates a `pgmq` schema with all required functions. See [PGMQ Installation Guide](https://github.com/pgmq/pgmq/blob/main/INSTALLATION.md) for details.
|
|
128
|
+
|
|
129
|
+
**Comparison:**
|
|
130
|
+
|
|
131
|
+
| Feature | Extension | SQL-only |
|
|
132
|
+
|---------|-----------|----------|
|
|
133
|
+
| Version tracking | Yes | No |
|
|
134
|
+
| Upgrade path | Yes | Manual |
|
|
135
|
+
| Filesystem access | Required | Not needed |
|
|
136
|
+
| Managed cloud services | Limited | Full support |
|
|
137
|
+
|
|
138
|
+
#### Using pg_tle (Trusted Language Extensions)
|
|
139
|
+
|
|
140
|
+
If your managed PostgreSQL service supports [pg_tle](https://github.com/aws/pg_tle) (available on AWS RDS PostgreSQL 14.5+ and Aurora), you can potentially install PGMQ as a Trusted Language Extension since PGMQ is written in PL/pgSQL and SQL (both supported by pg_tle).
|
|
141
|
+
|
|
142
|
+
To use pg_tle:
|
|
143
|
+
|
|
144
|
+
1. Enable pg_tle on your instance (add to `shared_preload_libraries`)
|
|
145
|
+
2. Create the pg_tle extension: `CREATE EXTENSION pg_tle;`
|
|
146
|
+
3. Use `pgtle.install_extension()` to install PGMQ's SQL functions
|
|
147
|
+
|
|
148
|
+
See [AWS pg_tle documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL_trusted_language_extension.html) for setup instructions.
|
|
149
|
+
|
|
150
|
+
> **Note:** The SQL-only installation is simpler and recommended for most managed service use cases. pg_tle provides additional version management and extension lifecycle features if needed.
|
|
151
|
+
|
|
78
152
|
## Installation
|
|
79
153
|
|
|
80
154
|
Add to your Gemfile:
|
|
@@ -218,7 +292,7 @@ client = PGMQ::Client.new(
|
|
|
218
292
|
|
|
219
293
|
**Connection Pool Benefits:**
|
|
220
294
|
- **Thread-safe** - Multiple threads can safely share a single client
|
|
221
|
-
- **Fiber-aware** - Works with Ruby 3.0+ Fiber Scheduler for non-blocking I/O
|
|
295
|
+
- **Fiber-aware** - Works with Ruby 3.0+ Fiber Scheduler for non-blocking I/O (tested with the `async` gem)
|
|
222
296
|
- **Auto-reconnect** - Recovers from lost connections (configurable)
|
|
223
297
|
- **Health checks** - Verifies connections before use to prevent stale connection errors
|
|
224
298
|
- **Monitoring** - Track pool utilization with `client.stats`
|
|
@@ -334,6 +408,50 @@ msg = client.pop("queue_name")
|
|
|
334
408
|
|
|
335
409
|
# Pop batch (atomic read + delete for multiple messages)
|
|
336
410
|
messages = client.pop_batch("queue_name", 10)
|
|
411
|
+
|
|
412
|
+
# Grouped round-robin reading (fair processing across entities)
|
|
413
|
+
# Messages are grouped by the first key in their JSON payload
|
|
414
|
+
messages = client.read_grouped_rr("queue_name", vt: 30, qty: 10)
|
|
415
|
+
|
|
416
|
+
# Grouped round-robin with long-polling
|
|
417
|
+
messages = client.read_grouped_rr_with_poll("queue_name",
|
|
418
|
+
vt: 30,
|
|
419
|
+
qty: 10,
|
|
420
|
+
max_poll_seconds: 5,
|
|
421
|
+
poll_interval_ms: 100
|
|
422
|
+
)
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
#### Grouped Round-Robin Reading
|
|
426
|
+
|
|
427
|
+
When processing messages from multiple entities (users, orders, tenants), regular FIFO ordering can cause starvation - one entity with many messages can monopolize workers.
|
|
428
|
+
|
|
429
|
+
Grouped round-robin ensures fair processing by interleaving messages from different groups:
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
# Queue contains messages for different users:
|
|
433
|
+
# user_a: 5 messages, user_b: 2 messages, user_c: 1 message
|
|
434
|
+
|
|
435
|
+
# Regular read would process all user_a messages first (unfair)
|
|
436
|
+
messages = client.read_batch("tasks", vt: 30, qty: 8)
|
|
437
|
+
# => [user_a_1, user_a_2, user_a_3, user_a_4, user_a_5, user_b_1, user_b_2, user_c_1]
|
|
438
|
+
|
|
439
|
+
# Grouped round-robin ensures fair distribution
|
|
440
|
+
messages = client.read_grouped_rr("tasks", vt: 30, qty: 8)
|
|
441
|
+
# => [user_a_1, user_b_1, user_c_1, user_a_2, user_b_2, user_a_3, user_a_4, user_a_5]
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**How it works:**
|
|
445
|
+
- Messages are grouped by the **first key** in their JSON payload
|
|
446
|
+
- The first key should be your grouping identifier (e.g., `user_id`, `tenant_id`, `order_id`)
|
|
447
|
+
- PGMQ rotates through groups, taking one message from each before repeating
|
|
448
|
+
|
|
449
|
+
**Message format for grouping:**
|
|
450
|
+
```ruby
|
|
451
|
+
# Good - user_id is first key, used for grouping
|
|
452
|
+
client.produce("tasks", '{"user_id":"user_a","task":"process"}')
|
|
453
|
+
|
|
454
|
+
# The grouping key should come first in your JSON
|
|
337
455
|
```
|
|
338
456
|
|
|
339
457
|
#### Conditional Message Filtering
|
|
@@ -398,17 +516,24 @@ client.archive("queue_name", msg_id)
|
|
|
398
516
|
# Archive batch
|
|
399
517
|
archived_ids = client.archive_batch("queue_name", [101, 102, 103])
|
|
400
518
|
|
|
401
|
-
# Update visibility timeout
|
|
402
|
-
msg = client.set_vt("queue_name", msg_id,
|
|
519
|
+
# Update visibility timeout with integer offset (seconds from now)
|
|
520
|
+
msg = client.set_vt("queue_name", msg_id, vt: 60)
|
|
521
|
+
|
|
522
|
+
# Update visibility timeout with absolute Time (PGMQ v1.11.0+)
|
|
523
|
+
future_time = Time.now + 300 # 5 minutes from now
|
|
524
|
+
msg = client.set_vt("queue_name", msg_id, vt: future_time)
|
|
403
525
|
|
|
404
526
|
# Batch update visibility timeout
|
|
405
|
-
updated_msgs = client.set_vt_batch("queue_name", [101, 102, 103],
|
|
527
|
+
updated_msgs = client.set_vt_batch("queue_name", [101, 102, 103], vt: 60)
|
|
528
|
+
|
|
529
|
+
# Batch update with absolute Time
|
|
530
|
+
updated_msgs = client.set_vt_batch("queue_name", [101, 102, 103], vt: Time.now + 120)
|
|
406
531
|
|
|
407
532
|
# Update visibility timeout across multiple queues
|
|
408
533
|
client.set_vt_multi({
|
|
409
534
|
"orders" => [1, 2, 3],
|
|
410
535
|
"notifications" => [5, 6]
|
|
411
|
-
},
|
|
536
|
+
}, vt: 120)
|
|
412
537
|
|
|
413
538
|
# Purge all messages
|
|
414
539
|
count = client.purge_queue("queue_name")
|
|
@@ -512,6 +637,82 @@ end
|
|
|
512
637
|
- Read operations with long visibility timeouts may cause lock contention
|
|
513
638
|
- Consider using `pop()` for atomic read+delete in simple cases
|
|
514
639
|
|
|
640
|
+
### Topic Routing (AMQP-like Patterns)
|
|
641
|
+
|
|
642
|
+
PGMQ v1.11.0+ supports AMQP-style topic routing, allowing messages to be delivered to multiple queues based on pattern matching.
|
|
643
|
+
|
|
644
|
+
#### Topic Patterns
|
|
645
|
+
|
|
646
|
+
Topic patterns support wildcards:
|
|
647
|
+
- `*` matches exactly one word (e.g., `orders.*` matches `orders.new` but not `orders.new.priority`)
|
|
648
|
+
- `#` matches zero or more words (e.g., `orders.#` matches `orders`, `orders.new`, and `orders.new.priority`)
|
|
649
|
+
|
|
650
|
+
```ruby
|
|
651
|
+
# Create queues for different purposes
|
|
652
|
+
client.create("new_orders")
|
|
653
|
+
client.create("order_updates")
|
|
654
|
+
client.create("all_orders")
|
|
655
|
+
client.create("audit_log")
|
|
656
|
+
|
|
657
|
+
# Bind topic patterns to queues
|
|
658
|
+
client.bind_topic("orders.new", "new_orders") # Exact match
|
|
659
|
+
client.bind_topic("orders.update", "order_updates") # Exact match
|
|
660
|
+
client.bind_topic("orders.*", "all_orders") # Single-word wildcard
|
|
661
|
+
client.bind_topic("#", "audit_log") # Catch-all
|
|
662
|
+
|
|
663
|
+
# Send messages via routing key
|
|
664
|
+
# Message is delivered to ALL queues with matching patterns
|
|
665
|
+
count = client.produce_topic("orders.new", '{"order_id":123}')
|
|
666
|
+
# => 3 (delivered to: new_orders, all_orders, audit_log)
|
|
667
|
+
|
|
668
|
+
count = client.produce_topic("orders.update", '{"order_id":123,"status":"shipped"}')
|
|
669
|
+
# => 3 (delivered to: order_updates, all_orders, audit_log)
|
|
670
|
+
|
|
671
|
+
# Send with headers and delay
|
|
672
|
+
count = client.produce_topic("orders.new.priority",
|
|
673
|
+
'{"order_id":456}',
|
|
674
|
+
headers: '{"trace_id":"abc123"}',
|
|
675
|
+
delay: 0
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Batch send via topic routing
|
|
679
|
+
results = client.produce_batch_topic("orders.new", [
|
|
680
|
+
'{"order_id":1}',
|
|
681
|
+
'{"order_id":2}',
|
|
682
|
+
'{"order_id":3}'
|
|
683
|
+
])
|
|
684
|
+
# => [{ queue_name: "new_orders", msg_id: "1" }, ...]
|
|
685
|
+
|
|
686
|
+
# List all topic bindings
|
|
687
|
+
bindings = client.list_topic_bindings
|
|
688
|
+
bindings.each do |b|
|
|
689
|
+
puts "#{b[:pattern]} -> #{b[:queue_name]}"
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# List bindings for specific queue
|
|
693
|
+
bindings = client.list_topic_bindings(queue_name: "all_orders")
|
|
694
|
+
|
|
695
|
+
# Test which queues a routing key would match (for debugging)
|
|
696
|
+
matches = client.test_routing("orders.new.priority")
|
|
697
|
+
# => [{ pattern: "orders.#", queue_name: "all_orders" }, ...]
|
|
698
|
+
|
|
699
|
+
# Validate routing keys and patterns
|
|
700
|
+
client.validate_routing_key("orders.new.priority") # => true
|
|
701
|
+
client.validate_routing_key("orders.*") # => false (wildcards not allowed in keys)
|
|
702
|
+
client.validate_topic_pattern("orders.*") # => true
|
|
703
|
+
client.validate_topic_pattern("orders.#") # => true
|
|
704
|
+
|
|
705
|
+
# Remove bindings when done
|
|
706
|
+
client.unbind_topic("orders.new", "new_orders")
|
|
707
|
+
client.unbind_topic("orders.*", "all_orders")
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**Use Cases:**
|
|
711
|
+
- **Event broadcasting**: Send events to multiple consumers based on event type
|
|
712
|
+
- **Multi-tenant routing**: Route messages to tenant-specific queues
|
|
713
|
+
- **Log aggregation**: Capture all messages in an audit queue while routing to specific handlers
|
|
714
|
+
- **Fan-out patterns**: Deliver one message to multiple processing pipelines
|
|
715
|
+
|
|
515
716
|
## Message Object
|
|
516
717
|
|
|
517
718
|
PGMQ-Ruby is a **low-level transport library** - it returns raw values from PostgreSQL without any transformation. You are responsible for parsing JSON and type conversion.
|
|
@@ -524,6 +725,7 @@ msg.msg_id # => "123" (String, not Integer)
|
|
|
524
725
|
msg.id # => "123" (alias for msg_id)
|
|
525
726
|
msg.read_ct # => "1" (String, not Integer)
|
|
526
727
|
msg.enqueued_at # => "2025-01-15 10:30:00+00" (String, not Time)
|
|
728
|
+
msg.last_read_at # => "2025-01-15 10:30:15+00" (String, or nil if never read)
|
|
527
729
|
msg.vt # => "2025-01-15 10:30:30+00" (String, not Time)
|
|
528
730
|
msg.message # => "{\"data\":\"value\"}" (Raw JSONB as JSON string)
|
|
529
731
|
msg.headers # => "{\"trace_id\":\"abc123\"}" (Raw JSONB as JSON string, optional)
|
|
@@ -537,6 +739,7 @@ metadata = JSON.parse(msg.headers) if msg.headers # => { "trace_id" => "abc123"
|
|
|
537
739
|
id = msg.msg_id.to_i # => 123
|
|
538
740
|
read_count = msg.read_ct.to_i # => 1
|
|
539
741
|
enqueued = Time.parse(msg.enqueued_at) # => 2025-01-15 10:30:00 UTC
|
|
742
|
+
last_read = Time.parse(msg.last_read_at) if msg.last_read_at # => Time or nil
|
|
540
743
|
```
|
|
541
744
|
|
|
542
745
|
### Message Headers
|
data/Rakefile
CHANGED
|
@@ -1,4 +1,73 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "bundler/gem_tasks"
|
|
5
|
+
|
|
6
|
+
namespace :examples do
|
|
7
|
+
desc "Run all examples (validates gem functionality)"
|
|
8
|
+
task :run do
|
|
9
|
+
examples_dir = File.expand_path("spec/integration", __dir__)
|
|
10
|
+
example_files = Dir.glob(File.join(examples_dir, "*_spec.rb")).sort
|
|
11
|
+
|
|
12
|
+
puts "Running #{example_files.size} examples..."
|
|
13
|
+
puts
|
|
14
|
+
|
|
15
|
+
failed = []
|
|
16
|
+
example_files.each_with_index do |example, index|
|
|
17
|
+
name = File.basename(example)
|
|
18
|
+
puts "[#{index + 1}/#{example_files.size}] Running #{name}..."
|
|
19
|
+
|
|
20
|
+
success = system("bundle exec ruby #{example}")
|
|
21
|
+
if success.nil?
|
|
22
|
+
puts "Interrupted. Aborting."
|
|
23
|
+
exit(130)
|
|
24
|
+
elsif !success
|
|
25
|
+
failed << name
|
|
26
|
+
puts "FAILED: #{name}"
|
|
27
|
+
end
|
|
28
|
+
puts
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
puts "=" * 60
|
|
32
|
+
if failed.empty?
|
|
33
|
+
puts "All #{example_files.size} examples passed."
|
|
34
|
+
else
|
|
35
|
+
puts "#{failed.size} example(s) failed:"
|
|
36
|
+
failed.each { |f| puts " - #{f}" }
|
|
37
|
+
exit(1)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "Run a specific example by name (e.g., rake examples:run_one[basic_produce_consume])"
|
|
42
|
+
task :run_one, [:name] do |_t, args|
|
|
43
|
+
examples_dir = File.expand_path("spec/integration", __dir__)
|
|
44
|
+
pattern = File.join(examples_dir, "*#{args[:name]}*_spec.rb")
|
|
45
|
+
matches = Dir.glob(pattern)
|
|
46
|
+
|
|
47
|
+
if matches.empty?
|
|
48
|
+
puts "No example found matching: #{args[:name]}"
|
|
49
|
+
exit(1)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
exec("bundle exec ruby #{matches.first}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
desc "List all available examples"
|
|
56
|
+
task :list do
|
|
57
|
+
examples_dir = File.expand_path("spec/integration", __dir__)
|
|
58
|
+
example_files = Dir.glob(File.join(examples_dir, "*_spec.rb")).sort
|
|
59
|
+
|
|
60
|
+
puts "Available examples:"
|
|
61
|
+
example_files.each do |f|
|
|
62
|
+
name = File.basename(f, "_spec.rb")
|
|
63
|
+
puts " #{name}"
|
|
64
|
+
end
|
|
65
|
+
puts
|
|
66
|
+
puts "Run with: bundle exec rake examples:run_one[NAME]"
|
|
67
|
+
puts "Example: bundle exec rake examples:run_one[basic_produce_consume]"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Shorthand task
|
|
72
|
+
desc "Run all examples"
|
|
73
|
+
task examples: "examples:run"
|
data/docker-compose.yml
CHANGED
|
@@ -2,7 +2,7 @@ version: '3.8'
|
|
|
2
2
|
|
|
3
3
|
services:
|
|
4
4
|
postgres:
|
|
5
|
-
image: ghcr.io/pgmq/pg18-pgmq:v1.
|
|
5
|
+
image: ghcr.io/pgmq/pg18-pgmq:v1.9.0
|
|
6
6
|
container_name: pgmq_postgres_test
|
|
7
7
|
environment:
|
|
8
8
|
POSTGRES_USER: postgres
|
|
@@ -11,7 +11,7 @@ services:
|
|
|
11
11
|
ports:
|
|
12
12
|
- "5433:5432" # Use port 5433 locally to avoid conflicts
|
|
13
13
|
volumes:
|
|
14
|
-
- pgmq_data:/var/lib/postgresql
|
|
14
|
+
- pgmq_data:/var/lib/postgresql
|
|
15
15
|
healthcheck:
|
|
16
16
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
17
17
|
interval: 5s
|