async-redis 0.9.0 → 0.10.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/lib/async/redis/client.rb +61 -57
- data/lib/async/redis/cluster_client.rb +211 -0
- data/lib/async/redis/endpoint.rb +5 -1
- 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 +3 -2
- 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: 31704f6caaeffba85223e9917ac746128c05862b53738ab42d03f0c91bd808c3
|
4
|
+
data.tar.gz: 72afd9739ce773d441870c238e9f2600a6ac3e3d46b13bb66c3a7f3febf3f4d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b12190eab39bba378d6ca1793abd3e56ab513a13af4bedfbb332d3d7e869d5b61c9d3958eaa96f22987e341e50b6acdc018c84c1d88effea3fee72fd132a7fe
|
7
|
+
data.tar.gz: '086307e4ec9c27d407cce2e61e99c6b4712e1d1fb3b8bca660c7e1a4270a9aa9061c36dd5a801e3edfa42eb52a6f41a41c858830ccc7e673e38917d0a4f9e700'
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
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,211 @@
|
|
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
|
+
Node = Struct.new(:id, :endpoint, :role, :health, :client)
|
17
|
+
|
18
|
+
class RangeMap
|
19
|
+
def initialize
|
20
|
+
@ranges = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def add(range, value)
|
24
|
+
@ranges << [range, value]
|
25
|
+
|
26
|
+
return value
|
27
|
+
end
|
28
|
+
|
29
|
+
def find(key)
|
30
|
+
@ranges.each do |range, value|
|
31
|
+
return value if range.include?(key)
|
32
|
+
end
|
33
|
+
|
34
|
+
if block_given?
|
35
|
+
return yield
|
36
|
+
end
|
37
|
+
|
38
|
+
return nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def each
|
42
|
+
@ranges.each do |range, value|
|
43
|
+
yield value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def clear
|
48
|
+
@ranges.clear
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create a new instance of the cluster client.
|
53
|
+
#
|
54
|
+
# @property endpoints [Array(Endpoint)] The list of cluster endpoints.
|
55
|
+
def initialize(endpoints, **options)
|
56
|
+
@endpoints = endpoints
|
57
|
+
@shards = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def clients_for(*keys, role: :master, attempts: 3)
|
61
|
+
slots = slots_for(keys)
|
62
|
+
|
63
|
+
slots.each do |slot, keys|
|
64
|
+
yield client_for(slot, role), keys
|
65
|
+
end
|
66
|
+
rescue ServerError => error
|
67
|
+
Console.warn(self, error)
|
68
|
+
|
69
|
+
if error.message =~ /MOVED|ASK/
|
70
|
+
reload_cluster!
|
71
|
+
|
72
|
+
attempts -= 1
|
73
|
+
|
74
|
+
retry if attempts > 0
|
75
|
+
|
76
|
+
raise
|
77
|
+
else
|
78
|
+
raise
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def client_for(slot, role = :master)
|
83
|
+
unless @shards
|
84
|
+
reload_cluster!
|
85
|
+
end
|
86
|
+
|
87
|
+
nodes = @shards.find(slot)
|
88
|
+
|
89
|
+
nodes = nodes.select{|node| node.role == role}
|
90
|
+
|
91
|
+
if node = nodes.sample
|
92
|
+
return (node.client ||= Client.new(node.endpoint))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
def reload_cluster!(endpoints = @endpoints)
|
99
|
+
@endpoints.each do |endpoint|
|
100
|
+
client = Client.new(endpoint)
|
101
|
+
|
102
|
+
shards = RangeMap.new
|
103
|
+
endpoints = []
|
104
|
+
|
105
|
+
client.call('CLUSTER', 'SHARDS').each do |shard|
|
106
|
+
shard = shard.each_slice(2).to_h
|
107
|
+
|
108
|
+
slots = shard['slots']
|
109
|
+
range = Range.new(*slots, exclude_end: false)
|
110
|
+
|
111
|
+
nodes = shard['nodes'].map do |node|
|
112
|
+
node = node.each_slice(2).to_h
|
113
|
+
endpoint = Endpoint.remote(node['ip'], node['port'])
|
114
|
+
|
115
|
+
# Collect all endpoints:
|
116
|
+
endpoints << endpoint
|
117
|
+
|
118
|
+
Node.new(node['id'], endpoint, node['role'].to_sym, node['health'].to_sym)
|
119
|
+
end
|
120
|
+
|
121
|
+
shards.add(range, nodes)
|
122
|
+
end
|
123
|
+
|
124
|
+
@shards = shards
|
125
|
+
# @endpoints = @endpoints | endpoints
|
126
|
+
|
127
|
+
return true
|
128
|
+
rescue Errno::ECONNREFUSED
|
129
|
+
next
|
130
|
+
end
|
131
|
+
|
132
|
+
raise ReloadError, "Failed to reload cluster configuration."
|
133
|
+
end
|
134
|
+
|
135
|
+
XMODEM_CRC16_LOOKUP = [
|
136
|
+
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
137
|
+
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
138
|
+
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
139
|
+
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
140
|
+
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
141
|
+
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
142
|
+
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
143
|
+
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
144
|
+
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
145
|
+
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
146
|
+
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
147
|
+
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
148
|
+
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
149
|
+
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
150
|
+
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
151
|
+
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
152
|
+
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
153
|
+
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
154
|
+
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
155
|
+
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
156
|
+
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
157
|
+
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
158
|
+
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
159
|
+
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
160
|
+
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
161
|
+
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
162
|
+
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
163
|
+
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
164
|
+
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
165
|
+
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
166
|
+
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
167
|
+
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
168
|
+
].freeze
|
169
|
+
|
170
|
+
# This is the CRC16 algorithm used by Redis Cluster to hash keys.
|
171
|
+
# Copied from https://github.com/antirez/redis-rb-cluster/blob/master/crc16.rb
|
172
|
+
def crc16(bytes)
|
173
|
+
sum = 0
|
174
|
+
|
175
|
+
bytes.each_byte do |byte|
|
176
|
+
sum = ((sum << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((sum >> 8) ^ byte) & 0xff]
|
177
|
+
end
|
178
|
+
|
179
|
+
return sum
|
180
|
+
end
|
181
|
+
|
182
|
+
HASH_SLOTS = 16_384
|
183
|
+
|
184
|
+
public
|
185
|
+
|
186
|
+
# Return Redis::Client for a given key.
|
187
|
+
# Modified from https://github.com/antirez/redis-rb-cluster/blob/master/cluster.rb#L104-L117
|
188
|
+
def slot_for(key)
|
189
|
+
key = key.to_s
|
190
|
+
|
191
|
+
if s = key.index('{')
|
192
|
+
if e = key.index('}', s + 1) and e != s + 1
|
193
|
+
key = key[s + 1..e - 1]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
return crc16(key) % HASH_SLOTS
|
198
|
+
end
|
199
|
+
|
200
|
+
def slots_for(keys)
|
201
|
+
slots = Hash.new{|hash, key| hash[key] = []}
|
202
|
+
|
203
|
+
keys.each do |key|
|
204
|
+
slots[slot_for(key)] << key
|
205
|
+
end
|
206
|
+
|
207
|
+
return slots
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
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,
|
@@ -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.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -128,6 +128,7 @@ extra_rdoc_files: []
|
|
128
128
|
files:
|
129
129
|
- lib/async/redis.rb
|
130
130
|
- lib/async/redis/client.rb
|
131
|
+
- lib/async/redis/cluster_client.rb
|
131
132
|
- lib/async/redis/context/generic.rb
|
132
133
|
- lib/async/redis/context/pipeline.rb
|
133
134
|
- lib/async/redis/context/subscribe.rb
|
@@ -137,7 +138,7 @@ files:
|
|
137
138
|
- lib/async/redis/protocol/authenticated.rb
|
138
139
|
- lib/async/redis/protocol/resp2.rb
|
139
140
|
- lib/async/redis/protocol/selected.rb
|
140
|
-
- lib/async/redis/
|
141
|
+
- lib/async/redis/sentinel_client.rb
|
141
142
|
- lib/async/redis/version.rb
|
142
143
|
- license.md
|
143
144
|
- 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
|