async-redis 0.11.2 → 0.13.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
- checksums.yaml.gz.sig +0 -0
- data/context/client-architecture.md +124 -0
- data/context/data-structures.md +486 -0
- data/context/getting-started.md +90 -0
- data/context/index.yaml +36 -0
- data/context/scripting.md +243 -0
- data/context/streams.md +317 -0
- data/context/subscriptions.md +172 -0
- data/context/transactions-and-pipelines.md +197 -0
- data/lib/async/redis/client.rb +43 -5
- data/lib/async/redis/cluster_client.rb +55 -49
- data/lib/async/redis/cluster_subscription.rb +129 -0
- data/lib/async/redis/context/generic.rb +7 -2
- data/lib/async/redis/context/pipeline.rb +2 -2
- data/lib/async/redis/context/{subscribe.rb → subscription.rb} +39 -5
- data/lib/async/redis/endpoint.rb +61 -4
- data/lib/async/redis/key.rb +1 -1
- data/lib/async/redis/protocol/resp2.rb +1 -1
- data/lib/async/redis/range_map.rb +61 -0
- data/lib/async/redis/sentinel_client.rb +14 -12
- data/lib/async/redis/version.rb +1 -1
- data/lib/async/redis.rb +1 -1
- data/license.md +1 -1
- data/readme.md +24 -0
- data/releases.md +12 -0
- data.tar.gz.sig +0 -0
- metadata +16 -6
- metadata.gz.sig +0 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
# Subscriptions
|
2
|
+
|
3
|
+
This guide explains how to use Redis pub/sub functionality with `async-redis` to publish and subscribe to messages.
|
4
|
+
|
5
|
+
Redis pub/sub enables real-time communication between different parts of your application or between different applications. It's perfect for broadcasting notifications, coordinating distributed systems, or building real-time features, provided you don't need reliable messaging.
|
6
|
+
|
7
|
+
Common use cases:
|
8
|
+
- **Real-time notifications**: Alert users about new messages, updates, or events.
|
9
|
+
- **System coordination**: Notify services about configuration changes or cache invalidation.
|
10
|
+
- **Live updates**: Push data changes to web interfaces or mobile apps.
|
11
|
+
- **Event-driven architecture**: Decouple services through asynchronous messaging.
|
12
|
+
|
13
|
+
## Overview
|
14
|
+
|
15
|
+
Redis provides 3 pub/sub mechanisms:
|
16
|
+
- **SUBSCRIBE**: Subscribe to specific channels by name.
|
17
|
+
- **PSUBSCRIBE**: Subscribe to channels matching patterns (e.g., `user.*`).
|
18
|
+
- **SSUBSCRIBE**: Sharded subscriptions for cluster environments (better performance).
|
19
|
+
|
20
|
+
## Subscribe
|
21
|
+
|
22
|
+
The `SUBSCRIBE` command lets you listen for messages on specific channels. This creates a persistent connection that receives messages as they're published.
|
23
|
+
|
24
|
+
Here's a simple notification system - first, create a listener:
|
25
|
+
|
26
|
+
``` ruby
|
27
|
+
require "async"
|
28
|
+
require "async/redis"
|
29
|
+
|
30
|
+
client = Async::Redis::Client.new
|
31
|
+
|
32
|
+
Async do
|
33
|
+
client.subscribe "status.frontend" do |context|
|
34
|
+
puts "Listening for messages on 'status.frontend'..."
|
35
|
+
|
36
|
+
type, name, message = context.listen
|
37
|
+
|
38
|
+
puts "Received: #{message}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
In another part of your application, publish notifications:
|
44
|
+
|
45
|
+
``` ruby
|
46
|
+
require "async"
|
47
|
+
require "async/redis"
|
48
|
+
|
49
|
+
client = Async::Redis::Client.new
|
50
|
+
|
51
|
+
Async do
|
52
|
+
puts "Publishing message..."
|
53
|
+
client.publish "status.frontend", "good"
|
54
|
+
puts "Message sent!"
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
To see pub/sub in action, you can run the listener in one terminal and the publisher in another. The listener will receive any messages sent by the publisher to the `status.frontend` channel:
|
59
|
+
|
60
|
+
```bash
|
61
|
+
$ ruby listener.rb
|
62
|
+
Listening for messages on 'status.frontend'...
|
63
|
+
Received: good
|
64
|
+
```
|
65
|
+
|
66
|
+
### Error Handling
|
67
|
+
|
68
|
+
Subscriptions are at-most-once delivery. In addition, subscriptions are stateful, meaning that they maintain their own internal state and can be affected by network issues or server restarts. In order to improve resilience, it's important to implement error handling and reconnection logic.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
require "async"
|
72
|
+
require "async/redis"
|
73
|
+
|
74
|
+
client = Async::Redis::Client.new
|
75
|
+
|
76
|
+
Async do
|
77
|
+
client.subscribe "status.frontend" do |context|
|
78
|
+
puts "Listening for messages on 'status.frontend'..."
|
79
|
+
|
80
|
+
context.each do |type, name, message|
|
81
|
+
puts "Received: #{message}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
rescue => error
|
85
|
+
Console.warn(self, "Subscription failed", error)
|
86
|
+
sleep 1
|
87
|
+
retry
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
## Pattern Subscribe
|
92
|
+
|
93
|
+
When you need to listen to multiple related channels, patterns save you from subscribing to each channel individually. This is perfect for monitoring all user activities or all events from a specific service.
|
94
|
+
|
95
|
+
For example, to monitor all user-related events (`user.login`, `user.logout`, `user.signup`):
|
96
|
+
|
97
|
+
``` ruby
|
98
|
+
require "async"
|
99
|
+
require "async/redis"
|
100
|
+
|
101
|
+
endpoint = Async::Redis.local_endpoint
|
102
|
+
client = Async::Redis::Client.new(endpoint)
|
103
|
+
|
104
|
+
Async do
|
105
|
+
client.psubscribe "status.*" do |context|
|
106
|
+
puts "Listening for messages on 'status.*'..."
|
107
|
+
|
108
|
+
type, pattern, name, message = context.listen
|
109
|
+
|
110
|
+
puts "Received: #{message}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
Note that an extra field, `pattern` is returned when using `PSUBSCRIBE`. This field indicates the pattern that was matched for the incoming message. This can be useful for logging or debugging purposes, as it allows you to see which pattern triggered the message delivery.
|
116
|
+
|
117
|
+
## Shard Subscribe
|
118
|
+
|
119
|
+
If you are working with a clustered environment, you can improve performance by limiting the scope of your subscriptions to specific shards. This can help reduce the amount of data that needs to be sent between shards and improve overall throughput.
|
120
|
+
|
121
|
+
To use sharded subscriptions, use a cluster client which supports sharded pub/sub:
|
122
|
+
|
123
|
+
``` ruby
|
124
|
+
require "async"
|
125
|
+
require "async/redis"
|
126
|
+
|
127
|
+
# endpoints = ...
|
128
|
+
cluster_client = Async::Redis::ClusterClient.new(endpoints)
|
129
|
+
|
130
|
+
Async do
|
131
|
+
cluster_client.subscribe "status.frontend" do |context|
|
132
|
+
puts "Listening for messages on 'status.frontend'..."
|
133
|
+
|
134
|
+
type, name, message = context.listen
|
135
|
+
|
136
|
+
puts "Received: #{message}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
``` ruby
|
142
|
+
require "async"
|
143
|
+
require "async/redis"
|
144
|
+
|
145
|
+
# endpoints = ...
|
146
|
+
cluster_client = Async::Redis::ClusterClient.new(endpoints)
|
147
|
+
|
148
|
+
Async do
|
149
|
+
puts "Publishing message..."
|
150
|
+
cluster_client.publish("status.frontend", "good")
|
151
|
+
puts "Message sent!"
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
### Clustered Subscriptions
|
156
|
+
|
157
|
+
While general `PUBLISH` and `SUBSCRIBE` will work on a cluster, they are less efficient as they require inter-shard communication. By default, the {ruby Async::Redis::ClusterClient} subscription mechanism defaults to `SSUBSCRIBE` and `SPUBLISH`, which are optimized for sharded environments. However, if using multiple subscriptions, internally, several connections will be made to the relevant shards, which increases the complexity.
|
158
|
+
|
159
|
+
#### Cluster Topology Changes and Subscription Invalidation
|
160
|
+
|
161
|
+
If the cluster is re-configured (e.g. adding or removing nodes, resharding), the subscription state may need to be re-established to account for the new topology. During this process, messages may be lost. This is expected as subscriptions are stateless.
|
162
|
+
|
163
|
+
**Important**: When any individual shard subscription fails (due to resharding, node failures, or network issues), the entire cluster subscription is invalidated and will stop delivering messages. This design ensures consistency and prevents partial subscription states that could lead to missed messages on some shards.
|
164
|
+
|
165
|
+
Common scenarios that trigger subscription invalidation:
|
166
|
+
|
167
|
+
- **Resharding operations**: When slots are migrated between nodes (`MOVED` errors)
|
168
|
+
- **Node failures**: When Redis nodes become unavailable
|
169
|
+
- **Network partitions**: When connections to specific shards are lost
|
170
|
+
- **Cluster reconfiguration**: When the cluster topology changes
|
171
|
+
|
172
|
+
Applications should be prepared to handle subscription failures and implement appropriate retry strategies.
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# Transactions and Pipelines
|
2
|
+
|
3
|
+
This guide explains how to use Redis transactions and pipelines with `async-redis` for atomic operations and improved performance.
|
4
|
+
|
5
|
+
By default, each command (e.g. `GET key`) acquires a connection from the client, runs the command, returns the result and releases the connection back to the client's connection pool. This may be inefficient for some use cases, and so there are several ways to group together operations that run on the same connection.
|
6
|
+
|
7
|
+
## Transactions (MULTI/EXEC)
|
8
|
+
|
9
|
+
Transactions ensure that multiple Redis commands execute atomically - either all commands succeed, or none of them do. This is crucial when you need to maintain data consistency, such as transferring money between accounts or updating related fields together.
|
10
|
+
|
11
|
+
Use transactions when you need:
|
12
|
+
- **Atomic updates**: Multiple operations that must all succeed or all fail.
|
13
|
+
- **Data consistency**: Keeping related data in sync across multiple keys.
|
14
|
+
- **Preventing partial updates**: Avoiding situations where only some of your changes are applied.
|
15
|
+
|
16
|
+
Redis transactions queue commands and execute them all at once:
|
17
|
+
|
18
|
+
``` ruby
|
19
|
+
require "async/redis"
|
20
|
+
|
21
|
+
endpoint = Async::Redis.local_endpoint
|
22
|
+
client = Async::Redis::Client.new(endpoint)
|
23
|
+
|
24
|
+
Async do
|
25
|
+
begin
|
26
|
+
# Execute commands atomically:
|
27
|
+
results = client.transaction do |context|
|
28
|
+
context.multi
|
29
|
+
|
30
|
+
# Queue commands for atomic execution:
|
31
|
+
context.set("user:1:name", "Alice")
|
32
|
+
context.set("user:1:email", "alice@example.com")
|
33
|
+
context.incr("user:count")
|
34
|
+
|
35
|
+
# Execute all queued commands:
|
36
|
+
context.execute
|
37
|
+
end
|
38
|
+
|
39
|
+
puts "Transaction results: #{results}"
|
40
|
+
|
41
|
+
ensure
|
42
|
+
client.close
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
### Watch/Unwatch for Optimistic Locking
|
48
|
+
|
49
|
+
When multiple clients might modify the same data simultaneously, you need to handle race conditions. Redis WATCH provides optimistic locking - the transaction only executes if watched keys haven't changed since you started watching them.
|
50
|
+
|
51
|
+
This is essential for scenarios like:
|
52
|
+
- Updating counters or balances where the current value matters.
|
53
|
+
- Implementing atomic increment operations with business logic.
|
54
|
+
- Preventing lost updates in concurrent environments.
|
55
|
+
|
56
|
+
Here's how to implement safe concurrent updates:
|
57
|
+
|
58
|
+
``` ruby
|
59
|
+
require "async/redis"
|
60
|
+
|
61
|
+
endpoint = Async::Redis.local_endpoint
|
62
|
+
client = Async::Redis::Client.new(endpoint)
|
63
|
+
|
64
|
+
Async do
|
65
|
+
begin
|
66
|
+
# Initialize counter.
|
67
|
+
client.set("counter", 0)
|
68
|
+
|
69
|
+
# Optimistic locking example:
|
70
|
+
5.times do |i|
|
71
|
+
success = false
|
72
|
+
attempts = 0
|
73
|
+
|
74
|
+
while !success && attempts < 3
|
75
|
+
attempts += 1
|
76
|
+
|
77
|
+
result = client.transaction do |context|
|
78
|
+
# Watch the counter for changes (executes immediately):
|
79
|
+
context.watch("counter")
|
80
|
+
|
81
|
+
# Read current value (executes immediately):
|
82
|
+
current_value = client.get("counter").to_i
|
83
|
+
new_value = current_value + 1
|
84
|
+
|
85
|
+
# Start transaction - commands after this are queued, not executed:
|
86
|
+
context.multi
|
87
|
+
|
88
|
+
# Queue commands (these return "QUEUED", don't execute yet):
|
89
|
+
context.set("counter", new_value)
|
90
|
+
context.set("last_update", Time.now.to_f)
|
91
|
+
|
92
|
+
# Execute all queued commands atomically.
|
93
|
+
# Returns nil if watched key was modified by another client:
|
94
|
+
context.execute
|
95
|
+
end
|
96
|
+
|
97
|
+
if result
|
98
|
+
puts "Increment #{i + 1} succeeded: #{result}"
|
99
|
+
success = true
|
100
|
+
else
|
101
|
+
puts "Increment #{i + 1} failed, retrying (attempt #{attempts})"
|
102
|
+
sleep 0.01
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
final_value = client.get("counter")
|
108
|
+
puts "Final counter value: #{final_value}"
|
109
|
+
|
110
|
+
ensure
|
111
|
+
client.close
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
## Pipelines
|
117
|
+
|
118
|
+
When you need to execute many Redis commands quickly, sending them one-by-one creates network latency bottlenecks. Pipelines solve this by batching multiple commands together, dramatically reducing round-trip time.
|
119
|
+
|
120
|
+
Use pipelines when you need:
|
121
|
+
- **Better performance**: Reduce network round trips for bulk operations.
|
122
|
+
- **High throughput**: Process hundreds or thousands of commands efficiently.
|
123
|
+
- **Independent operations**: Commands that don't depend on each other's results.
|
124
|
+
|
125
|
+
Unlike transactions, pipeline commands are not atomic - some may succeed while others fail:
|
126
|
+
|
127
|
+
``` ruby
|
128
|
+
require "async/redis"
|
129
|
+
|
130
|
+
endpoint = Async::Redis.local_endpoint
|
131
|
+
client = Async::Redis::Client.new(endpoint)
|
132
|
+
|
133
|
+
Async do
|
134
|
+
begin
|
135
|
+
# Execute commands in a pipeline:
|
136
|
+
results = client.pipeline do |context|
|
137
|
+
# Commands are buffered and flushed if needed:
|
138
|
+
context.set("pipeline:1", "value1")
|
139
|
+
context.set("pipeline:2", "value2")
|
140
|
+
context.set("pipeline:3", "value3")
|
141
|
+
|
142
|
+
context.get("pipeline:1")
|
143
|
+
context.get("pipeline:2")
|
144
|
+
context.get("pipeline:3")
|
145
|
+
|
146
|
+
# Flush and collect the results from all previous commands:
|
147
|
+
context.collect
|
148
|
+
end
|
149
|
+
|
150
|
+
puts "Pipeline results: #{results}"
|
151
|
+
|
152
|
+
ensure
|
153
|
+
client.close
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
### Synchronous Pipeline Operations
|
159
|
+
|
160
|
+
When you need immediate results from individual commands within a pipeline, use `context.sync`:
|
161
|
+
|
162
|
+
``` ruby
|
163
|
+
require "async/redis"
|
164
|
+
|
165
|
+
endpoint = Async::Redis.local_endpoint
|
166
|
+
client = Async::Redis::Client.new(endpoint)
|
167
|
+
|
168
|
+
Async do
|
169
|
+
begin
|
170
|
+
client.pipeline do |context|
|
171
|
+
# Set values using pipeline - no immediate response:
|
172
|
+
context.set("async_key_1", "value1")
|
173
|
+
context.set("async_key_2", "value2")
|
174
|
+
|
175
|
+
# Get immediate response using sync:
|
176
|
+
immediate_result = context.sync.get("async_key_1")
|
177
|
+
puts "Immediate result: #{immediate_result}"
|
178
|
+
|
179
|
+
# Continue with pipelined operations:
|
180
|
+
context.get("async_key_2")
|
181
|
+
|
182
|
+
# Collect remaining pipelined results:
|
183
|
+
context.collect
|
184
|
+
end
|
185
|
+
|
186
|
+
ensure
|
187
|
+
client.close
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
Use `context.sync` when you need to:
|
193
|
+
- **Check values mid-pipeline**: Verify data before continuing with more operations.
|
194
|
+
- **Conditional logic**: Make decisions based on current Redis state.
|
195
|
+
- **Debugging**: Get immediate feedback during pipeline development.
|
196
|
+
|
197
|
+
Note that `sync` operations execute immediately and flush pending responses, so use them strategically to maintain pipeline performance benefits.
|
data/lib/async/redis/client.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2018, by Huba Nagy.
|
6
6
|
# Copyright, 2019, by Mikael Henriksson.
|
7
7
|
# Copyright, 2019, by David Ortiz.
|
@@ -9,7 +9,7 @@
|
|
9
9
|
|
10
10
|
require_relative "context/pipeline"
|
11
11
|
require_relative "context/transaction"
|
12
|
-
require_relative "context/
|
12
|
+
require_relative "context/subscription"
|
13
13
|
require_relative "endpoint"
|
14
14
|
|
15
15
|
require "io/endpoint/host_endpoint"
|
@@ -32,11 +32,49 @@ module Async
|
|
32
32
|
# Subscribe to one or more channels for pub/sub messaging.
|
33
33
|
# @parameter channels [Array(String)] The channels to subscribe to.
|
34
34
|
# @yields {|context| ...} If a block is given, it will be executed within the subscription context.
|
35
|
-
# @parameter context [Context::
|
35
|
+
# @parameter context [Context::Subscription] The subscription context.
|
36
36
|
# @returns [Object] The result of the block if block given.
|
37
|
-
# @returns [Context::
|
37
|
+
# @returns [Context::Subscription] The subscription context if no block given.
|
38
38
|
def subscribe(*channels)
|
39
|
-
context = Context::
|
39
|
+
context = Context::Subscription.new(@pool, channels)
|
40
|
+
|
41
|
+
return context unless block_given?
|
42
|
+
|
43
|
+
begin
|
44
|
+
yield context
|
45
|
+
ensure
|
46
|
+
context.close
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Subscribe to one or more channel patterns for pub/sub messaging.
|
51
|
+
# @parameter patterns [Array(String)] The channel patterns to subscribe to.
|
52
|
+
# @yields {|context| ...} If a block is given, it will be executed within the subscription context.
|
53
|
+
# @parameter context [Context::Subscription] The subscription context.
|
54
|
+
# @returns [Object] The result of the block if block given.
|
55
|
+
# @returns [Context::Subscription] The subscription context if no block given.
|
56
|
+
def psubscribe(*patterns)
|
57
|
+
context = Context::Subscription.new(@pool, [])
|
58
|
+
context.psubscribe(patterns)
|
59
|
+
|
60
|
+
return context unless block_given?
|
61
|
+
|
62
|
+
begin
|
63
|
+
yield context
|
64
|
+
ensure
|
65
|
+
context.close
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Subscribe to one or more sharded channels for pub/sub messaging (Redis 7.0+).
|
70
|
+
# @parameter channels [Array(String)] The sharded channels to subscribe to.
|
71
|
+
# @yields {|context| ...} If a block is given, it will be executed within the subscription context.
|
72
|
+
# @parameter context [Context::Subscription] The subscription context.
|
73
|
+
# @returns [Object] The result of the block if block given.
|
74
|
+
# @returns [Context::Subscription] The subscription context if no block given.
|
75
|
+
def ssubscribe(*channels)
|
76
|
+
context = Context::Subscription.new(@pool, [])
|
77
|
+
context.ssubscribe(channels)
|
40
78
|
|
41
79
|
return context unless block_given?
|
42
80
|
|
@@ -5,12 +5,18 @@
|
|
5
5
|
# Copyright, 2025, by Travis Bell.
|
6
6
|
|
7
7
|
require_relative "client"
|
8
|
+
require_relative "cluster_subscription"
|
9
|
+
require_relative "range_map"
|
8
10
|
require "io/stream"
|
9
11
|
|
12
|
+
require "protocol/redis/cluster/methods"
|
13
|
+
|
10
14
|
module Async
|
11
15
|
module Redis
|
12
16
|
# A Redis cluster client that manages multiple Redis instances and handles cluster operations.
|
13
17
|
class ClusterClient
|
18
|
+
include ::Protocol::Redis::Cluster::Methods
|
19
|
+
|
14
20
|
# Raised when cluster configuration cannot be reloaded.
|
15
21
|
class ReloadError < StandardError
|
16
22
|
end
|
@@ -21,54 +27,6 @@ module Async
|
|
21
27
|
|
22
28
|
Node = Struct.new(:id, :endpoint, :role, :health, :client)
|
23
29
|
|
24
|
-
# A map that stores ranges and their associated values for efficient lookup.
|
25
|
-
class RangeMap
|
26
|
-
# Initialize a new RangeMap.
|
27
|
-
def initialize
|
28
|
-
@ranges = []
|
29
|
-
end
|
30
|
-
|
31
|
-
# Add a range-value pair to the map.
|
32
|
-
# @parameter range [Range] The range to map.
|
33
|
-
# @parameter value [Object] The value to associate with the range.
|
34
|
-
# @returns [Object] The added value.
|
35
|
-
def add(range, value)
|
36
|
-
@ranges << [range, value]
|
37
|
-
|
38
|
-
return value
|
39
|
-
end
|
40
|
-
|
41
|
-
# Find the value associated with a key within any range.
|
42
|
-
# @parameter key [Object] The key to find.
|
43
|
-
# @yields {...} Block called if no range contains the key.
|
44
|
-
# @returns [Object] The value if found, result of block if given, or nil.
|
45
|
-
def find(key)
|
46
|
-
@ranges.each do |range, value|
|
47
|
-
return value if range.include?(key)
|
48
|
-
end
|
49
|
-
|
50
|
-
if block_given?
|
51
|
-
return yield
|
52
|
-
end
|
53
|
-
|
54
|
-
return nil
|
55
|
-
end
|
56
|
-
|
57
|
-
# Iterate over all values in the map.
|
58
|
-
# @yields {|value| ...} Block called for each value.
|
59
|
-
# @parameter value [Object] The value from the range-value pair.
|
60
|
-
def each
|
61
|
-
@ranges.each do |range, value|
|
62
|
-
yield value
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
# Clear all ranges from the map.
|
67
|
-
def clear
|
68
|
-
@ranges.clear
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
30
|
# Create a new instance of the cluster client.
|
73
31
|
#
|
74
32
|
# @property endpoints [Array(Endpoint)] The list of cluster endpoints.
|
@@ -127,13 +85,35 @@ module Async
|
|
127
85
|
end
|
128
86
|
end
|
129
87
|
|
88
|
+
# Get any available client from the cluster.
|
89
|
+
# This is useful for operations that don't require slot-specific routing, such as global pub/sub operations, INFO commands, or other cluster-wide operations.
|
90
|
+
# @parameter role [Symbol] The role of node to get (:master or :slave).
|
91
|
+
# @returns [Client] A Redis client for any available node.
|
92
|
+
def any_client(role = :master)
|
93
|
+
unless @shards
|
94
|
+
reload_cluster!
|
95
|
+
end
|
96
|
+
|
97
|
+
# Sample a random shard to get better load distribution
|
98
|
+
if nodes = @shards.sample
|
99
|
+
nodes = nodes.select{|node| node.role == role}
|
100
|
+
|
101
|
+
if node = nodes.sample
|
102
|
+
return (node.client ||= Client.new(node.endpoint, **@options))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Fallback to slot 0 if sampling fails
|
107
|
+
client_for(0, role)
|
108
|
+
end
|
109
|
+
|
130
110
|
protected
|
131
111
|
|
132
112
|
def reload_cluster!(endpoints = @endpoints)
|
133
113
|
@endpoints.each do |endpoint|
|
134
114
|
client = Client.new(endpoint, **@options)
|
135
115
|
|
136
|
-
shards = RangeMap.new
|
116
|
+
shards = Async::Redis::RangeMap.new
|
137
117
|
endpoints = []
|
138
118
|
|
139
119
|
client.call("CLUSTER", "SHARDS").each do |shard|
|
@@ -243,6 +223,32 @@ module Async
|
|
243
223
|
|
244
224
|
return slots
|
245
225
|
end
|
226
|
+
|
227
|
+
# Subscribe to one or more sharded channels for pub/sub messaging in cluster environment.
|
228
|
+
# The subscription will be created on the appropriate nodes responsible for each channel's hash slot.
|
229
|
+
#
|
230
|
+
# @parameter channels [Array(String)] The sharded channels to subscribe to.
|
231
|
+
# @yields {|context| ...} If a block is given, it will be executed within the subscription context.
|
232
|
+
# @parameter context [ClusterSubscription] The cluster subscription context.
|
233
|
+
# @returns [Object] The result of the block if block given.
|
234
|
+
# @returns [ClusterSubscription] The cluster subscription context if no block given.
|
235
|
+
def subscribe(*channels)
|
236
|
+
context = ClusterSubscription.new(self)
|
237
|
+
|
238
|
+
if channels.any?
|
239
|
+
context.subscribe(channels)
|
240
|
+
end
|
241
|
+
|
242
|
+
if block_given?
|
243
|
+
begin
|
244
|
+
yield context
|
245
|
+
ensure
|
246
|
+
context.close
|
247
|
+
end
|
248
|
+
else
|
249
|
+
return context
|
250
|
+
end
|
251
|
+
end
|
246
252
|
end
|
247
253
|
end
|
248
254
|
end
|