async-redis 0.8.1 → 0.10.0
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/lib/async/redis/client.rb +63 -63
- data/lib/async/redis/cluster_client.rb +211 -0
- data/lib/async/redis/context/pipeline.rb +13 -10
- data/lib/async/redis/endpoint.rb +272 -0
- data/lib/async/redis/protocol/authenticated.rb +44 -0
- data/lib/async/redis/protocol/selected.rb +44 -0
- data/lib/async/redis/sentinel_client.rb +153 -0
- data/lib/async/redis/version.rb +2 -2
- data/lib/async/redis.rb +3 -1
- data/readme.md +5 -5
- data.tar.gz.sig +0 -0
- metadata +15 -16
- metadata.gz.sig +0 -0
- data/lib/async/redis/sentinels.rb +0 -97
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
@@ -10,7 +10,7 @@
|
|
10
10
|
require_relative 'context/pipeline'
|
11
11
|
require_relative 'context/transaction'
|
12
12
|
require_relative 'context/subscribe'
|
13
|
-
require_relative '
|
13
|
+
require_relative 'endpoint'
|
14
14
|
|
15
15
|
require 'io/endpoint/host_endpoint'
|
16
16
|
require 'async/pool/controller'
|
@@ -23,14 +23,69 @@ module Async
|
|
23
23
|
# Legacy.
|
24
24
|
ServerError = ::Protocol::Redis::ServerError
|
25
25
|
|
26
|
-
def self.local_endpoint(port: 6379)
|
27
|
-
::IO::Endpoint.tcp('localhost', port)
|
28
|
-
end
|
29
|
-
|
30
26
|
class Client
|
31
27
|
include ::Protocol::Redis::Methods
|
32
28
|
|
33
|
-
|
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
|
+
|
88
|
+
def initialize(endpoint = Endpoint.local, protocol: endpoint.protocol, **options)
|
34
89
|
@endpoint = endpoint
|
35
90
|
@protocol = protocol
|
36
91
|
|
@@ -42,8 +97,8 @@ module Async
|
|
42
97
|
|
43
98
|
# @return [client] if no block provided.
|
44
99
|
# @yield [client, task] yield the client in an async task.
|
45
|
-
def self.open(*arguments, &block)
|
46
|
-
client = self.new(*arguments)
|
100
|
+
def self.open(*arguments, **options, &block)
|
101
|
+
client = self.new(*arguments, **options)
|
47
102
|
|
48
103
|
return client unless block_given?
|
49
104
|
|
@@ -56,61 +111,6 @@ module Async
|
|
56
111
|
end.wait
|
57
112
|
end
|
58
113
|
|
59
|
-
def close
|
60
|
-
@pool.close
|
61
|
-
end
|
62
|
-
|
63
|
-
def subscribe(*channels)
|
64
|
-
context = Context::Subscribe.new(@pool, channels)
|
65
|
-
|
66
|
-
return context unless block_given?
|
67
|
-
|
68
|
-
begin
|
69
|
-
yield context
|
70
|
-
ensure
|
71
|
-
context.close
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def transaction(&block)
|
76
|
-
context = Context::Transaction.new(@pool)
|
77
|
-
|
78
|
-
return context unless block_given?
|
79
|
-
|
80
|
-
begin
|
81
|
-
yield context
|
82
|
-
ensure
|
83
|
-
context.close
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
alias multi transaction
|
88
|
-
|
89
|
-
def pipeline(&block)
|
90
|
-
context = Context::Pipeline.new(@pool)
|
91
|
-
|
92
|
-
return context unless block_given?
|
93
|
-
|
94
|
-
begin
|
95
|
-
yield context
|
96
|
-
ensure
|
97
|
-
context.close
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
# Deprecated.
|
102
|
-
alias nested pipeline
|
103
|
-
|
104
|
-
def call(*arguments)
|
105
|
-
@pool.acquire do |connection|
|
106
|
-
connection.write_request(arguments)
|
107
|
-
|
108
|
-
connection.flush
|
109
|
-
|
110
|
-
return connection.read_response
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
114
|
protected
|
115
115
|
|
116
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
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2019, by David Ortiz.
|
5
|
-
# Copyright, 2019-
|
5
|
+
# Copyright, 2019-2024, by Samuel Williams.
|
6
6
|
# Copyright, 2022, by Tim Willard.
|
7
7
|
|
8
8
|
require_relative 'generic'
|
@@ -22,8 +22,8 @@ module Async
|
|
22
22
|
end
|
23
23
|
|
24
24
|
# This method just accumulates the commands and their params.
|
25
|
-
def call(
|
26
|
-
@pipeline.call(
|
25
|
+
def call(...)
|
26
|
+
@pipeline.call(...)
|
27
27
|
|
28
28
|
@pipeline.flush(1)
|
29
29
|
|
@@ -46,6 +46,15 @@ module Async
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
+
def collect
|
50
|
+
if block_given?
|
51
|
+
flush
|
52
|
+
yield
|
53
|
+
end
|
54
|
+
|
55
|
+
@count.times.map{read_response}
|
56
|
+
end
|
57
|
+
|
49
58
|
def sync
|
50
59
|
@sync ||= Sync.new(self)
|
51
60
|
end
|
@@ -73,15 +82,9 @@ module Async
|
|
73
82
|
end
|
74
83
|
end
|
75
84
|
|
76
|
-
def collect
|
77
|
-
yield
|
78
|
-
|
79
|
-
@count.times.map{read_response}
|
80
|
-
end
|
81
|
-
|
82
85
|
def close
|
83
86
|
flush
|
84
|
-
|
87
|
+
ensure
|
85
88
|
super
|
86
89
|
end
|
87
90
|
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require 'io/endpoint'
|
7
|
+
require 'io/endpoint/host_endpoint'
|
8
|
+
require 'io/endpoint/ssl_endpoint'
|
9
|
+
|
10
|
+
require_relative 'protocol/resp2'
|
11
|
+
require_relative 'protocol/authenticated'
|
12
|
+
require_relative 'protocol/selected'
|
13
|
+
|
14
|
+
module Async
|
15
|
+
module Redis
|
16
|
+
def self.local_endpoint(**options)
|
17
|
+
Endpoint.local(**options)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Represents a way to connect to a remote Redis server.
|
21
|
+
class Endpoint < ::IO::Endpoint::Generic
|
22
|
+
LOCALHOST = URI.parse("redis://localhost").freeze
|
23
|
+
|
24
|
+
def self.local(**options)
|
25
|
+
self.new(LOCALHOST, **options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.remote(host, port = 6379, **options)
|
29
|
+
self.new(URI.parse("redis://#{host}:#{port}"), **options)
|
30
|
+
end
|
31
|
+
|
32
|
+
SCHEMES = {
|
33
|
+
'redis' => URI::Generic,
|
34
|
+
'rediss' => URI::Generic,
|
35
|
+
}
|
36
|
+
|
37
|
+
def self.parse(string, endpoint = nil, **options)
|
38
|
+
url = URI.parse(string).normalize
|
39
|
+
|
40
|
+
return self.new(url, endpoint, **options)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Construct an endpoint with a specified scheme, hostname, optional path, and options.
|
44
|
+
#
|
45
|
+
# @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
|
46
|
+
# @parameter hostname [String] The hostname to connect to (or bind to).
|
47
|
+
# @parameter *options [Hash] Additional options, passed to {#initialize}.
|
48
|
+
def self.for(scheme, hostname, credentials: nil, port: nil, database: nil, **options)
|
49
|
+
uri_klass = SCHEMES.fetch(scheme.downcase) do
|
50
|
+
raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
|
51
|
+
end
|
52
|
+
|
53
|
+
if database
|
54
|
+
path = "/#{database}"
|
55
|
+
end
|
56
|
+
|
57
|
+
self.new(
|
58
|
+
uri_klass.new(scheme, credentials&.join(":"), hostname, port, nil, path, nil, nil, nil).normalize,
|
59
|
+
**options
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Coerce the given object into an endpoint.
|
64
|
+
# @parameter url [String | Endpoint] The URL or endpoint to convert.
|
65
|
+
def self.[](object)
|
66
|
+
if object.is_a?(self)
|
67
|
+
return object
|
68
|
+
else
|
69
|
+
self.parse(object.to_s)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Create a new endpoint.
|
74
|
+
#
|
75
|
+
# @parameter url [URI] The URL to connect to.
|
76
|
+
# @parameter endpoint [Endpoint] The underlying endpoint to use.
|
77
|
+
# @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
|
78
|
+
# @parameter hostname [String] The hostname to connect to (or bind to), overrides the URL hostname (used for SNI).
|
79
|
+
# @parameter port [Integer] The port to bind to, overrides the URL port.
|
80
|
+
def initialize(url, endpoint = nil, **options)
|
81
|
+
super(**options)
|
82
|
+
|
83
|
+
raise ArgumentError, "URL must be absolute (include scheme, host): #{url}" unless url.absolute?
|
84
|
+
|
85
|
+
@url = url
|
86
|
+
|
87
|
+
if endpoint
|
88
|
+
@endpoint = self.build_endpoint(endpoint)
|
89
|
+
else
|
90
|
+
@endpoint = nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_url
|
95
|
+
url = @url.dup
|
96
|
+
|
97
|
+
unless default_port?
|
98
|
+
url.port = self.port
|
99
|
+
end
|
100
|
+
|
101
|
+
return url
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_s
|
105
|
+
"\#<#{self.class} #{self.to_url} #{@options}>"
|
106
|
+
end
|
107
|
+
|
108
|
+
def inspect
|
109
|
+
"\#<#{self.class} #{self.to_url} #{@options.inspect}>"
|
110
|
+
end
|
111
|
+
|
112
|
+
attr :url
|
113
|
+
|
114
|
+
def address
|
115
|
+
endpoint.address
|
116
|
+
end
|
117
|
+
|
118
|
+
def secure?
|
119
|
+
['rediss'].include?(self.scheme)
|
120
|
+
end
|
121
|
+
|
122
|
+
def protocol
|
123
|
+
protocol = @options.fetch(:protocol, Protocol::RESP2)
|
124
|
+
|
125
|
+
if credentials = self.credentials
|
126
|
+
protocol = Protocol::Authenticated.new(credentials, protocol)
|
127
|
+
end
|
128
|
+
|
129
|
+
if database = self.database
|
130
|
+
protocol = Protocol::Selected.new(database, protocol)
|
131
|
+
end
|
132
|
+
|
133
|
+
return protocol
|
134
|
+
end
|
135
|
+
|
136
|
+
def default_port
|
137
|
+
6379
|
138
|
+
end
|
139
|
+
|
140
|
+
def default_port?
|
141
|
+
port == default_port
|
142
|
+
end
|
143
|
+
|
144
|
+
def port
|
145
|
+
@options[:port] || @url.port || default_port
|
146
|
+
end
|
147
|
+
|
148
|
+
# The hostname is the server we are connecting to:
|
149
|
+
def hostname
|
150
|
+
@options[:hostname] || @url.hostname
|
151
|
+
end
|
152
|
+
|
153
|
+
def scheme
|
154
|
+
@options[:scheme] || @url.scheme
|
155
|
+
end
|
156
|
+
|
157
|
+
def database
|
158
|
+
@options[:database] || extract_database(@url.path)
|
159
|
+
end
|
160
|
+
|
161
|
+
private def extract_database(path)
|
162
|
+
if path =~ /\/(\d+)$/
|
163
|
+
return $1.to_i
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def credentials
|
168
|
+
@options[:credentials] || extract_userinfo(@url.userinfo)
|
169
|
+
end
|
170
|
+
|
171
|
+
private def extract_userinfo(userinfo)
|
172
|
+
if userinfo
|
173
|
+
credentials = userinfo.split(":").reject(&:empty?)
|
174
|
+
|
175
|
+
if credentials.any?
|
176
|
+
return credentials
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def localhost?
|
182
|
+
@url.hostname =~ /^(.*?\.)?localhost\.?$/
|
183
|
+
end
|
184
|
+
|
185
|
+
# We don't try to validate peer certificates when talking to localhost because they would always be self-signed.
|
186
|
+
def ssl_verify_mode
|
187
|
+
if self.localhost?
|
188
|
+
OpenSSL::SSL::VERIFY_NONE
|
189
|
+
else
|
190
|
+
OpenSSL::SSL::VERIFY_PEER
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def ssl_context
|
195
|
+
@options[:ssl_context] || OpenSSL::SSL::SSLContext.new.tap do |context|
|
196
|
+
context.set_params(
|
197
|
+
verify_mode: self.ssl_verify_mode
|
198
|
+
)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def build_endpoint(endpoint = nil)
|
203
|
+
endpoint ||= tcp_endpoint
|
204
|
+
|
205
|
+
if secure?
|
206
|
+
# Wrap it in SSL:
|
207
|
+
return ::IO::Endpoint::SSLEndpoint.new(endpoint,
|
208
|
+
ssl_context: self.ssl_context,
|
209
|
+
hostname: @url.hostname,
|
210
|
+
timeout: self.timeout,
|
211
|
+
)
|
212
|
+
end
|
213
|
+
|
214
|
+
return endpoint
|
215
|
+
end
|
216
|
+
|
217
|
+
def endpoint
|
218
|
+
@endpoint ||= build_endpoint
|
219
|
+
end
|
220
|
+
|
221
|
+
def endpoint=(endpoint)
|
222
|
+
@endpoint = build_endpoint(endpoint)
|
223
|
+
end
|
224
|
+
|
225
|
+
def bind(*arguments, &block)
|
226
|
+
endpoint.bind(*arguments, &block)
|
227
|
+
end
|
228
|
+
|
229
|
+
def connect(&block)
|
230
|
+
endpoint.connect(&block)
|
231
|
+
end
|
232
|
+
|
233
|
+
def each
|
234
|
+
return to_enum unless block_given?
|
235
|
+
|
236
|
+
self.tcp_endpoint.each do |endpoint|
|
237
|
+
yield self.class.new(@url, endpoint, **@options)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def key
|
242
|
+
[@url, @options]
|
243
|
+
end
|
244
|
+
|
245
|
+
def eql? other
|
246
|
+
self.key.eql? other.key
|
247
|
+
end
|
248
|
+
|
249
|
+
def hash
|
250
|
+
self.key.hash
|
251
|
+
end
|
252
|
+
|
253
|
+
protected
|
254
|
+
|
255
|
+
def tcp_options
|
256
|
+
options = @options.dup
|
257
|
+
|
258
|
+
options.delete(:scheme)
|
259
|
+
options.delete(:port)
|
260
|
+
options.delete(:hostname)
|
261
|
+
options.delete(:ssl_context)
|
262
|
+
options.delete(:protocol)
|
263
|
+
|
264
|
+
return options
|
265
|
+
end
|
266
|
+
|
267
|
+
def tcp_endpoint
|
268
|
+
::IO::Endpoint.tcp(self.hostname, port, **tcp_options)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require 'protocol/redis'
|
7
|
+
|
8
|
+
module Async
|
9
|
+
module Redis
|
10
|
+
module Protocol
|
11
|
+
# Executes AUTH after the user has established a connection.
|
12
|
+
class Authenticated
|
13
|
+
# Authentication has failed for some reason.
|
14
|
+
class AuthenticationError < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a new authenticated protocol.
|
18
|
+
#
|
19
|
+
# @parameter credentials [Array] The credentials to use for authentication.
|
20
|
+
# @parameter protocol [Object] The delegated protocol for connecting.
|
21
|
+
def initialize(credentials, protocol = Async::Redis::Protocol::RESP2)
|
22
|
+
@credentials = credentials
|
23
|
+
@protocol = protocol
|
24
|
+
end
|
25
|
+
|
26
|
+
attr :credentials
|
27
|
+
|
28
|
+
# Create a new client and authenticate it.
|
29
|
+
def client(stream)
|
30
|
+
client = @protocol.client(stream)
|
31
|
+
|
32
|
+
client.write_request(["AUTH", *@credentials])
|
33
|
+
response = client.read_response
|
34
|
+
|
35
|
+
if response != "OK"
|
36
|
+
raise AuthenticationError, "Could not authenticate: #{response}"
|
37
|
+
end
|
38
|
+
|
39
|
+
return client
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require 'protocol/redis'
|
7
|
+
|
8
|
+
module Async
|
9
|
+
module Redis
|
10
|
+
module Protocol
|
11
|
+
# Executes AUTH after the user has established a connection.
|
12
|
+
class Selected
|
13
|
+
# Authentication has failed for some reason.
|
14
|
+
class SelectionError < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a new authenticated protocol.
|
18
|
+
#
|
19
|
+
# @parameter index [Integer] The database index to select.
|
20
|
+
# @parameter protocol [Object] The delegated protocol for connecting.
|
21
|
+
def initialize(index, protocol = Async::Redis::Protocol::RESP2)
|
22
|
+
@index = index
|
23
|
+
@protocol = protocol
|
24
|
+
end
|
25
|
+
|
26
|
+
attr :index
|
27
|
+
|
28
|
+
# Create a new client and authenticate it.
|
29
|
+
def client(stream)
|
30
|
+
client = @protocol.client(stream)
|
31
|
+
|
32
|
+
client.write_request(["SELECT", @index])
|
33
|
+
response = client.read_response
|
34
|
+
|
35
|
+
if response != "OK"
|
36
|
+
raise SelectionError, "Could not select database: #{response}"
|
37
|
+
end
|
38
|
+
|
39
|
+
return client
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -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/readme.md
CHANGED
@@ -6,9 +6,9 @@ An asynchronous client for Redis including TLS. Support for streaming requests a
|
|
6
6
|
|
7
7
|
## Usage
|
8
8
|
|
9
|
-
Please see the [project documentation](https://github.
|
9
|
+
Please see the [project documentation](https://socketry.github.io/async-redis/) for more details.
|
10
10
|
|
11
|
-
- [Getting Started](https://github.
|
11
|
+
- [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.
|
12
12
|
|
13
13
|
## Contributing
|
14
14
|
|
@@ -22,8 +22,8 @@ We welcome contributions to this project.
|
|
22
22
|
|
23
23
|
### Developer Certificate of Origin
|
24
24
|
|
25
|
-
|
25
|
+
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
26
26
|
|
27
|
-
###
|
27
|
+
### Community Guidelines
|
28
28
|
|
29
|
-
This project is
|
29
|
+
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
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
|
@@ -48,28 +48,22 @@ cert_chain:
|
|
48
48
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
49
49
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
50
50
|
-----END CERTIFICATE-----
|
51
|
-
date: 2024-
|
51
|
+
date: 2024-08-16 00:00:00.000000000 Z
|
52
52
|
dependencies:
|
53
53
|
- !ruby/object:Gem::Dependency
|
54
54
|
name: async
|
55
55
|
requirement: !ruby/object:Gem::Requirement
|
56
56
|
requirements:
|
57
|
-
- - "
|
58
|
-
- !ruby/object:Gem::Version
|
59
|
-
version: '1.8'
|
60
|
-
- - "<"
|
57
|
+
- - "~>"
|
61
58
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
59
|
+
version: '2.10'
|
63
60
|
type: :runtime
|
64
61
|
prerelease: false
|
65
62
|
version_requirements: !ruby/object:Gem::Requirement
|
66
63
|
requirements:
|
67
|
-
- - "
|
68
|
-
- !ruby/object:Gem::Version
|
69
|
-
version: '1.8'
|
70
|
-
- - "<"
|
64
|
+
- - "~>"
|
71
65
|
- !ruby/object:Gem::Version
|
72
|
-
version: '
|
66
|
+
version: '2.10'
|
73
67
|
- !ruby/object:Gem::Dependency
|
74
68
|
name: async-pool
|
75
69
|
requirement: !ruby/object:Gem::Requirement
|
@@ -118,14 +112,14 @@ dependencies:
|
|
118
112
|
requirements:
|
119
113
|
- - "~>"
|
120
114
|
- !ruby/object:Gem::Version
|
121
|
-
version: 0.
|
115
|
+
version: '0.9'
|
122
116
|
type: :runtime
|
123
117
|
prerelease: false
|
124
118
|
version_requirements: !ruby/object:Gem::Requirement
|
125
119
|
requirements:
|
126
120
|
- - "~>"
|
127
121
|
- !ruby/object:Gem::Version
|
128
|
-
version: 0.
|
122
|
+
version: '0.9'
|
129
123
|
description:
|
130
124
|
email:
|
131
125
|
executables: []
|
@@ -134,13 +128,17 @@ extra_rdoc_files: []
|
|
134
128
|
files:
|
135
129
|
- lib/async/redis.rb
|
136
130
|
- lib/async/redis/client.rb
|
131
|
+
- lib/async/redis/cluster_client.rb
|
137
132
|
- lib/async/redis/context/generic.rb
|
138
133
|
- lib/async/redis/context/pipeline.rb
|
139
134
|
- lib/async/redis/context/subscribe.rb
|
140
135
|
- lib/async/redis/context/transaction.rb
|
136
|
+
- lib/async/redis/endpoint.rb
|
141
137
|
- lib/async/redis/key.rb
|
138
|
+
- lib/async/redis/protocol/authenticated.rb
|
142
139
|
- lib/async/redis/protocol/resp2.rb
|
143
|
-
- lib/async/redis/
|
140
|
+
- lib/async/redis/protocol/selected.rb
|
141
|
+
- lib/async/redis/sentinel_client.rb
|
144
142
|
- lib/async/redis/version.rb
|
145
143
|
- license.md
|
146
144
|
- readme.md
|
@@ -148,6 +146,7 @@ homepage: https://github.com/socketry/async-redis
|
|
148
146
|
licenses:
|
149
147
|
- MIT
|
150
148
|
metadata:
|
149
|
+
documentation_uri: https://socketry.github.io/async-redis/
|
151
150
|
source_code_uri: https://github.com/socketry/async-redis.git
|
152
151
|
post_install_message:
|
153
152
|
rdoc_options: []
|
@@ -164,7 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
163
|
- !ruby/object:Gem::Version
|
165
164
|
version: '0'
|
166
165
|
requirements: []
|
167
|
-
rubygems_version: 3.5.
|
166
|
+
rubygems_version: 3.5.11
|
168
167
|
signing_key:
|
169
168
|
specification_version: 4
|
170
169
|
summary: A Redis client library.
|
metadata.gz.sig
CHANGED
Binary file
|
@@ -1,97 +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
|
-
@sentinel_endpoints = sentinels.map do |sentinel|
|
15
|
-
::IO::Endpoint.tcp(sentinel[:host], sentinel[:port])
|
16
|
-
end
|
17
|
-
@role = role
|
18
|
-
|
19
|
-
@protocol = protocol
|
20
|
-
@pool = connect(**options)
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
# Override the parent method. The only difference is that this one needs
|
26
|
-
# to resolve the master/slave address.
|
27
|
-
def connect(**options)
|
28
|
-
Async::Pool::Controller.wrap(**options) do
|
29
|
-
endpoint = resolve_address
|
30
|
-
peer = endpoint.connect
|
31
|
-
stream = ::IO::Stream(peer)
|
32
|
-
|
33
|
-
@protocol.client(stream)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def resolve_address
|
38
|
-
address = case @role
|
39
|
-
when :master then resolve_master
|
40
|
-
when :slave then resolve_slave
|
41
|
-
else raise ArgumentError, "Unknown instance role #{@role}"
|
42
|
-
end
|
43
|
-
|
44
|
-
address or raise RuntimeError, "Unable to fetch #{@role} via Sentinel."
|
45
|
-
end
|
46
|
-
|
47
|
-
def resolve_master
|
48
|
-
@sentinel_endpoints.each do |sentinel_endpoint|
|
49
|
-
client = Client.new(sentinel_endpoint)
|
50
|
-
|
51
|
-
begin
|
52
|
-
address = client.call('sentinel', 'get-master-addr-by-name', @master_name)
|
53
|
-
rescue Errno::ECONNREFUSED
|
54
|
-
next
|
55
|
-
end
|
56
|
-
|
57
|
-
return ::IO::Endpoint.tcp(address[0], address[1]) if address
|
58
|
-
end
|
59
|
-
|
60
|
-
nil
|
61
|
-
end
|
62
|
-
|
63
|
-
def resolve_slave
|
64
|
-
@sentinel_endpoints.each do |sentinel_endpoint|
|
65
|
-
client = Client.new(sentinel_endpoint)
|
66
|
-
|
67
|
-
begin
|
68
|
-
reply = client.call('sentinel', 'slaves', @master_name)
|
69
|
-
rescue Errno::ECONNREFUSED
|
70
|
-
next
|
71
|
-
end
|
72
|
-
|
73
|
-
slaves = available_slaves(reply)
|
74
|
-
next if slaves.empty?
|
75
|
-
|
76
|
-
slave = select_slave(slaves)
|
77
|
-
return ::IO::Endpoint.tcp(slave['ip'], slave['port'])
|
78
|
-
end
|
79
|
-
|
80
|
-
nil
|
81
|
-
end
|
82
|
-
|
83
|
-
def available_slaves(slaves_cmd_reply)
|
84
|
-
# The reply is an array with the format: [field1, value1, field2,
|
85
|
-
# value2, etc.].
|
86
|
-
# When a slave is marked as down by the sentinel, the "flags" field
|
87
|
-
# (comma-separated array) contains the "s_down" value.
|
88
|
-
slaves_cmd_reply.map { |s| s.each_slice(2).to_h }
|
89
|
-
.reject { |s| s.fetch('flags').split(',').include?('s_down') }
|
90
|
-
end
|
91
|
-
|
92
|
-
def select_slave(available_slaves)
|
93
|
-
available_slaves.sample
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|