redis-streams-pubsub 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aad088b02ed70d2fd4c7dae25caf5c3d869c8ea3ab31a284e5e764313aaaa5cc
4
+ data.tar.gz: 859ddf4126081dd7e9cd090e5e0d67b5300ecf7962a4df8a6d7e8c3182bc7e43
5
+ SHA512:
6
+ metadata.gz: fcc90e2859fa25f9adebf2c415431702dd35d030571ced66ea027ca13d05b8cf182a63c656a29333a8f4d2b49bba585e4167915eb44a2f1a84e3af64ec519c41
7
+ data.tar.gz: 052ff09e732138e89a0b9aaf52dd3437aa1c13cac774bfcb86d6a5553d9172a322eeabd28719dc6621c67b8446e147cde0eca07bb22247862ca06bd083f0e87d
data/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # Redis Streams PubSub
2
+
3
+ A simple, elegant Ruby gem that provides a publish/subscribe API on top of Redis Streams. It simplifies working with Redis Streams by offering familiar pub/sub patterns with automatic consumer group management and message acknowledgment.
4
+
5
+ ## Features
6
+
7
+ - **Simple API**: Familiar publish/subscribe interface
8
+ - **Consumer Groups**: Automatic consumer group creation and management
9
+ - **Message Acknowledgment**: Automatic XACK after message processing
10
+ - **JSON Support**: Automatic JSON serialization/deserialization
11
+ - **Blocking Reads**: Efficient blocking reads with configurable timeouts
12
+ - **Multiple Consumers**: Support for distributed message processing
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'redis-streams-pubsub'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ gem install redis-streams-pubsub
32
+ ```
33
+
34
+ ## Requirements
35
+
36
+ - Ruby >= 3.0
37
+ - Redis >= 5.0 (for Redis Streams support)
38
+ - redis-client >= 0.26
39
+
40
+ ## Quick Start
41
+
42
+ ### Publishing Messages
43
+
44
+ ```ruby
45
+ require 'redis-streams-pubsub'
46
+
47
+ # Option 1: Use a shorthand alias
48
+ Client = Redis::Streams::PubSub::Client
49
+
50
+ publisher = Client.new(url: "redis://localhost:6379")
51
+
52
+ # Publish a message
53
+ publisher.publish("notifications", {
54
+ type: "user_signup",
55
+ user_id: 123,
56
+ email: "user@example.com"
57
+ })
58
+ ```
59
+
60
+ ### Subscribing to Messages
61
+
62
+ ```ruby
63
+ require 'redis-streams-pubsub'
64
+
65
+ # Option 2: Include the module
66
+ include Redis::Streams::PubSub
67
+
68
+ subscriber = Client.new(url: "redis://localhost:6379")
69
+
70
+ # Subscribe and process messages
71
+ subscriber.subscribe("notifications") do |message|
72
+ puts "Received: #{message}"
73
+ # Process the message
74
+ # Return :stop to exit the subscription loop
75
+ end
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ ### Shortening the Namespace
81
+
82
+ There are several ways to avoid typing the long namespace:
83
+
84
+ ```ruby
85
+ # Option 1: Create an alias (recommended)
86
+ Client = Redis::Streams::PubSub::Client
87
+ client = Client.new
88
+
89
+ # Option 2: Include the module
90
+ include Redis::Streams::PubSub
91
+ client = Client.new
92
+
93
+ # Option 3: Assign to a local variable
94
+ PubSub = Redis::Streams::PubSub
95
+ client = PubSub::Client.new
96
+ ```
97
+
98
+ ### Basic Publisher
99
+
100
+ ```ruby
101
+ Client = Redis::Streams::PubSub::Client
102
+
103
+ client = Client.new(url: "redis://localhost:6379")
104
+
105
+ # Publish messages to a topic
106
+ client.publish("events", { event: "page_view", page: "/home" })
107
+ client.publish("events", { event: "button_click", button: "signup" })
108
+ ```
109
+
110
+ ### Basic Subscriber
111
+
112
+ ```ruby
113
+ include Redis::Streams::PubSub
114
+
115
+ client = Client.new(url: "redis://localhost:6379")
116
+
117
+ # Subscribe to a topic
118
+ client.subscribe("events") do |message|
119
+ puts "Event: #{message['event']}"
120
+ # Continue listening
121
+ end
122
+ ```
123
+
124
+ ### Stopping a Subscription
125
+
126
+ Return `:stop` from the block to exit the subscription loop:
127
+
128
+ ```ruby
129
+ client.subscribe("events") do |message|
130
+ puts "Received: #{message}"
131
+
132
+ # Stop after processing a specific message
133
+ :stop if message['type'] == 'shutdown'
134
+ end
135
+ ```
136
+
137
+ ### Custom Consumer Groups
138
+
139
+ By default, all subscribers use the same consumer group (`redis-streams-pubsub`), which means messages are distributed among subscribers (load balancing).
140
+
141
+ ```ruby
142
+ # Subscribers in the same group share the workload
143
+ subscriber1.subscribe("events", group: "workers") { |msg| process(msg) }
144
+ subscriber2.subscribe("events", group: "workers") { |msg| process(msg) }
145
+
146
+ # Subscribers in different groups each receive all messages
147
+ subscriber3.subscribe("events", group: "analytics") { |msg| analyze(msg) }
148
+ subscriber4.subscribe("events", group: "logging") { |msg| log(msg) }
149
+ ```
150
+
151
+ ### Custom Consumer ID
152
+
153
+ Each subscriber gets a unique consumer ID by default. You can specify a custom one:
154
+
155
+ ```ruby
156
+ client = Redis::Streams::PubSub::Client.new(
157
+ url: "redis://localhost:6379",
158
+ consumer: "worker-1"
159
+ )
160
+ ```
161
+
162
+ ### Error Handling
163
+
164
+ ```ruby
165
+ begin
166
+ client.subscribe("events") do |message|
167
+ process_message(message)
168
+ end
169
+ rescue Interrupt
170
+ puts "Subscriber stopped"
171
+ rescue => e
172
+ puts "Error: #{e.message}"
173
+ end
174
+ ```
175
+
176
+ ## How It Works
177
+
178
+ ### Consumer Groups
179
+
180
+ The gem uses Redis Streams consumer groups to manage message distribution:
181
+
182
+ - Messages are added to a stream using `XADD`
183
+ - Consumer groups track which messages have been delivered
184
+ - Each consumer in a group receives different messages (load balancing)
185
+ - Messages are automatically acknowledged after processing
186
+
187
+ ### Message Flow
188
+
189
+ 1. **Publisher** calls `publish(topic, payload)`
190
+ - Payload is serialized to JSON
191
+ - Message is added to the Redis Stream
192
+
193
+ 2. **Subscriber** calls `subscribe(topic)`
194
+ - Consumer group is created (if it doesn't exist)
195
+ - Subscriber blocks waiting for new messages
196
+ - When a message arrives, the block is called
197
+ - Message is automatically acknowledged
198
+
199
+ ### Blocking Behavior
200
+
201
+ The subscriber uses `XREADGROUP` with a 5-second block timeout. This means:
202
+ - The subscriber waits up to 5 seconds for new messages
203
+ - If no messages arrive, it loops and waits again
204
+ - This is efficient and doesn't poll continuously
205
+
206
+ ## Examples
207
+
208
+ See the [examples](examples/) directory for complete working examples:
209
+
210
+ - [publisher.rb](examples/publisher.rb) - Publishing messages
211
+ - [subscriber.rb](examples/subscriber.rb) - Subscribing to messages
212
+ - [README.md](examples/README.md) - Detailed examples documentation
213
+
214
+ To run the examples:
215
+
216
+ ```bash
217
+ # Terminal 1: Start the subscriber
218
+ ruby examples/subscriber.rb
219
+
220
+ # Terminal 2: Run the publisher
221
+ ruby examples/publisher.rb
222
+ ```
223
+
224
+ ## API Reference
225
+
226
+ ### `Redis::Streams::PubSub::Client`
227
+
228
+ #### `initialize(url: "redis://localhost:6379", consumer: nil)`
229
+
230
+ Creates a new client instance.
231
+
232
+ **Parameters:**
233
+ - `url` (String): Redis connection URL
234
+ - `consumer` (String, optional): Custom consumer ID (auto-generated if not provided)
235
+
236
+ #### `publish(topic, payload)`
237
+
238
+ Publishes a message to a topic.
239
+
240
+ **Parameters:**
241
+ - `topic` (String): The topic/stream name
242
+ - `payload` (Hash): Message payload (will be serialized to JSON)
243
+
244
+ **Returns:** Redis message ID
245
+
246
+ #### `subscribe(topic, group: DEFAULT_GROUP, &block)`
247
+
248
+ Subscribes to a topic and processes messages.
249
+
250
+ **Parameters:**
251
+ - `topic` (String): The topic/stream name
252
+ - `group` (String): Consumer group name (default: "redis-streams-pubsub")
253
+ - `block` (Block): Block to process each message
254
+
255
+ **Block Parameters:**
256
+ - `message` (Hash): Deserialized message payload
257
+
258
+ **Block Return:**
259
+ - Return `:stop` to exit the subscription loop
260
+ - Return anything else to continue listening
261
+
262
+ ## Testing
263
+
264
+ Run the test suite:
265
+
266
+ ```bash
267
+ bundle exec rspec
268
+ ```
269
+
270
+ Run RuboCop:
271
+
272
+ ```bash
273
+ bundle exec rubocop
274
+ ```
275
+
276
+ ## Development
277
+
278
+ After checking out the repo, run `bundle install` to install dependencies.
279
+
280
+ ## Contributing
281
+
282
+ 1. Fork it
283
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
284
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
285
+ 4. Push to the branch (`git push origin my-new-feature`)
286
+ 5. Create a new Pull Request
287
+
288
+ ## License
289
+
290
+ This project is available as open source under the terms of the MIT License.
291
+
292
+ ## Author
293
+
294
+ Davide V.
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis-client"
4
+ require "json"
5
+ require "securerandom"
6
+
7
+ module Redis
8
+ module Streams
9
+ module PubSub
10
+ # Client for publishing and subscribing to Redis Streams
11
+ class Client
12
+ DEFAULT_GROUP = "redis-streams-pubsub"
13
+
14
+ def initialize(url: "redis://localhost:6379", consumer: nil)
15
+ @url = url
16
+ @redis = RedisClient.new(url: url, timeout: 10.0)
17
+ @consumer = consumer || "consumer-#{SecureRandom.hex(3)}"
18
+ end
19
+
20
+ def publish(topic, payload)
21
+ # Use a separate connection for publishing to avoid conflicts with blocking reads
22
+ redis = RedisClient.new(url: @url)
23
+ redis.call("XADD", topic, "*", "data", payload.to_json)
24
+ ensure
25
+ redis&.close
26
+ end
27
+
28
+ def subscribe(topic, group: DEFAULT_GROUP, &block)
29
+ create_group(topic, group)
30
+
31
+ catch(:stop_subscription) do
32
+ loop { process_stream_entries(topic, group, &block) }
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def process_stream_entries(topic, group, &)
39
+ entries = read_stream_entries(topic, group)
40
+ return unless entries && !entries.empty?
41
+
42
+ _, messages = entries.first
43
+ process_messages(topic, group, messages, &)
44
+ end
45
+
46
+ def read_stream_entries(topic, group)
47
+ @redis.call(
48
+ "XREADGROUP",
49
+ "GROUP", group, @consumer,
50
+ "BLOCK", 5000,
51
+ "STREAMS", topic, ">"
52
+ )
53
+ end
54
+
55
+ def process_messages(topic, group, messages)
56
+ messages.each do |id, fields|
57
+ data = parse_message_data(fields[1])
58
+ result = yield(data)
59
+ @redis.call("XACK", topic, group, id)
60
+
61
+ throw(:stop_subscription) if result == :stop
62
+ end
63
+ end
64
+
65
+ def parse_message_data(raw_data)
66
+ JSON.parse(raw_data)
67
+ rescue StandardError
68
+ raw_data
69
+ end
70
+
71
+ def create_group(topic, group)
72
+ @redis.call("XGROUP", "CREATE", topic, group, "$", "MKSTREAM")
73
+ rescue RedisClient::CommandError => e
74
+ raise unless e.message.include?("BUSYGROUP")
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redis
4
+ module Streams
5
+ module PubSub
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis/streams/pubsub/version"
4
+ require "redis/streams/pubsub/client"
5
+
6
+ # Redis namespace
7
+ module Redis
8
+ # Streams namespace
9
+ module Streams
10
+ # PubSub implementation using Redis Streams
11
+ module PubSub
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-streams-pubsub
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Davide Villani
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: redis-client
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.26'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.26'
26
+ description: Basic Redis Streams wrapper with consumer groups for pub/sub
27
+ email:
28
+ - daemonzone@users.noreply.github.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - lib/redis-streams-pubsub.rb
35
+ - lib/redis/streams/pubsub/client.rb
36
+ - lib/redis/streams/pubsub/version.rb
37
+ homepage: https://github.com/daemonzone/redis-streams-pubsub
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ source_code_uri: https://github.com/daemonzone/redis-streams-pubsub
42
+ changelog_uri: https://github.com/daemonzone/redis-streams-pubsub/CHANGELOG.md
43
+ rubygems_mfa_required: 'true'
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '3.0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.7.2
59
+ specification_version: 4
60
+ summary: A Redis Streams pub/sub client for Ruby
61
+ test_files: []