async-redis 0.11.2 → 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 +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 +1 -1
- data/lib/async/redis/context/{subscribe.rb → subscription.rb} +39 -5
- data/lib/async/redis/endpoint.rb +36 -1
- data/lib/async/redis/key.rb +1 -1
- data/lib/async/redis/protocol/resp2.rb +1 -1
- data/lib/async/redis/range_map.rb +58 -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/readme.md +9 -0
- data/releases.md +7 -0
- data.tar.gz.sig +0 -0
- metadata +10 -5
- metadata.gz.sig +0 -0
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"
|
@@ -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
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
require "async/limited_queue"
|
7
|
+
require "async/barrier"
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Redis
|
11
|
+
# Context for managing sharded subscriptions across multiple Redis cluster nodes.
|
12
|
+
# This class handles the complexity of subscribing to channels that may be distributed
|
13
|
+
# across different shards in a Redis cluster.
|
14
|
+
class ClusterSubscription
|
15
|
+
# Represents a failure in the subscription process, e.g. network issues, shard failures.
|
16
|
+
class SubscriptionError < StandardError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Initialize a new shard subscription context.
|
20
|
+
# @parameter cluster_client [ClusterClient] The cluster client to use.
|
21
|
+
def initialize(cluster_client, queue: Async::LimitedQueue.new)
|
22
|
+
@cluster_client = cluster_client
|
23
|
+
@subscriptions = {}
|
24
|
+
@channels = []
|
25
|
+
|
26
|
+
@barrier = Async::Barrier.new
|
27
|
+
@queue = queue
|
28
|
+
end
|
29
|
+
|
30
|
+
# Close all shard subscriptions.
|
31
|
+
def close
|
32
|
+
if barrier = @barrier
|
33
|
+
@barrier = nil
|
34
|
+
barrier.stop
|
35
|
+
end
|
36
|
+
|
37
|
+
@subscriptions.each_value(&:close)
|
38
|
+
@subscriptions.clear
|
39
|
+
end
|
40
|
+
|
41
|
+
# Listen for the next message from any subscribed shard.
|
42
|
+
# @returns [Array] The next message response.
|
43
|
+
# @raises [SubscriptionError] If the subscription has failed for any reason.
|
44
|
+
def listen
|
45
|
+
@queue.pop
|
46
|
+
rescue => error
|
47
|
+
raise SubscriptionError, "Failed to read message!"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Iterate over all messages from all subscribed shards.
|
51
|
+
# @yields {|response| ...} Block called for each message.
|
52
|
+
# @parameter response [Array] The message response.
|
53
|
+
def each
|
54
|
+
return to_enum unless block_given?
|
55
|
+
|
56
|
+
while response = self.listen
|
57
|
+
yield response
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Subscribe to additional sharded channels.
|
62
|
+
# @parameter channels [Array(String)] The channels to subscribe to.
|
63
|
+
def subscribe(channels)
|
64
|
+
slots = @cluster_client.slots_for(channels)
|
65
|
+
|
66
|
+
slots.each do |slot, channels_for_slot|
|
67
|
+
if subscription = @subscriptions[slot]
|
68
|
+
# Add to existing subscription for this shard
|
69
|
+
subscription.ssubscribe(channels_for_slot)
|
70
|
+
else
|
71
|
+
# Create new subscription for this shard
|
72
|
+
client = @cluster_client.client_for(slot)
|
73
|
+
subscription = @subscriptions[slot] = client.ssubscribe(*channels_for_slot)
|
74
|
+
|
75
|
+
@barrier.async do
|
76
|
+
# This is optimistic, in other words, subscription.listen will also fail on close.
|
77
|
+
until subscription.closed?
|
78
|
+
@queue << subscription.listen
|
79
|
+
end
|
80
|
+
ensure
|
81
|
+
# If we are exiting here for any reason OTHER than the subscription was closed, we need to re-create the subscription state:
|
82
|
+
unless subscription.closed?
|
83
|
+
@queue.close
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@channels.concat(channels)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Unsubscribe from sharded channels.
|
93
|
+
# @parameter channels [Array(String)] The channels to unsubscribe from.
|
94
|
+
def unsubscribe(channels)
|
95
|
+
slots = @cluster_client.slots_for(channels)
|
96
|
+
|
97
|
+
slots.each do |slot, channels_for_slot|
|
98
|
+
if subscription = @subscriptions[slot]
|
99
|
+
subscription.sunsubscribe(channels_for_slot)
|
100
|
+
|
101
|
+
# Remove channels from our tracking
|
102
|
+
@channels -= channels_for_slot
|
103
|
+
|
104
|
+
# Check if this shard still has channels
|
105
|
+
remaining_channels_for_slot = @channels.select {|ch| @cluster_client.slot_for(ch) == slot}
|
106
|
+
|
107
|
+
# If no channels left for this shard, close and remove it
|
108
|
+
if remaining_channels_for_slot.empty?
|
109
|
+
@subscriptions.delete(slot)
|
110
|
+
subscription.close
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get the list of currently subscribed channels.
|
117
|
+
# @returns [Array(String)] The list of subscribed channels.
|
118
|
+
def channels
|
119
|
+
@channels.dup
|
120
|
+
end
|
121
|
+
|
122
|
+
# Get the number of active shard subscriptions.
|
123
|
+
# @returns [Integer] The number of shard connections.
|
124
|
+
def shard_count
|
125
|
+
@subscriptions.size
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -22,12 +22,17 @@ module Async
|
|
22
22
|
|
23
23
|
# Close the context and release the connection back to the pool.
|
24
24
|
def close
|
25
|
-
if @connection
|
26
|
-
@pool.release(@connection)
|
25
|
+
if connection = @connection
|
27
26
|
@connection = nil
|
27
|
+
@pool.release(connection)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
# @returns [Boolean] Whether the context is closed.
|
32
|
+
def closed?
|
33
|
+
@connection.nil?
|
34
|
+
end
|
35
|
+
|
31
36
|
# Write a Redis command request to the connection.
|
32
37
|
# @parameter command [String] The Redis command.
|
33
38
|
# @parameter arguments [Array] The command arguments.
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2018, by Huba Nagy.
|
5
|
-
# Copyright, 2018-
|
5
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
6
6
|
|
7
7
|
require_relative "generic"
|
8
8
|
|
@@ -10,8 +10,10 @@ module Async
|
|
10
10
|
module Redis
|
11
11
|
module Context
|
12
12
|
# Context for Redis pub/sub subscription operations.
|
13
|
-
class
|
13
|
+
class Subscription < Generic
|
14
14
|
MESSAGE = "message"
|
15
|
+
PMESSAGE = "pmessage"
|
16
|
+
SMESSAGE = "smessage"
|
15
17
|
|
16
18
|
# Initialize a new subscription context.
|
17
19
|
# @parameter pool [Pool] The connection pool to use.
|
@@ -19,12 +21,12 @@ module Async
|
|
19
21
|
def initialize(pool, channels)
|
20
22
|
super(pool)
|
21
23
|
|
22
|
-
subscribe(channels)
|
24
|
+
subscribe(channels) if channels.any?
|
23
25
|
end
|
24
26
|
|
25
27
|
# Close the subscription context.
|
26
28
|
def close
|
27
|
-
#
|
29
|
+
# This causes anyone calling `#listen` to exit, as `read_response` will fail. If we decided to use `RESET` instead, we'd need to take that into account.
|
28
30
|
@connection&.close
|
29
31
|
|
30
32
|
super
|
@@ -34,7 +36,11 @@ module Async
|
|
34
36
|
# @returns [Array] The next message response, or nil if connection closed.
|
35
37
|
def listen
|
36
38
|
while response = @connection.read_response
|
37
|
-
|
39
|
+
type = response.first
|
40
|
+
|
41
|
+
if type == MESSAGE || type == PMESSAGE || type == SMESSAGE
|
42
|
+
return response
|
43
|
+
end
|
38
44
|
end
|
39
45
|
end
|
40
46
|
|
@@ -62,6 +68,34 @@ module Async
|
|
62
68
|
@connection.write_request ["UNSUBSCRIBE", *channels]
|
63
69
|
@connection.flush
|
64
70
|
end
|
71
|
+
|
72
|
+
# Subscribe to channel patterns.
|
73
|
+
# @parameter patterns [Array(String)] The channel patterns to subscribe to.
|
74
|
+
def psubscribe(patterns)
|
75
|
+
@connection.write_request ["PSUBSCRIBE", *patterns]
|
76
|
+
@connection.flush
|
77
|
+
end
|
78
|
+
|
79
|
+
# Unsubscribe from channel patterns.
|
80
|
+
# @parameter patterns [Array(String)] The channel patterns to unsubscribe from.
|
81
|
+
def punsubscribe(patterns)
|
82
|
+
@connection.write_request ["PUNSUBSCRIBE", *patterns]
|
83
|
+
@connection.flush
|
84
|
+
end
|
85
|
+
|
86
|
+
# Subscribe to sharded channels (Redis 7.0+).
|
87
|
+
# @parameter channels [Array(String)] The sharded channels to subscribe to.
|
88
|
+
def ssubscribe(channels)
|
89
|
+
@connection.write_request ["SSUBSCRIBE", *channels]
|
90
|
+
@connection.flush
|
91
|
+
end
|
92
|
+
|
93
|
+
# Unsubscribe from sharded channels (Redis 7.0+).
|
94
|
+
# @parameter channels [Array(String)] The sharded channels to unsubscribe from.
|
95
|
+
def sunsubscribe(channels)
|
96
|
+
@connection.write_request ["SUNSUBSCRIBE", *channels]
|
97
|
+
@connection.flush
|
98
|
+
end
|
65
99
|
end
|
66
100
|
end
|
67
101
|
end
|
data/lib/async/redis/endpoint.rb
CHANGED
@@ -58,11 +58,19 @@ module Async
|
|
58
58
|
end
|
59
59
|
|
60
60
|
# Construct an endpoint with a specified scheme, hostname, optional path, and options.
|
61
|
+
# If no scheme is provided, it will be auto-detected based on SSL context.
|
61
62
|
#
|
62
|
-
# @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
|
63
|
+
# @parameter scheme [String, nil] The scheme to use, e.g. "redis" or "rediss". If nil, will auto-detect.
|
63
64
|
# @parameter hostname [String] The hostname to connect to (or bind to).
|
64
65
|
# @parameter options [Hash] Additional options, passed to {#initialize}.
|
65
66
|
def self.for(scheme, host, credentials: nil, port: nil, database: nil, **options)
|
67
|
+
# Auto-detect scheme if not provided:
|
68
|
+
if default_scheme = options.delete(:scheme)
|
69
|
+
scheme ||= default_scheme
|
70
|
+
end
|
71
|
+
|
72
|
+
scheme ||= options.key?(:ssl_context) ? "rediss" : "redis"
|
73
|
+
|
66
74
|
uri_klass = SCHEMES.fetch(scheme.downcase) do
|
67
75
|
raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
|
68
76
|
end
|
@@ -210,6 +218,8 @@ module Async
|
|
210
218
|
end
|
211
219
|
end
|
212
220
|
|
221
|
+
# Get the credentials for authentication.
|
222
|
+
# @returns [Array(String) | Nil] The username and password credentials or nil if not specified.
|
213
223
|
def credentials
|
214
224
|
@options[:credentials] || extract_userinfo(@url.userinfo)
|
215
225
|
end
|
@@ -224,6 +234,8 @@ module Async
|
|
224
234
|
end
|
225
235
|
end
|
226
236
|
|
237
|
+
# Check if the endpoint is connecting to localhost.
|
238
|
+
# @returns [Boolean] True if connecting to localhost.
|
227
239
|
def localhost?
|
228
240
|
@url.hostname =~ /^(.*?\.)?localhost\.?$/
|
229
241
|
end
|
@@ -237,6 +249,8 @@ module Async
|
|
237
249
|
end
|
238
250
|
end
|
239
251
|
|
252
|
+
# Get the SSL context for secure connections.
|
253
|
+
# @returns [OpenSSL::SSL::SSLContext] The SSL context configured for this endpoint.
|
240
254
|
def ssl_context
|
241
255
|
@options[:ssl_context] || OpenSSL::SSL::SSLContext.new.tap do |context|
|
242
256
|
context.set_params(
|
@@ -245,6 +259,9 @@ module Async
|
|
245
259
|
end
|
246
260
|
end
|
247
261
|
|
262
|
+
# Build the underlying endpoint with optional SSL wrapping.
|
263
|
+
# @parameter endpoint [IO::Endpoint] Optional base endpoint to wrap.
|
264
|
+
# @returns [IO::Endpoint] The built endpoint, potentially wrapped with SSL.
|
248
265
|
def build_endpoint(endpoint = nil)
|
249
266
|
endpoint ||= tcp_endpoint
|
250
267
|
|
@@ -260,22 +277,33 @@ module Async
|
|
260
277
|
return endpoint
|
261
278
|
end
|
262
279
|
|
280
|
+
# Get the underlying endpoint, building it if necessary.
|
281
|
+
# @returns [IO::Endpoint] The underlying endpoint for connections.
|
263
282
|
def endpoint
|
264
283
|
@endpoint ||= build_endpoint
|
265
284
|
end
|
266
285
|
|
286
|
+
# Set the underlying endpoint.
|
287
|
+
# @parameter endpoint [IO::Endpoint] The endpoint to wrap and use.
|
267
288
|
def endpoint=(endpoint)
|
268
289
|
@endpoint = build_endpoint(endpoint)
|
269
290
|
end
|
270
291
|
|
292
|
+
# Bind to the endpoint and yield the server socket.
|
293
|
+
# @parameter arguments [Array] Arguments to pass to the underlying endpoint bind method.
|
294
|
+
# @yields [IO] The bound server socket.
|
271
295
|
def bind(*arguments, &block)
|
272
296
|
endpoint.bind(*arguments, &block)
|
273
297
|
end
|
274
298
|
|
299
|
+
# Connect to the endpoint and yield the client socket.
|
300
|
+
# @yields [IO] The connected client socket.
|
275
301
|
def connect(&block)
|
276
302
|
endpoint.connect(&block)
|
277
303
|
end
|
278
304
|
|
305
|
+
# Iterate over each possible endpoint variation.
|
306
|
+
# @yields [Endpoint] Each endpoint variant.
|
279
307
|
def each
|
280
308
|
return to_enum unless block_given?
|
281
309
|
|
@@ -284,14 +312,21 @@ module Async
|
|
284
312
|
end
|
285
313
|
end
|
286
314
|
|
315
|
+
# Get the key for hashing and equality comparison.
|
316
|
+
# @returns [Array] The key components for this endpoint.
|
287
317
|
def key
|
288
318
|
[@url, @options]
|
289
319
|
end
|
290
320
|
|
321
|
+
# Check if this endpoint is equal to another.
|
322
|
+
# @parameter other [Endpoint] The other endpoint to compare with.
|
323
|
+
# @returns [Boolean] True if the endpoints are equal.
|
291
324
|
def eql? other
|
292
325
|
self.key.eql? other.key
|
293
326
|
end
|
294
327
|
|
328
|
+
# Get the hash code for this endpoint.
|
329
|
+
# @returns [Integer] The hash code based on the endpoint's key.
|
295
330
|
def hash
|
296
331
|
self.key.hash
|
297
332
|
end
|
data/lib/async/redis/key.rb
CHANGED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Async
|
4
|
+
module Redis
|
5
|
+
# A map that stores ranges and their associated values for efficient lookup.
|
6
|
+
class RangeMap
|
7
|
+
# Initialize a new RangeMap.
|
8
|
+
def initialize
|
9
|
+
@ranges = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Add a range-value pair to the map.
|
13
|
+
# @parameter range [Range] The range to map.
|
14
|
+
# @parameter value [Object] The value to associate with the range.
|
15
|
+
# @returns [Object] The added value.
|
16
|
+
def add(range, value)
|
17
|
+
@ranges << [range, value]
|
18
|
+
return value
|
19
|
+
end
|
20
|
+
|
21
|
+
# Find the value associated with a key within any range.
|
22
|
+
# @parameter key [Object] The key to find.
|
23
|
+
# @yields {...} Block called if no range contains the key.
|
24
|
+
# @returns [Object] The value if found, result of block if given, or nil.
|
25
|
+
def find(key)
|
26
|
+
@ranges.each do |range, value|
|
27
|
+
return value if range.include?(key)
|
28
|
+
end
|
29
|
+
if block_given?
|
30
|
+
return yield
|
31
|
+
end
|
32
|
+
return nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# Iterate over all values in the map.
|
36
|
+
# @yields {|value| ...} Block called for each value.
|
37
|
+
# @parameter value [Object] The value from the range-value pair.
|
38
|
+
def each
|
39
|
+
@ranges.each do |range, value|
|
40
|
+
yield value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get a random value from the map.
|
45
|
+
# @returns [Object] A randomly selected value, or nil if map is empty.
|
46
|
+
def sample
|
47
|
+
return nil if @ranges.empty?
|
48
|
+
range, value = @ranges.sample
|
49
|
+
return value
|
50
|
+
end
|
51
|
+
|
52
|
+
# Clear all ranges from the map.
|
53
|
+
def clear
|
54
|
+
@ranges.clear
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2020, by David Ortiz.
|
5
|
-
# Copyright, 2023-
|
5
|
+
# Copyright, 2023-2025, by Samuel Williams.
|
6
6
|
# Copyright, 2024, by Joan Lledó.
|
7
7
|
|
8
8
|
require_relative "client"
|
@@ -21,13 +21,15 @@ module Async
|
|
21
21
|
#
|
22
22
|
# @property endpoints [Array(Endpoint)] The list of sentinel endpoints.
|
23
23
|
# @property master_name [String] The name of the master instance, defaults to 'mymaster'.
|
24
|
+
# @property master_options [Hash] Connection options for master instances.
|
25
|
+
# @property slave_options [Hash] Connection options for slave instances (defaults to master_options if not specified).
|
24
26
|
# @property role [Symbol] The role of the instance that you want to connect to, either `:master` or `:slave`.
|
25
|
-
|
26
|
-
def initialize(endpoints, master_name: DEFAULT_MASTER_NAME, role: :master, protocol: Protocol::RESP2, **options)
|
27
|
+
def initialize(endpoints, master_name: DEFAULT_MASTER_NAME, master_options: nil, slave_options: nil, role: :master, **options)
|
27
28
|
@endpoints = endpoints
|
28
29
|
@master_name = master_name
|
30
|
+
@master_options = master_options || {}
|
31
|
+
@slave_options = slave_options || @master_options
|
29
32
|
@role = role
|
30
|
-
@protocol = protocol
|
31
33
|
|
32
34
|
# A cache of sentinel connections.
|
33
35
|
@sentinels = {}
|
@@ -94,9 +96,11 @@ module Async
|
|
94
96
|
end
|
95
97
|
end
|
96
98
|
|
99
|
+
private
|
100
|
+
|
97
101
|
# Resolve the master endpoint address.
|
98
102
|
# @returns [Endpoint | Nil] The master endpoint or nil if not found.
|
99
|
-
def resolve_master
|
103
|
+
def resolve_master(options = @master_options)
|
100
104
|
sentinels do |client|
|
101
105
|
begin
|
102
106
|
address = client.call("SENTINEL", "GET-MASTER-ADDR-BY-NAME", @master_name)
|
@@ -104,7 +108,7 @@ module Async
|
|
104
108
|
next
|
105
109
|
end
|
106
110
|
|
107
|
-
return Endpoint.
|
111
|
+
return Endpoint.for(nil, address[0], port: address[1], **options) if address
|
108
112
|
end
|
109
113
|
|
110
114
|
return nil
|
@@ -112,7 +116,7 @@ module Async
|
|
112
116
|
|
113
117
|
# Resolve a slave endpoint address.
|
114
118
|
# @returns [Endpoint | Nil] A slave endpoint or nil if not found.
|
115
|
-
def resolve_slave
|
119
|
+
def resolve_slave(options = @slave_options)
|
116
120
|
sentinels do |client|
|
117
121
|
begin
|
118
122
|
reply = client.call("SENTINEL", "SLAVES", @master_name)
|
@@ -124,16 +128,14 @@ module Async
|
|
124
128
|
next if slaves.empty?
|
125
129
|
|
126
130
|
slave = select_slave(slaves)
|
127
|
-
return Endpoint.
|
131
|
+
return Endpoint.for(nil, slave["ip"], port: slave["port"], **options)
|
128
132
|
end
|
129
133
|
|
130
134
|
return nil
|
131
135
|
end
|
132
136
|
|
133
|
-
protected
|
134
|
-
|
135
137
|
def assign_default_tags(tags)
|
136
|
-
tags[:
|
138
|
+
tags[:role] ||= @role
|
137
139
|
end
|
138
140
|
|
139
141
|
# Override the parent method. The only difference is that this one needs to resolve the master/slave address.
|
@@ -145,7 +147,7 @@ module Async
|
|
145
147
|
peer = endpoint.connect
|
146
148
|
stream = ::IO::Stream(peer)
|
147
149
|
|
148
|
-
|
150
|
+
endpoint.protocol.client(stream)
|
149
151
|
end
|
150
152
|
end
|
151
153
|
|
data/lib/async/redis/version.rb
CHANGED
data/lib/async/redis.rb
CHANGED
data/readme.md
CHANGED
@@ -14,10 +14,19 @@ Please see the [project documentation](https://socketry.github.io/async-redis/)
|
|
14
14
|
|
15
15
|
- [Getting Started](https://socketry.github.io/async-redis/guides/getting-started/index) - This guide explains how to use the `async-redis` gem to connect to a Redis server and perform basic operations.
|
16
16
|
|
17
|
+
- [Subscriptions](https://socketry.github.io/async-redis/guides/subscriptions/index) - This guide explains how to use Redis pub/sub functionality with `async-redis` to publish and subscribe to messages.
|
18
|
+
|
17
19
|
## Releases
|
18
20
|
|
19
21
|
Please see the [project releases](https://socketry.github.io/async-redis/releases/index) for all releases.
|
20
22
|
|
23
|
+
### v0.12.0
|
24
|
+
|
25
|
+
- Add agent context.
|
26
|
+
- Add support for pattern pub/sub.
|
27
|
+
- Add support for sharded pub/sub.
|
28
|
+
- Add support for `master_options` and `slave_options` (and removed `protocol`) from `SentinelClient`.
|
29
|
+
|
21
30
|
### v0.11.2
|
22
31
|
|
23
32
|
- Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
|
data/releases.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Releases
|
2
2
|
|
3
|
+
## v0.12.0
|
4
|
+
|
5
|
+
- Add agent context.
|
6
|
+
- Add support for pattern pub/sub.
|
7
|
+
- Add support for sharded pub/sub.
|
8
|
+
- Add support for `master_options` and `slave_options` (and removed `protocol`) from `SentinelClient`.
|
9
|
+
|
3
10
|
## v0.11.2
|
4
11
|
|
5
12
|
- Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-redis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -113,30 +113,35 @@ dependencies:
|
|
113
113
|
requirements:
|
114
114
|
- - "~>"
|
115
115
|
- !ruby/object:Gem::Version
|
116
|
-
version: '0.
|
116
|
+
version: '0.11'
|
117
117
|
type: :runtime
|
118
118
|
prerelease: false
|
119
119
|
version_requirements: !ruby/object:Gem::Requirement
|
120
120
|
requirements:
|
121
121
|
- - "~>"
|
122
122
|
- !ruby/object:Gem::Version
|
123
|
-
version: '0.
|
123
|
+
version: '0.11'
|
124
124
|
executables: []
|
125
125
|
extensions: []
|
126
126
|
extra_rdoc_files: []
|
127
127
|
files:
|
128
|
+
- context/getting-started.md
|
129
|
+
- context/index.yaml
|
130
|
+
- context/subscriptions.md
|
128
131
|
- lib/async/redis.rb
|
129
132
|
- lib/async/redis/client.rb
|
130
133
|
- lib/async/redis/cluster_client.rb
|
134
|
+
- lib/async/redis/cluster_subscription.rb
|
131
135
|
- lib/async/redis/context/generic.rb
|
132
136
|
- lib/async/redis/context/pipeline.rb
|
133
|
-
- lib/async/redis/context/
|
137
|
+
- lib/async/redis/context/subscription.rb
|
134
138
|
- lib/async/redis/context/transaction.rb
|
135
139
|
- lib/async/redis/endpoint.rb
|
136
140
|
- lib/async/redis/key.rb
|
137
141
|
- lib/async/redis/protocol/authenticated.rb
|
138
142
|
- lib/async/redis/protocol/resp2.rb
|
139
143
|
- lib/async/redis/protocol/selected.rb
|
144
|
+
- lib/async/redis/range_map.rb
|
140
145
|
- lib/async/redis/sentinel_client.rb
|
141
146
|
- lib/async/redis/version.rb
|
142
147
|
- license.md
|
@@ -162,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
167
|
- !ruby/object:Gem::Version
|
163
168
|
version: '0'
|
164
169
|
requirements: []
|
165
|
-
rubygems_version: 3.6.
|
170
|
+
rubygems_version: 3.6.9
|
166
171
|
specification_version: 4
|
167
172
|
summary: A Redis client library.
|
168
173
|
test_files: []
|
metadata.gz.sig
CHANGED
Binary file
|