async-redis 0.9.0 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/changes.md +46 -0
- data/lib/async/redis/client.rb +61 -57
- data/lib/async/redis/cluster_client.rb +216 -0
- data/lib/async/redis/endpoint.rb +10 -5
- data/lib/async/redis/sentinel_client.rb +153 -0
- data/lib/async/redis/version.rb +1 -1
- data/lib/async/redis.rb +3 -1
- data.tar.gz.sig +0 -0
- metadata +5 -3
- metadata.gz.sig +0 -0
- data/lib/async/redis/sentinels.rb +0 -104
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da847e343c291506a20337eec6170867a0e0bf295b713ece60fd7f48928d873f
|
4
|
+
data.tar.gz: 2ea6e2720f556084f57415501a1964c0bb2a93a411a9cb52138dbb8ed141c45b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ac74325525a0dc00b06aaee1fa505566bce74f4806c07f26a8581ac7b20db79f11cbb052d6d598d02b7eec94aae6a142d88da00eb01e4b0a8d72e134ef5a798
|
7
|
+
data.tar.gz: 2b873fd8ee1cfffda74d301265ec9477a34dfe571d30d4c36d195cf39b089c22635bc74c07ade143ca46a5e169d0db226470a6149ce912be3b3da0db96e26b49
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/changes.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# v0.10.0
|
2
|
+
|
3
|
+
## Cluster Client
|
4
|
+
|
5
|
+
`Async::Redis::ClusterClient` is a new class that provides a high-level interface to a Redis Cluster. Due to the way clustering works, it does not provide the same interface as the `Async::Redis::Client` class. Instead, you must request an appropriate client for the key you are working with.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
endpoints = [
|
9
|
+
Async::Redis::Endpoint.parse("redis://redis-a"),
|
10
|
+
Async::Redis::Endpoint.parse("redis://redis-b"),
|
11
|
+
Async::Redis::Endpoint.parse("redis://redis-c"),
|
12
|
+
]
|
13
|
+
|
14
|
+
cluster_client = Async::Redis::ClusterClient.new(endpoints)
|
15
|
+
|
16
|
+
cluster_client.clients_for("key") do |client|
|
17
|
+
puts client.get("key")
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
## Sentinel Client
|
22
|
+
|
23
|
+
The previous implementation `Async::Redis::SentinelsClient` has been replaced with `Async::Redis::SentinelClient`. This new class uses `Async::Redis::Endpoint` objects to represent the sentinels and the master.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
sentinels = [
|
27
|
+
Async::Redis::Endpoint.parse("redis://redis-sentinel-a"),
|
28
|
+
Async::Redis::Endpoint.parse("redis://redis-sentinel-b"),
|
29
|
+
Async::Redis::Endpoint.parse("redis://redis-sentinel-c"),
|
30
|
+
]
|
31
|
+
|
32
|
+
master_client = Async::Redis::SentinelClient.new(sentinels)
|
33
|
+
slave_client = Async::Redis::SentinelClient.new(sentinels, role: :slave)
|
34
|
+
|
35
|
+
master_client.session do |session|
|
36
|
+
session.set("key", "value")
|
37
|
+
end
|
38
|
+
|
39
|
+
slave_client.session do |session|
|
40
|
+
puts session.get("key")
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
## Integration Tests
|
45
|
+
|
46
|
+
Integration tests for Redis Cluster and Sentinel have been added, using `docker-compose` to start the required services and run the tests. These tests are not part of the default test suite and must be run separately. See the documentation in the `sentinel/` and `cluster/` directories for more information.
|
data/lib/async/redis/client.rb
CHANGED
@@ -26,6 +26,65 @@ module Async
|
|
26
26
|
class Client
|
27
27
|
include ::Protocol::Redis::Methods
|
28
28
|
|
29
|
+
module Methods
|
30
|
+
def subscribe(*channels)
|
31
|
+
context = Context::Subscribe.new(@pool, channels)
|
32
|
+
|
33
|
+
return context unless block_given?
|
34
|
+
|
35
|
+
begin
|
36
|
+
yield context
|
37
|
+
ensure
|
38
|
+
context.close
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def transaction(&block)
|
43
|
+
context = Context::Transaction.new(@pool)
|
44
|
+
|
45
|
+
return context unless block_given?
|
46
|
+
|
47
|
+
begin
|
48
|
+
yield context
|
49
|
+
ensure
|
50
|
+
context.close
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
alias multi transaction
|
55
|
+
|
56
|
+
def pipeline(&block)
|
57
|
+
context = Context::Pipeline.new(@pool)
|
58
|
+
|
59
|
+
return context unless block_given?
|
60
|
+
|
61
|
+
begin
|
62
|
+
yield context
|
63
|
+
ensure
|
64
|
+
context.close
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Deprecated.
|
69
|
+
alias nested pipeline
|
70
|
+
|
71
|
+
def call(*arguments)
|
72
|
+
@pool.acquire do |connection|
|
73
|
+
connection.write_request(arguments)
|
74
|
+
|
75
|
+
connection.flush
|
76
|
+
|
77
|
+
return connection.read_response
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def close
|
82
|
+
@pool.close
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
include Methods
|
87
|
+
|
29
88
|
def initialize(endpoint = Endpoint.local, protocol: endpoint.protocol, **options)
|
30
89
|
@endpoint = endpoint
|
31
90
|
@protocol = protocol
|
@@ -38,8 +97,8 @@ module Async
|
|
38
97
|
|
39
98
|
# @return [client] if no block provided.
|
40
99
|
# @yield [client, task] yield the client in an async task.
|
41
|
-
def self.open(*arguments, &block)
|
42
|
-
client = self.new(*arguments)
|
100
|
+
def self.open(*arguments, **options, &block)
|
101
|
+
client = self.new(*arguments, **options)
|
43
102
|
|
44
103
|
return client unless block_given?
|
45
104
|
|
@@ -52,61 +111,6 @@ module Async
|
|
52
111
|
end.wait
|
53
112
|
end
|
54
113
|
|
55
|
-
def close
|
56
|
-
@pool.close
|
57
|
-
end
|
58
|
-
|
59
|
-
def subscribe(*channels)
|
60
|
-
context = Context::Subscribe.new(@pool, channels)
|
61
|
-
|
62
|
-
return context unless block_given?
|
63
|
-
|
64
|
-
begin
|
65
|
-
yield context
|
66
|
-
ensure
|
67
|
-
context.close
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def transaction(&block)
|
72
|
-
context = Context::Transaction.new(@pool)
|
73
|
-
|
74
|
-
return context unless block_given?
|
75
|
-
|
76
|
-
begin
|
77
|
-
yield context
|
78
|
-
ensure
|
79
|
-
context.close
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
alias multi transaction
|
84
|
-
|
85
|
-
def pipeline(&block)
|
86
|
-
context = Context::Pipeline.new(@pool)
|
87
|
-
|
88
|
-
return context unless block_given?
|
89
|
-
|
90
|
-
begin
|
91
|
-
yield context
|
92
|
-
ensure
|
93
|
-
context.close
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
# Deprecated.
|
98
|
-
alias nested pipeline
|
99
|
-
|
100
|
-
def call(*arguments)
|
101
|
-
@pool.acquire do |connection|
|
102
|
-
connection.write_request(arguments)
|
103
|
-
|
104
|
-
connection.flush
|
105
|
-
|
106
|
-
return connection.read_response
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
114
|
protected
|
111
115
|
|
112
116
|
def connect(**options)
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2020, by David Ortiz.
|
5
|
+
# Copyright, 2023-2024, by Samuel Williams.
|
6
|
+
|
7
|
+
require_relative 'client'
|
8
|
+
require 'io/stream'
|
9
|
+
|
10
|
+
module Async
|
11
|
+
module Redis
|
12
|
+
class ClusterClient
|
13
|
+
class ReloadError < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
class SlotError < StandardError
|
17
|
+
end
|
18
|
+
|
19
|
+
Node = Struct.new(:id, :endpoint, :role, :health, :client)
|
20
|
+
|
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
|
+
# Create a new instance of the cluster client.
|
56
|
+
#
|
57
|
+
# @property endpoints [Array(Endpoint)] The list of cluster endpoints.
|
58
|
+
def initialize(endpoints, **options)
|
59
|
+
@endpoints = endpoints
|
60
|
+
@shards = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def clients_for(*keys, role: :master, attempts: 3)
|
64
|
+
slots = slots_for(keys)
|
65
|
+
|
66
|
+
slots.each do |slot, keys|
|
67
|
+
yield client_for(slot, role), keys
|
68
|
+
end
|
69
|
+
rescue ServerError => error
|
70
|
+
Console.warn(self, error)
|
71
|
+
|
72
|
+
if error.message =~ /MOVED|ASK/
|
73
|
+
reload_cluster!
|
74
|
+
|
75
|
+
attempts -= 1
|
76
|
+
|
77
|
+
retry if attempts > 0
|
78
|
+
|
79
|
+
raise
|
80
|
+
else
|
81
|
+
raise
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def client_for(slot, role = :master)
|
86
|
+
unless @shards
|
87
|
+
reload_cluster!
|
88
|
+
end
|
89
|
+
|
90
|
+
if nodes = @shards.find(slot)
|
91
|
+
nodes = nodes.select{|node| node.role == role}
|
92
|
+
else
|
93
|
+
raise SlotError, "No nodes found for slot #{slot}"
|
94
|
+
end
|
95
|
+
|
96
|
+
if node = nodes.sample
|
97
|
+
return (node.client ||= Client.new(node.endpoint))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
102
|
+
|
103
|
+
def reload_cluster!(endpoints = @endpoints)
|
104
|
+
@endpoints.each do |endpoint|
|
105
|
+
client = Client.new(endpoint)
|
106
|
+
|
107
|
+
shards = RangeMap.new
|
108
|
+
endpoints = []
|
109
|
+
|
110
|
+
client.call('CLUSTER', 'SHARDS').each do |shard|
|
111
|
+
shard = shard.each_slice(2).to_h
|
112
|
+
|
113
|
+
slots = shard['slots']
|
114
|
+
range = Range.new(*slots)
|
115
|
+
|
116
|
+
nodes = shard['nodes'].map do |node|
|
117
|
+
node = node.each_slice(2).to_h
|
118
|
+
endpoint = Endpoint.remote(node['ip'], node['port'])
|
119
|
+
|
120
|
+
# Collect all endpoints:
|
121
|
+
endpoints << endpoint
|
122
|
+
|
123
|
+
Node.new(node['id'], endpoint, node['role'].to_sym, node['health'].to_sym)
|
124
|
+
end
|
125
|
+
|
126
|
+
shards.add(range, nodes)
|
127
|
+
end
|
128
|
+
|
129
|
+
@shards = shards
|
130
|
+
# @endpoints = @endpoints | endpoints
|
131
|
+
|
132
|
+
return true
|
133
|
+
rescue Errno::ECONNREFUSED
|
134
|
+
next
|
135
|
+
end
|
136
|
+
|
137
|
+
raise ReloadError, "Failed to reload cluster configuration."
|
138
|
+
end
|
139
|
+
|
140
|
+
XMODEM_CRC16_LOOKUP = [
|
141
|
+
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
142
|
+
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
143
|
+
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
144
|
+
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
145
|
+
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
146
|
+
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
147
|
+
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
148
|
+
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
149
|
+
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
150
|
+
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
151
|
+
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
152
|
+
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
153
|
+
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
154
|
+
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
155
|
+
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
156
|
+
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
157
|
+
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
158
|
+
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
159
|
+
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
160
|
+
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
161
|
+
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
162
|
+
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
163
|
+
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
164
|
+
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
165
|
+
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
166
|
+
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
167
|
+
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
168
|
+
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
169
|
+
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
170
|
+
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
171
|
+
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
172
|
+
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
173
|
+
].freeze
|
174
|
+
|
175
|
+
# This is the CRC16 algorithm used by Redis Cluster to hash keys.
|
176
|
+
# Copied from https://github.com/antirez/redis-rb-cluster/blob/master/crc16.rb
|
177
|
+
def crc16(bytes)
|
178
|
+
sum = 0
|
179
|
+
|
180
|
+
bytes.each_byte do |byte|
|
181
|
+
sum = ((sum << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((sum >> 8) ^ byte) & 0xff]
|
182
|
+
end
|
183
|
+
|
184
|
+
return sum
|
185
|
+
end
|
186
|
+
|
187
|
+
public
|
188
|
+
|
189
|
+
HASH_SLOTS = 16_384
|
190
|
+
|
191
|
+
# Return Redis::Client for a given key.
|
192
|
+
# Modified from https://github.com/antirez/redis-rb-cluster/blob/master/cluster.rb#L104-L117
|
193
|
+
def slot_for(key)
|
194
|
+
key = key.to_s
|
195
|
+
|
196
|
+
if s = key.index('{')
|
197
|
+
if e = key.index('}', s + 1) and e != s + 1
|
198
|
+
key = key[s + 1..e - 1]
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
return crc16(key) % HASH_SLOTS
|
203
|
+
end
|
204
|
+
|
205
|
+
def slots_for(keys)
|
206
|
+
slots = Hash.new{|hash, key| hash[key] = []}
|
207
|
+
|
208
|
+
keys.each do |key|
|
209
|
+
slots[slot_for(key)] << key
|
210
|
+
end
|
211
|
+
|
212
|
+
return slots
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
data/lib/async/redis/endpoint.rb
CHANGED
@@ -23,7 +23,11 @@ module Async
|
|
23
23
|
|
24
24
|
def self.local(**options)
|
25
25
|
self.new(LOCALHOST, **options)
|
26
|
-
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.remote(host, port = 6379, **options)
|
29
|
+
self.new(URI.parse("redis://#{host}:#{port}"), **options)
|
30
|
+
end
|
27
31
|
|
28
32
|
SCHEMES = {
|
29
33
|
'redis' => URI::Generic,
|
@@ -40,7 +44,7 @@ module Async
|
|
40
44
|
#
|
41
45
|
# @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
|
42
46
|
# @parameter hostname [String] The hostname to connect to (or bind to).
|
43
|
-
# @parameter
|
47
|
+
# @parameter options [Hash] Additional options, passed to {#initialize}.
|
44
48
|
def self.for(scheme, hostname, credentials: nil, port: nil, database: nil, **options)
|
45
49
|
uri_klass = SCHEMES.fetch(scheme.downcase) do
|
46
50
|
raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
|
@@ -70,9 +74,10 @@ module Async
|
|
70
74
|
#
|
71
75
|
# @parameter url [URI] The URL to connect to.
|
72
76
|
# @parameter endpoint [Endpoint] The underlying endpoint to use.
|
73
|
-
# @
|
74
|
-
# @
|
75
|
-
# @
|
77
|
+
# @option scheme [String] The scheme to use, e.g. "redis" or "rediss".
|
78
|
+
# @option hostname [String] The hostname to connect to (or bind to), overrides the URL hostname (used for SNI).
|
79
|
+
# @option port [Integer] The port to bind to, overrides the URL port.
|
80
|
+
# @option ssl_context [OpenSSL::SSL::SSLContext] The SSL context to use for secure connections.
|
76
81
|
def initialize(url, endpoint = nil, **options)
|
77
82
|
super(**options)
|
78
83
|
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2020, by David Ortiz.
|
5
|
+
# Copyright, 2023-2024, by Samuel Williams.
|
6
|
+
|
7
|
+
require_relative 'client'
|
8
|
+
require 'io/stream'
|
9
|
+
|
10
|
+
module Async
|
11
|
+
module Redis
|
12
|
+
class SentinelClient
|
13
|
+
DEFAULT_MASTER_NAME = 'mymaster'
|
14
|
+
|
15
|
+
include ::Protocol::Redis::Methods
|
16
|
+
include Client::Methods
|
17
|
+
|
18
|
+
# Create a new instance of the SentinelClient.
|
19
|
+
#
|
20
|
+
# @property endpoints [Array(Endpoint)] The list of sentinel endpoints.
|
21
|
+
# @property master_name [String] The name of the master instance, defaults to 'mymaster'.
|
22
|
+
# @property role [Symbol] The role of the instance that you want to connect to, either `:master` or `:slave`.
|
23
|
+
# @property protocol [Protocol] The protocol to use when connecting to the actual Redis server, defaults to {Protocol::RESP2}.
|
24
|
+
def initialize(endpoints, master_name: DEFAULT_MASTER_NAME, role: :master, protocol: Protocol::RESP2, **options)
|
25
|
+
@endpoints = endpoints
|
26
|
+
@master_name = master_name
|
27
|
+
@role = role
|
28
|
+
@protocol = protocol
|
29
|
+
|
30
|
+
# A cache of sentinel connections.
|
31
|
+
@sentinels = {}
|
32
|
+
|
33
|
+
@pool = connect(**options)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @attribute [String] The name of the master instance.
|
37
|
+
attr :master_name
|
38
|
+
|
39
|
+
# @attribute [Symbol] The role of the instance that you want to connect to.
|
40
|
+
attr :role
|
41
|
+
|
42
|
+
def resolve_address(role = @role)
|
43
|
+
case role
|
44
|
+
when :master
|
45
|
+
resolve_master
|
46
|
+
when :slave
|
47
|
+
resolve_slave
|
48
|
+
else
|
49
|
+
raise ArgumentError, "Unknown instance role #{role}"
|
50
|
+
end => address
|
51
|
+
|
52
|
+
Console.debug(self, "Resolved #{@role} address: #{address}")
|
53
|
+
|
54
|
+
address or raise RuntimeError, "Unable to fetch #{role} via Sentinel."
|
55
|
+
end
|
56
|
+
|
57
|
+
def close
|
58
|
+
super
|
59
|
+
|
60
|
+
@sentinels.each do |_, client|
|
61
|
+
client.close
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def failover(name = @master_name)
|
66
|
+
sentinels do |client|
|
67
|
+
return client.call('SENTINEL', 'FAILOVER', name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def masters
|
72
|
+
sentinels do |client|
|
73
|
+
return client.call('SENTINEL', 'MASTERS').map{|fields| fields.each_slice(2).to_h}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def master(name = @master_name)
|
78
|
+
sentinels do |client|
|
79
|
+
return client.call('SENTINEL', 'MASTER', name).each_slice(2).to_h
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def resolve_master
|
84
|
+
sentinels do |client|
|
85
|
+
begin
|
86
|
+
address = client.call('SENTINEL', 'GET-MASTER-ADDR-BY-NAME', @master_name)
|
87
|
+
rescue Errno::ECONNREFUSED
|
88
|
+
next
|
89
|
+
end
|
90
|
+
|
91
|
+
return Endpoint.remote(address[0], address[1]) if address
|
92
|
+
end
|
93
|
+
|
94
|
+
return nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def resolve_slave
|
98
|
+
sentinels do |client|
|
99
|
+
begin
|
100
|
+
reply = client.call('SENTINEL', 'SLAVES', @master_name)
|
101
|
+
rescue Errno::ECONNREFUSED
|
102
|
+
next
|
103
|
+
end
|
104
|
+
|
105
|
+
slaves = available_slaves(reply)
|
106
|
+
next if slaves.empty?
|
107
|
+
|
108
|
+
slave = select_slave(slaves)
|
109
|
+
return Endpoint.remote(slave['ip'], slave['port'])
|
110
|
+
end
|
111
|
+
|
112
|
+
return nil
|
113
|
+
end
|
114
|
+
|
115
|
+
protected
|
116
|
+
|
117
|
+
# Override the parent method. The only difference is that this one needs to resolve the master/slave address.
|
118
|
+
def connect(**options)
|
119
|
+
Async::Pool::Controller.wrap(**options) do
|
120
|
+
endpoint = resolve_address
|
121
|
+
peer = endpoint.connect
|
122
|
+
stream = ::IO::Stream(peer)
|
123
|
+
|
124
|
+
@protocol.client(stream)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def sentinels
|
129
|
+
@endpoints.map do |endpoint|
|
130
|
+
@sentinels[endpoint] ||= Client.new(endpoint)
|
131
|
+
|
132
|
+
yield @sentinels[endpoint]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def available_slaves(reply)
|
137
|
+
# The reply is an array with the format: [field1, value1, field2,
|
138
|
+
# value2, etc.].
|
139
|
+
# When a slave is marked as down by the sentinel, the "flags" field
|
140
|
+
# (comma-separated array) contains the "s_down" value.
|
141
|
+
slaves = reply.map{|fields| fields.each_slice(2).to_h}
|
142
|
+
|
143
|
+
slaves.reject do |slave|
|
144
|
+
slave['flags'].split(',').include?('s_down')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def select_slave(available_slaves)
|
149
|
+
available_slaves.sample
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/async/redis/version.rb
CHANGED
data/lib/async/redis.rb
CHANGED
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.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -48,7 +48,7 @@ cert_chain:
|
|
48
48
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
49
49
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
50
50
|
-----END CERTIFICATE-----
|
51
|
-
date: 2024-08-
|
51
|
+
date: 2024-08-22 00:00:00.000000000 Z
|
52
52
|
dependencies:
|
53
53
|
- !ruby/object:Gem::Dependency
|
54
54
|
name: async
|
@@ -126,8 +126,10 @@ executables: []
|
|
126
126
|
extensions: []
|
127
127
|
extra_rdoc_files: []
|
128
128
|
files:
|
129
|
+
- changes.md
|
129
130
|
- lib/async/redis.rb
|
130
131
|
- lib/async/redis/client.rb
|
132
|
+
- lib/async/redis/cluster_client.rb
|
131
133
|
- lib/async/redis/context/generic.rb
|
132
134
|
- lib/async/redis/context/pipeline.rb
|
133
135
|
- lib/async/redis/context/subscribe.rb
|
@@ -137,7 +139,7 @@ files:
|
|
137
139
|
- lib/async/redis/protocol/authenticated.rb
|
138
140
|
- lib/async/redis/protocol/resp2.rb
|
139
141
|
- lib/async/redis/protocol/selected.rb
|
140
|
-
- lib/async/redis/
|
142
|
+
- lib/async/redis/sentinel_client.rb
|
141
143
|
- lib/async/redis/version.rb
|
142
144
|
- license.md
|
143
145
|
- readme.md
|
metadata.gz.sig
CHANGED
Binary file
|
@@ -1,104 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2020, by David Ortiz.
|
5
|
-
# Copyright, 2023-2024, by Samuel Williams.
|
6
|
-
|
7
|
-
require 'io/stream'
|
8
|
-
|
9
|
-
module Async
|
10
|
-
module Redis
|
11
|
-
class SentinelsClient < Client
|
12
|
-
def initialize(master_name, sentinels, role = :master, protocol = Protocol::RESP2, **options)
|
13
|
-
@master_name = master_name
|
14
|
-
|
15
|
-
@sentinel_endpoints = sentinels.map do |sentinel|
|
16
|
-
::IO::Endpoint.tcp(sentinel[:host], sentinel[:port])
|
17
|
-
end
|
18
|
-
|
19
|
-
@role = role
|
20
|
-
@protocol = protocol
|
21
|
-
@pool = connect(**options)
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
# Override the parent method. The only difference is that this one needs
|
27
|
-
# to resolve the master/slave address.
|
28
|
-
def connect(**options)
|
29
|
-
Async::Pool::Controller.wrap(**options) do
|
30
|
-
endpoint = resolve_address
|
31
|
-
peer = endpoint.connect
|
32
|
-
stream = ::IO::Stream(peer)
|
33
|
-
|
34
|
-
@protocol.client(stream)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def resolve_address
|
39
|
-
case @role
|
40
|
-
when :master
|
41
|
-
resolve_master
|
42
|
-
when :slave
|
43
|
-
resolve_slave
|
44
|
-
else
|
45
|
-
raise ArgumentError, "Unknown instance role #{@role}"
|
46
|
-
end => address
|
47
|
-
|
48
|
-
address or raise RuntimeError, "Unable to fetch #{@role} via Sentinel."
|
49
|
-
end
|
50
|
-
|
51
|
-
def resolve_master
|
52
|
-
@sentinel_endpoints.each do |sentinel_endpoint|
|
53
|
-
client = Client.new(sentinel_endpoint, protocol: @protocol)
|
54
|
-
|
55
|
-
begin
|
56
|
-
address = client.call('sentinel', 'get-master-addr-by-name', @master_name)
|
57
|
-
rescue Errno::ECONNREFUSED
|
58
|
-
next
|
59
|
-
end
|
60
|
-
|
61
|
-
return ::IO::Endpoint.tcp(address[0], address[1]) if address
|
62
|
-
end
|
63
|
-
|
64
|
-
nil
|
65
|
-
end
|
66
|
-
|
67
|
-
def resolve_slave
|
68
|
-
@sentinel_endpoints.each do |sentinel_endpoint|
|
69
|
-
client = Client.new(sentinel_endpoint, protocol: @protocol)
|
70
|
-
|
71
|
-
begin
|
72
|
-
reply = client.call('sentinel', 'slaves', @master_name)
|
73
|
-
rescue Errno::ECONNREFUSED
|
74
|
-
next
|
75
|
-
end
|
76
|
-
|
77
|
-
slaves = available_slaves(reply)
|
78
|
-
next if slaves.empty?
|
79
|
-
|
80
|
-
slave = select_slave(slaves)
|
81
|
-
return ::IO::Endpoint.tcp(slave['ip'], slave['port'])
|
82
|
-
end
|
83
|
-
|
84
|
-
nil
|
85
|
-
end
|
86
|
-
|
87
|
-
def available_slaves(reply)
|
88
|
-
# The reply is an array with the format: [field1, value1, field2,
|
89
|
-
# value2, etc.].
|
90
|
-
# When a slave is marked as down by the sentinel, the "flags" field
|
91
|
-
# (comma-separated array) contains the "s_down" value.
|
92
|
-
slaves = reply.map{|fields| fields.each_slice(2).to_h}
|
93
|
-
|
94
|
-
slaves.reject do |slave|
|
95
|
-
slave['flags'].split(',').include?('s_down')
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def select_slave(available_slaves)
|
100
|
-
available_slaves.sample
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|