async-redis 0.11.1 → 0.12.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/getting-started.md +94 -0
- data/context/index.yaml +16 -0
- data/context/subscriptions.md +161 -0
- data/lib/async/redis/client.rb +76 -5
- data/lib/async/redis/cluster_client.rb +73 -36
- data/lib/async/redis/cluster_subscription.rb +129 -0
- data/lib/async/redis/context/generic.rb +22 -2
- data/lib/async/redis/context/pipeline.rb +14 -1
- data/lib/async/redis/context/subscription.rb +102 -0
- data/lib/async/redis/context/transaction.rb +8 -0
- data/lib/async/redis/endpoint.rb +87 -7
- data/lib/async/redis/key.rb +19 -1
- data/lib/async/redis/protocol/resp2.rb +13 -1
- data/lib/async/redis/range_map.rb +58 -0
- data/lib/async/redis/sentinel_client.rb +31 -12
- data/lib/async/redis/version.rb +2 -2
- data/lib/async/redis.rb +9 -1
- data/readme.md +13 -0
- data/releases.md +11 -0
- data.tar.gz.sig +0 -0
- metadata +13 -8
- metadata.gz.sig +0 -0
- data/lib/async/redis/context/subscribe.rb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 631249ff627c06ac8931e39bbebd3a357695b390452bc67e7fc69e98a8abbcf0
|
4
|
+
data.tar.gz: 9f85726c02e706838bbf90d78d36233a4749901593e8c766ab008f797d18f381
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4471fecf34ff2cc9a3ead17ac8a92978aafe7e4ef8abcc5b25a36c7a48223ac9f9734ce8972a337ee2a83fd684e790a8a2bbaf263fd09d8ac646f39520f79443
|
7
|
+
data.tar.gz: 3d7388f8ab00d99aa5fd97d06ddec1f07c9027c7512fb8c194a08afa1becabf7c93407e73e85be25fc75c6af54129984fee5e638b550e22846e30c0c2b56f5fc
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Getting Started
|
2
|
+
|
3
|
+
This guide explains how to use the `async-redis` gem to connect to a Redis server and perform basic operations.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add the gem to your project:
|
8
|
+
|
9
|
+
``` shell
|
10
|
+
$ bundle add async-redis
|
11
|
+
```
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
### Basic Local Connection
|
16
|
+
|
17
|
+
``` ruby
|
18
|
+
require 'async/redis'
|
19
|
+
|
20
|
+
Async do
|
21
|
+
endpoint = Async::Redis.local_endpoint(
|
22
|
+
# Optional database index:
|
23
|
+
database: 1,
|
24
|
+
# Optional credentials:
|
25
|
+
credentials: ["username", "password"]
|
26
|
+
)
|
27
|
+
|
28
|
+
client = Async::Redis::Client.new(endpoint)
|
29
|
+
puts client.info
|
30
|
+
|
31
|
+
client.set("mykey", "myvalue")
|
32
|
+
puts client.get("mykey")
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
You can also encode this information in a URL:
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
### Connecting to Redis SSL Endpoint
|
41
|
+
|
42
|
+
This example demonstrates parsing an environment variable with a `redis://` or SSL `rediss://` scheme, and demonstrates how you can specify SSL parameters on the SSLContext object.
|
43
|
+
|
44
|
+
``` ruby
|
45
|
+
require 'async/redis'
|
46
|
+
|
47
|
+
ssl_context = OpenSSL::SSL::SSLContext.new.tap do |context|
|
48
|
+
# Load the certificate store:
|
49
|
+
context.cert_store = OpenSSL::X509::Store.new.tap do |store|
|
50
|
+
store.add_file(Rails.root.join("config/redis.pem").to_s)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Load the certificate:
|
54
|
+
context.cert = OpenSSL::X509::Certificate.new(File.read(
|
55
|
+
Rails.root.join("config/redis.crt")
|
56
|
+
))
|
57
|
+
|
58
|
+
# Load the private key:
|
59
|
+
context.key = OpenSSL::PKey::RSA.new(
|
60
|
+
Rails.application.credentials.services.redis.private_key
|
61
|
+
)
|
62
|
+
|
63
|
+
# Ensure the connection is verified according to the above certificates:
|
64
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
65
|
+
end
|
66
|
+
|
67
|
+
# e.g. REDIS_URL=rediss://:PASSWORD@redis.example.com:12345
|
68
|
+
endpoint = Async::Redis::Endpoint.parse(ENV["REDIS_URL"], ssl_context: ssl_context)
|
69
|
+
client = Async::Redis::Client.new(endpoint)
|
70
|
+
Sync do
|
71
|
+
puts client.call("PING")
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
### Variables
|
76
|
+
|
77
|
+
``` ruby
|
78
|
+
require 'async'
|
79
|
+
require 'async/redis'
|
80
|
+
|
81
|
+
endpoint = Async::Redis.local_endpoint
|
82
|
+
client = Async::Redis::Client.new(endpoint)
|
83
|
+
|
84
|
+
Async do
|
85
|
+
client.set('X', 10)
|
86
|
+
puts client.get('X')
|
87
|
+
ensure
|
88
|
+
client.close
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
## Next Steps
|
93
|
+
|
94
|
+
- [Subscriptions](../subscriptions/) - Learn how to use Redis pub/sub functionality for real-time messaging.
|
data/context/index.yaml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
3
|
+
---
|
4
|
+
description: A Redis client library.
|
5
|
+
metadata:
|
6
|
+
documentation_uri: https://socketry.github.io/async-redis/
|
7
|
+
source_code_uri: https://github.com/socketry/async-redis.git
|
8
|
+
files:
|
9
|
+
- path: getting-started.md
|
10
|
+
title: Getting Started
|
11
|
+
description: This guide explains how to use the `async-redis` gem to connect to
|
12
|
+
a Redis server and perform basic operations.
|
13
|
+
- path: subscriptions.md
|
14
|
+
title: Subscriptions
|
15
|
+
description: This guide explains how to use Redis pub/sub functionality with `async-redis`
|
16
|
+
to publish and subscribe to messages.
|
@@ -0,0 +1,161 @@
|
|
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
|
+
## Overview
|
6
|
+
|
7
|
+
Redis actually has 3 mechanisms to support pub/sub - a general `SUBSCRIBE` command, a pattern-based `PSUBSCRIBE` command, and a sharded `SSUBSCRIBE` command for cluster environments. They mostly work the same way, but have different use cases.
|
8
|
+
|
9
|
+
## Subscribe
|
10
|
+
|
11
|
+
The `SUBSCRIBE` command is used to subscribe to one or more channels. When a message is published to a subscribed channel, the client receives the message in real-time.
|
12
|
+
|
13
|
+
First, let's create a simple listener that subscribes to messages on a channel:
|
14
|
+
|
15
|
+
``` ruby
|
16
|
+
require 'async'
|
17
|
+
require 'async/redis'
|
18
|
+
|
19
|
+
client = Async::Redis::Client.new
|
20
|
+
|
21
|
+
Async do
|
22
|
+
client.subscribe 'status.frontend' do |context|
|
23
|
+
puts "Listening for messages on 'status.frontend'..."
|
24
|
+
|
25
|
+
type, name, message = context.listen
|
26
|
+
|
27
|
+
puts "Received: #{message}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
Now, let's create a publisher that sends messages to the same channel:
|
33
|
+
|
34
|
+
``` ruby
|
35
|
+
require 'async'
|
36
|
+
require 'async/redis'
|
37
|
+
|
38
|
+
client = Async::Redis::Client.new
|
39
|
+
|
40
|
+
Async do
|
41
|
+
puts "Publishing message..."
|
42
|
+
client.publish 'status.frontend', 'good'
|
43
|
+
puts "Message sent!"
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
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:
|
48
|
+
|
49
|
+
```bash
|
50
|
+
$ ruby listener.rb
|
51
|
+
Listening for messages on 'status.frontend'...
|
52
|
+
Received: good
|
53
|
+
```
|
54
|
+
|
55
|
+
### Error Handling
|
56
|
+
|
57
|
+
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.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
require 'async'
|
61
|
+
require 'async/redis'
|
62
|
+
|
63
|
+
client = Async::Redis::Client.new
|
64
|
+
|
65
|
+
Async do
|
66
|
+
client.subscribe 'status.frontend' do |context|
|
67
|
+
puts "Listening for messages on 'status.frontend'..."
|
68
|
+
|
69
|
+
context.each do |type, name, message|
|
70
|
+
puts "Received: #{message}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
rescue => error
|
74
|
+
Console.warn(self, "Subscription failed", error)
|
75
|
+
sleep 1
|
76
|
+
retry
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
## Pattern Subscribe
|
81
|
+
|
82
|
+
The `PSUBSCRIBE` command is used to subscribe to channels that match a given pattern. This allows clients to receive messages from multiple channels without subscribing to each one individually.
|
83
|
+
|
84
|
+
Let's replace the receiver in the above example:
|
85
|
+
|
86
|
+
``` ruby
|
87
|
+
require 'async'
|
88
|
+
require 'async/redis'
|
89
|
+
|
90
|
+
endpoint = Async::Redis.local_endpoint
|
91
|
+
client = Async::Redis::Client.new(endpoint)
|
92
|
+
|
93
|
+
Async do
|
94
|
+
client.psubscribe 'status.*' do |context|
|
95
|
+
puts "Listening for messages on 'status.*'..."
|
96
|
+
|
97
|
+
type, pattern, name, message = context.listen
|
98
|
+
|
99
|
+
puts "Received: #{message}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
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.
|
105
|
+
|
106
|
+
## Shard Subscribe
|
107
|
+
|
108
|
+
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.
|
109
|
+
|
110
|
+
To use sharded subscriptions, use a cluster client which supports sharded pub/sub:
|
111
|
+
|
112
|
+
``` ruby
|
113
|
+
require 'async'
|
114
|
+
require 'async/redis'
|
115
|
+
|
116
|
+
# endpoints = ...
|
117
|
+
cluster_client = Async::Redis::ClusterClient.new(endpoints)
|
118
|
+
|
119
|
+
Async do
|
120
|
+
cluster_client.subscribe 'status.frontend' do |context|
|
121
|
+
puts "Listening for messages on 'status.frontend'..."
|
122
|
+
|
123
|
+
type, name, message = context.listen
|
124
|
+
|
125
|
+
puts "Received: #{message}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
``` ruby
|
131
|
+
require 'async'
|
132
|
+
require 'async/redis'
|
133
|
+
|
134
|
+
# endpoints = ...
|
135
|
+
cluster_client = Async::Redis::ClusterClient.new(endpoints)
|
136
|
+
|
137
|
+
Async do
|
138
|
+
puts "Publishing message..."
|
139
|
+
cluster_client.publish('status.frontend', 'good')
|
140
|
+
puts "Message sent!"
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
### Clustered Subscriptions
|
145
|
+
|
146
|
+
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.
|
147
|
+
|
148
|
+
#### Cluster Topology Changes and Subscription Invalidation
|
149
|
+
|
150
|
+
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.
|
151
|
+
|
152
|
+
**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.
|
153
|
+
|
154
|
+
Common scenarios that trigger subscription invalidation:
|
155
|
+
|
156
|
+
- **Resharding operations**: When slots are migrated between nodes (`MOVED` errors)
|
157
|
+
- **Node failures**: When Redis nodes become unavailable
|
158
|
+
- **Network partitions**: When connections to specific shards are lost
|
159
|
+
- **Cluster reconfiguration**: When the cluster topology changes
|
160
|
+
|
161
|
+
Applications should be prepared to handle subscription failures and implement appropriate retry strategies.
|
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"
|
@@ -23,12 +23,20 @@ module Async
|
|
23
23
|
# Legacy.
|
24
24
|
ServerError = ::Protocol::Redis::ServerError
|
25
25
|
|
26
|
+
# A Redis client that provides connection pooling and context management.
|
26
27
|
class Client
|
27
28
|
include ::Protocol::Redis::Methods
|
28
29
|
|
30
|
+
# Methods module providing Redis-specific functionality.
|
29
31
|
module Methods
|
32
|
+
# Subscribe to one or more channels for pub/sub messaging.
|
33
|
+
# @parameter channels [Array(String)] The channels to subscribe to.
|
34
|
+
# @yields {|context| ...} If a block is given, it will be executed within the subscription context.
|
35
|
+
# @parameter context [Context::Subscription] The subscription context.
|
36
|
+
# @returns [Object] The result of the block if block given.
|
37
|
+
# @returns [Context::Subscription] The subscription context if no block given.
|
30
38
|
def subscribe(*channels)
|
31
|
-
context = Context::
|
39
|
+
context = Context::Subscription.new(@pool, channels)
|
32
40
|
|
33
41
|
return context unless block_given?
|
34
42
|
|
@@ -39,6 +47,49 @@ module Async
|
|
39
47
|
end
|
40
48
|
end
|
41
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)
|
78
|
+
|
79
|
+
return context unless block_given?
|
80
|
+
|
81
|
+
begin
|
82
|
+
yield context
|
83
|
+
ensure
|
84
|
+
context.close
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Execute commands within a Redis transaction.
|
89
|
+
# @yields {|context| ...} If a block is given, it will be executed within the transaction context.
|
90
|
+
# @parameter context [Context::Transaction] The transaction context.
|
91
|
+
# @returns [Object] The result of the block if block given.
|
92
|
+
# @returns [Context::Transaction] Else if no block is given, returns the transaction context.
|
42
93
|
def transaction(&block)
|
43
94
|
context = Context::Transaction.new(@pool)
|
44
95
|
|
@@ -53,6 +104,11 @@ module Async
|
|
53
104
|
|
54
105
|
alias multi transaction
|
55
106
|
|
107
|
+
# Execute commands in a pipeline for improved performance.
|
108
|
+
# @yields {|context| ...} If a block is given, it will be executed within the pipeline context.
|
109
|
+
# @parameter context [Context::Pipeline] The pipeline context.
|
110
|
+
# @returns [Object] The result of the block if block given.
|
111
|
+
# @returns [Context::Pipeline] The pipeline context if no block given.
|
56
112
|
def pipeline(&block)
|
57
113
|
context = Context::Pipeline.new(@pool)
|
58
114
|
|
@@ -68,6 +124,9 @@ module Async
|
|
68
124
|
# Deprecated.
|
69
125
|
alias nested pipeline
|
70
126
|
|
127
|
+
# Execute a Redis command directly.
|
128
|
+
# @parameter arguments [Array] The command and its arguments.
|
129
|
+
# @returns [Object] The response from the Redis server.
|
71
130
|
def call(*arguments)
|
72
131
|
@pool.acquire do |connection|
|
73
132
|
connection.write_request(arguments)
|
@@ -78,6 +137,7 @@ module Async
|
|
78
137
|
end
|
79
138
|
end
|
80
139
|
|
140
|
+
# Close the client and all its connections.
|
81
141
|
def close
|
82
142
|
@pool.close
|
83
143
|
end
|
@@ -85,6 +145,10 @@ module Async
|
|
85
145
|
|
86
146
|
include Methods
|
87
147
|
|
148
|
+
# Create a new Redis client.
|
149
|
+
# @parameter endpoint [Endpoint] The Redis endpoint to connect to.
|
150
|
+
# @parameter protocol [Protocol] The protocol to use for communication.
|
151
|
+
# @parameter options [Hash] Additional options for the connection pool.
|
88
152
|
def initialize(endpoint = Endpoint.local, protocol: endpoint.protocol, **options)
|
89
153
|
@endpoint = endpoint
|
90
154
|
@protocol = protocol
|
@@ -92,11 +156,18 @@ module Async
|
|
92
156
|
@pool = make_pool(**options)
|
93
157
|
end
|
94
158
|
|
159
|
+
# @attribute [Endpoint] The Redis endpoint.
|
95
160
|
attr :endpoint
|
161
|
+
|
162
|
+
# @attribute [Protocol] The communication protocol.
|
96
163
|
attr :protocol
|
97
164
|
|
98
|
-
#
|
99
|
-
# @
|
165
|
+
# Open a Redis client and optionally yield it in an async task.
|
166
|
+
# @yields {|client, task| ...} If a block is given, yield the client in an async task.
|
167
|
+
# @parameter client [Client] The Redis client instance.
|
168
|
+
# @parameter task [Async::Task] The async task.
|
169
|
+
# @returns [Client] The client if no block provided.
|
170
|
+
# @returns [Object] The result of the block if block given.
|
100
171
|
def self.open(*arguments, **options, &block)
|
101
172
|
client = self.new(*arguments, **options)
|
102
173
|
|
@@ -5,53 +5,28 @@
|
|
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
|
16
|
+
# A Redis cluster client that manages multiple Redis instances and handles cluster operations.
|
12
17
|
class ClusterClient
|
18
|
+
include ::Protocol::Redis::Cluster::Methods
|
19
|
+
|
20
|
+
# Raised when cluster configuration cannot be reloaded.
|
13
21
|
class ReloadError < StandardError
|
14
22
|
end
|
15
23
|
|
24
|
+
# Raised when no nodes are found for a specific slot.
|
16
25
|
class SlotError < StandardError
|
17
26
|
end
|
18
27
|
|
19
28
|
Node = Struct.new(:id, :endpoint, :role, :health, :client)
|
20
29
|
|
21
|
-
class RangeMap
|
22
|
-
def initialize
|
23
|
-
@ranges = []
|
24
|
-
end
|
25
|
-
|
26
|
-
def add(range, value)
|
27
|
-
@ranges << [range, value]
|
28
|
-
|
29
|
-
return value
|
30
|
-
end
|
31
|
-
|
32
|
-
def find(key)
|
33
|
-
@ranges.each do |range, value|
|
34
|
-
return value if range.include?(key)
|
35
|
-
end
|
36
|
-
|
37
|
-
if block_given?
|
38
|
-
return yield
|
39
|
-
end
|
40
|
-
|
41
|
-
return nil
|
42
|
-
end
|
43
|
-
|
44
|
-
def each
|
45
|
-
@ranges.each do |range, value|
|
46
|
-
yield value
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def clear
|
51
|
-
@ranges.clear
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
30
|
# Create a new instance of the cluster client.
|
56
31
|
#
|
57
32
|
# @property endpoints [Array(Endpoint)] The list of cluster endpoints.
|
@@ -61,6 +36,13 @@ module Async
|
|
61
36
|
@shards = nil
|
62
37
|
end
|
63
38
|
|
39
|
+
# Execute a block with clients for the given keys, grouped by cluster slot.
|
40
|
+
# @parameter keys [Array] The keys to find clients for.
|
41
|
+
# @parameter role [Symbol] The role of nodes to use (:master or :slave).
|
42
|
+
# @parameter attempts [Integer] Number of retry attempts for cluster errors.
|
43
|
+
# @yields {|client, keys| ...} Block called for each client-keys pair.
|
44
|
+
# @parameter client [Client] The Redis client for the slot.
|
45
|
+
# @parameter keys [Array] The keys handled by this client.
|
64
46
|
def clients_for(*keys, role: :master, attempts: 3)
|
65
47
|
slots = slots_for(keys)
|
66
48
|
|
@@ -83,6 +65,10 @@ module Async
|
|
83
65
|
end
|
84
66
|
end
|
85
67
|
|
68
|
+
# Get a client for a specific slot.
|
69
|
+
# @parameter slot [Integer] The cluster slot number.
|
70
|
+
# @parameter role [Symbol] The role of node to get (:master or :slave).
|
71
|
+
# @returns [Client] The Redis client for the slot.
|
86
72
|
def client_for(slot, role = :master)
|
87
73
|
unless @shards
|
88
74
|
reload_cluster!
|
@@ -99,13 +85,35 @@ module Async
|
|
99
85
|
end
|
100
86
|
end
|
101
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
|
+
|
102
110
|
protected
|
103
111
|
|
104
112
|
def reload_cluster!(endpoints = @endpoints)
|
105
113
|
@endpoints.each do |endpoint|
|
106
114
|
client = Client.new(endpoint, **@options)
|
107
115
|
|
108
|
-
shards = RangeMap.new
|
116
|
+
shards = Async::Redis::RangeMap.new
|
109
117
|
endpoints = []
|
110
118
|
|
111
119
|
client.call("CLUSTER", "SHARDS").each do |shard|
|
@@ -116,7 +124,7 @@ module Async
|
|
116
124
|
|
117
125
|
nodes = shard["nodes"].map do |node|
|
118
126
|
node = node.each_slice(2).to_h
|
119
|
-
endpoint = Endpoint.
|
127
|
+
endpoint = Endpoint.for(endpoint.scheme, node["endpoint"], port: node["port"])
|
120
128
|
|
121
129
|
# Collect all endpoints:
|
122
130
|
endpoints << endpoint
|
@@ -203,6 +211,9 @@ module Async
|
|
203
211
|
return crc16(key) % HASH_SLOTS
|
204
212
|
end
|
205
213
|
|
214
|
+
# Calculate the hash slots for multiple keys.
|
215
|
+
# @parameter keys [Array] The keys to calculate slots for.
|
216
|
+
# @returns [Hash] A hash mapping slot numbers to arrays of keys.
|
206
217
|
def slots_for(keys)
|
207
218
|
slots = Hash.new{|hash, key| hash[key] = []}
|
208
219
|
|
@@ -212,6 +223,32 @@ module Async
|
|
212
223
|
|
213
224
|
return slots
|
214
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
|
215
252
|
end
|
216
253
|
end
|
217
254
|
end
|