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.
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.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
- yard (0.9.38)
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
- - [Serializers](#serializers)
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
- | | `detach_archive` | Detach archive table from queue | ✅ |
58
- | **Utilities** | `set_vt` | Update message visibility timeout | ✅ |
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, vt_offset: 60)
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], vt_offset: 60)
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
- }, vt_offset: 120)
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 'bundler/setup'
4
- require 'bundler/gem_tasks'
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.8.0
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/data
14
+ - pgmq_data:/var/lib/postgresql
15
15
  healthcheck:
16
16
  test: ["CMD-SHELL", "pg_isready -U postgres"]
17
17
  interval: 5s