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 +7 -0
- data/README.md +294 -0
- data/lib/redis/streams/pubsub/client.rb +79 -0
- data/lib/redis/streams/pubsub/version.rb +9 -0
- data/lib/redis-streams-pubsub.rb +14 -0
- metadata +61 -0
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,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: []
|