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
@@ -1,28 +1,40 @@
|
|
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
|
|
6
6
|
require "protocol/redis"
|
7
7
|
|
8
8
|
module Async
|
9
9
|
module Redis
|
10
|
+
# @namespace
|
10
11
|
module Protocol
|
12
|
+
# RESP2 protocol implementation for Redis.
|
11
13
|
module RESP2
|
14
|
+
# A connection implementation for RESP2 protocol.
|
12
15
|
class Connection < ::Protocol::Redis::Connection
|
16
|
+
# Get the concurrency level for this connection.
|
17
|
+
# @returns [Integer] The concurrency level (1 for RESP2).
|
13
18
|
def concurrency
|
14
19
|
1
|
15
20
|
end
|
16
21
|
|
22
|
+
# Check if the connection is viable for use.
|
23
|
+
# @returns [Boolean] True if the stream is readable.
|
17
24
|
def viable?
|
18
25
|
@stream.readable?
|
19
26
|
end
|
20
27
|
|
28
|
+
# Check if the connection can be reused.
|
29
|
+
# @returns [Boolean] True if the stream is not closed.
|
21
30
|
def reusable?
|
22
31
|
!@stream.closed?
|
23
32
|
end
|
24
33
|
end
|
25
34
|
|
35
|
+
# Create a new RESP2 client connection.
|
36
|
+
# @parameter stream [IO] The stream to use for communication.
|
37
|
+
# @returns [Connection] A new RESP2 connection.
|
26
38
|
def self.client(stream)
|
27
39
|
Connection.new(stream)
|
28
40
|
end
|
@@ -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"
|
@@ -10,6 +10,7 @@ require "io/stream"
|
|
10
10
|
|
11
11
|
module Async
|
12
12
|
module Redis
|
13
|
+
# A Redis Sentinel client for high availability Redis deployments.
|
13
14
|
class SentinelClient
|
14
15
|
DEFAULT_MASTER_NAME = "mymaster"
|
15
16
|
|
@@ -20,13 +21,15 @@ module Async
|
|
20
21
|
#
|
21
22
|
# @property endpoints [Array(Endpoint)] The list of sentinel endpoints.
|
22
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).
|
23
26
|
# @property role [Symbol] The role of the instance that you want to connect to, either `:master` or `:slave`.
|
24
|
-
|
25
|
-
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)
|
26
28
|
@endpoints = endpoints
|
27
29
|
@master_name = master_name
|
30
|
+
@master_options = master_options || {}
|
31
|
+
@slave_options = slave_options || @master_options
|
28
32
|
@role = role
|
29
|
-
@protocol = protocol
|
30
33
|
|
31
34
|
# A cache of sentinel connections.
|
32
35
|
@sentinels = {}
|
@@ -40,6 +43,9 @@ module Async
|
|
40
43
|
# @attribute [Symbol] The role of the instance that you want to connect to.
|
41
44
|
attr :role
|
42
45
|
|
46
|
+
# Resolve an address for the specified role.
|
47
|
+
# @parameter role [Symbol] The role to resolve (:master or :slave).
|
48
|
+
# @returns [Endpoint] The resolved endpoint address.
|
43
49
|
def resolve_address(role = @role)
|
44
50
|
case role
|
45
51
|
when :master
|
@@ -55,6 +61,7 @@ module Async
|
|
55
61
|
address or raise RuntimeError, "Unable to fetch #{role} via Sentinel."
|
56
62
|
end
|
57
63
|
|
64
|
+
# Close the sentinel client and all connections.
|
58
65
|
def close
|
59
66
|
super
|
60
67
|
|
@@ -63,25 +70,37 @@ module Async
|
|
63
70
|
end
|
64
71
|
end
|
65
72
|
|
73
|
+
# Initiate a failover for the specified master.
|
74
|
+
# @parameter name [String] The name of the master to failover.
|
75
|
+
# @returns [Object] The result of the failover command.
|
66
76
|
def failover(name = @master_name)
|
67
77
|
sentinels do |client|
|
68
78
|
return client.call("SENTINEL", "FAILOVER", name)
|
69
79
|
end
|
70
80
|
end
|
71
81
|
|
82
|
+
# Get information about all masters.
|
83
|
+
# @returns [Array(Hash)] Array of master information hashes.
|
72
84
|
def masters
|
73
85
|
sentinels do |client|
|
74
86
|
return client.call("SENTINEL", "MASTERS").map{|fields| fields.each_slice(2).to_h}
|
75
87
|
end
|
76
88
|
end
|
77
89
|
|
90
|
+
# Get information about a specific master.
|
91
|
+
# @parameter name [String] The name of the master.
|
92
|
+
# @returns [Hash] The master information hash.
|
78
93
|
def master(name = @master_name)
|
79
94
|
sentinels do |client|
|
80
95
|
return client.call("SENTINEL", "MASTER", name).each_slice(2).to_h
|
81
96
|
end
|
82
97
|
end
|
83
98
|
|
84
|
-
|
99
|
+
private
|
100
|
+
|
101
|
+
# Resolve the master endpoint address.
|
102
|
+
# @returns [Endpoint | Nil] The master endpoint or nil if not found.
|
103
|
+
def resolve_master(options = @master_options)
|
85
104
|
sentinels do |client|
|
86
105
|
begin
|
87
106
|
address = client.call("SENTINEL", "GET-MASTER-ADDR-BY-NAME", @master_name)
|
@@ -89,13 +108,15 @@ module Async
|
|
89
108
|
next
|
90
109
|
end
|
91
110
|
|
92
|
-
return Endpoint.
|
111
|
+
return Endpoint.for(nil, address[0], port: address[1], **options) if address
|
93
112
|
end
|
94
113
|
|
95
114
|
return nil
|
96
115
|
end
|
97
116
|
|
98
|
-
|
117
|
+
# Resolve a slave endpoint address.
|
118
|
+
# @returns [Endpoint | Nil] A slave endpoint or nil if not found.
|
119
|
+
def resolve_slave(options = @slave_options)
|
99
120
|
sentinels do |client|
|
100
121
|
begin
|
101
122
|
reply = client.call("SENTINEL", "SLAVES", @master_name)
|
@@ -107,16 +128,14 @@ module Async
|
|
107
128
|
next if slaves.empty?
|
108
129
|
|
109
130
|
slave = select_slave(slaves)
|
110
|
-
return Endpoint.
|
131
|
+
return Endpoint.for(nil, slave["ip"], port: slave["port"], **options)
|
111
132
|
end
|
112
133
|
|
113
134
|
return nil
|
114
135
|
end
|
115
136
|
|
116
|
-
protected
|
117
|
-
|
118
137
|
def assign_default_tags(tags)
|
119
|
-
tags[:
|
138
|
+
tags[:role] ||= @role
|
120
139
|
end
|
121
140
|
|
122
141
|
# Override the parent method. The only difference is that this one needs to resolve the master/slave address.
|
@@ -128,7 +147,7 @@ module Async
|
|
128
147
|
peer = endpoint.connect
|
129
148
|
stream = ::IO::Stream(peer)
|
130
149
|
|
131
|
-
|
150
|
+
endpoint.protocol.client(stream)
|
132
151
|
end
|
133
152
|
end
|
134
153
|
|
data/lib/async/redis/version.rb
CHANGED
data/lib/async/redis.rb
CHANGED
@@ -1,11 +1,19 @@
|
|
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, 2020, by David Ortiz.
|
6
6
|
|
7
7
|
require_relative "redis/version"
|
8
8
|
require_relative "redis/client"
|
9
|
+
require_relative "redis/endpoint"
|
9
10
|
|
10
11
|
require_relative "redis/cluster_client"
|
11
12
|
require_relative "redis/sentinel_client"
|
13
|
+
|
14
|
+
# @namespace
|
15
|
+
module Async
|
16
|
+
# @namespace
|
17
|
+
module Redis
|
18
|
+
end
|
19
|
+
end
|
data/readme.md
CHANGED
@@ -14,10 +14,23 @@ 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
|
+
|
30
|
+
### v0.11.2
|
31
|
+
|
32
|
+
- Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
|
33
|
+
|
21
34
|
### v0.11.1
|
22
35
|
|
23
36
|
- Correctly pass `@options` to `Async::Redis::Client` instances created by `Async::Redis::ClusterClient`.
|
data/releases.md
CHANGED
@@ -1,5 +1,16 @@
|
|
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
|
+
|
10
|
+
## v0.11.2
|
11
|
+
|
12
|
+
- Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
|
13
|
+
|
3
14
|
## v0.11.1
|
4
15
|
|
5
16
|
- Correctly pass `@options` to `Async::Redis::Client` instances created by `Async::Redis::ClusterClient`.
|
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
|
@@ -9,6 +9,7 @@ authors:
|
|
9
9
|
- David Ortiz
|
10
10
|
- Gleb Sinyavskiy
|
11
11
|
- Mikael Henriksson
|
12
|
+
- Travis Bell
|
12
13
|
- Troex Nevelin
|
13
14
|
- Alex Matchneer
|
14
15
|
- Jeremy Jung
|
@@ -17,7 +18,6 @@ authors:
|
|
17
18
|
- Pierre Montelle
|
18
19
|
- Salim Semaoune
|
19
20
|
- Tim Willard
|
20
|
-
- Travis Bell
|
21
21
|
bindir: bin
|
22
22
|
cert_chain:
|
23
23
|
- |
|
@@ -49,7 +49,7 @@ cert_chain:
|
|
49
49
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
50
50
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
51
51
|
-----END CERTIFICATE-----
|
52
|
-
date:
|
52
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
53
53
|
dependencies:
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
55
|
name: async
|
@@ -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
|
@@ -155,14 +160,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
155
160
|
requirements:
|
156
161
|
- - ">="
|
157
162
|
- !ruby/object:Gem::Version
|
158
|
-
version: '3.
|
163
|
+
version: '3.2'
|
159
164
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
165
|
requirements:
|
161
166
|
- - ">="
|
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
|
@@ -1,54 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2018, by Huba Nagy.
|
5
|
-
# Copyright, 2018-2024, by Samuel Williams.
|
6
|
-
|
7
|
-
require_relative "generic"
|
8
|
-
|
9
|
-
module Async
|
10
|
-
module Redis
|
11
|
-
module Context
|
12
|
-
class Subscribe < Generic
|
13
|
-
MESSAGE = "message"
|
14
|
-
|
15
|
-
def initialize(pool, channels)
|
16
|
-
super(pool)
|
17
|
-
|
18
|
-
subscribe(channels)
|
19
|
-
end
|
20
|
-
|
21
|
-
def close
|
22
|
-
# There is no way to reset subscription state. On Redis v6+ you can use RESET, but this is not supported in <= v6.
|
23
|
-
@connection&.close
|
24
|
-
|
25
|
-
super
|
26
|
-
end
|
27
|
-
|
28
|
-
def listen
|
29
|
-
while response = @connection.read_response
|
30
|
-
return response if response.first == MESSAGE
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def each
|
35
|
-
return to_enum unless block_given?
|
36
|
-
|
37
|
-
while response = self.listen
|
38
|
-
yield response
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def subscribe(channels)
|
43
|
-
@connection.write_request ["SUBSCRIBE", *channels]
|
44
|
-
@connection.flush
|
45
|
-
end
|
46
|
-
|
47
|
-
def unsubscribe(channels)
|
48
|
-
@connection.write_request ["UNSUBSCRIBE", *channels]
|
49
|
-
@connection.flush
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|