Skip to content

Commit 8e7ff4f

Browse files
authored
THREESCALE-10605: Add support for sentinels in Async mode (#362)
* Implement support for sentinels in async mode * Add tests for async storage
1 parent 72d0794 commit 8e7ff4f

File tree

2 files changed

+258
-18
lines changed

2 files changed

+258
-18
lines changed

lib/3scale/backend/storage_async/client.rb

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'async/io'
22
require 'async/redis/client'
3+
require 'async/redis/sentinels'
34

45
module ThreeScale
56
module Backend
@@ -14,15 +15,6 @@ module StorageAsync
1415
class Client
1516
include Configurable
1617

17-
DEFAULT_HOST = 'localhost'.freeze
18-
private_constant :DEFAULT_HOST
19-
20-
DEFAULT_PORT = 22121
21-
private_constant :DEFAULT_PORT
22-
23-
HOST_PORT_REGEX = /redis:\/\/(.*):(\d+)/
24-
private_constant :HOST_PORT_REGEX
25-
2618
class << self
2719
attr_writer :instance
2820

@@ -41,14 +33,7 @@ def instance(reset = false)
4133
end
4234

4335
def initialize(opts)
44-
host, port = opts[:url].match(HOST_PORT_REGEX).captures if opts[:url]
45-
host ||= DEFAULT_HOST
46-
port ||= DEFAULT_PORT
47-
48-
endpoint = Async::IO::Endpoint.tcp(host, port)
49-
@redis_async = Async::Redis::Client.new(
50-
endpoint, limit: opts[:max_connections]
51-
)
36+
@redis_async = initialize_client(opts)
5237
@building_pipeline = false
5338
end
5439

@@ -200,8 +185,35 @@ def pipelined(&block)
200185
def close
201186
@redis_async.close
202187
end
203-
end
204188

189+
private
190+
191+
DEFAULT_HOST = 'localhost'.freeze
192+
DEFAULT_PORT = 22121
193+
194+
def initialize_client(opts)
195+
return init_host_client(opts) unless opts.key? :sentinels
196+
197+
init_sentinels_client(opts)
198+
end
199+
200+
def init_host_client(opts)
201+
uri = URI(opts[:url] || '')
202+
host = uri.host || DEFAULT_HOST
203+
port = uri.port || DEFAULT_PORT
204+
205+
endpoint = Async::IO::Endpoint.tcp(host, port)
206+
Async::Redis::Client.new(endpoint, limit: opts[:max_connections])
207+
end
208+
209+
def init_sentinels_client(opts)
210+
uri = URI(opts[:url] || '')
211+
name = uri.host
212+
role = opts[:role] || :master
213+
214+
Async::Redis::SentinelsClient.new(name, opts[:sentinels], role)
215+
end
216+
end
205217
end
206218
end
207219
end

test/unit/storage_async_test.rb

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2+
3+
class StorageAsyncTest < Test::Unit::TestCase
4+
def test_basic_operations
5+
storage = StorageAsync::Client.instance(true)
6+
storage.del('foo')
7+
assert_nil storage.get('foo')
8+
storage.set('foo', 'bar')
9+
assert_equal 'bar', storage.get('foo')
10+
end
11+
12+
def test_redis_host_and_port
13+
storage = StorageAsync::Client.send :new, url('127.0.0.1:6379')
14+
assert_connection(storage)
15+
end
16+
17+
def test_redis_url
18+
storage = StorageAsync::Client.send :new, url('redis://127.0.0.1:6379/0')
19+
assert_connection(storage)
20+
end
21+
22+
def test_redis_unix
23+
storage = StorageAsync::Client.send :new, url('unix:///tmp/redis_unix.6379.sock')
24+
assert_connection(storage)
25+
end
26+
27+
def test_redis_protected_url
28+
assert_nothing_raised do
29+
StorageAsync::Client.send :new, url('redis://user:[email protected]:6379/0')
30+
end
31+
end
32+
33+
def test_redis_malformed_url
34+
assert_raise Storage::InvalidURI do
35+
StorageAsync::Client.send :new, url('a_malformed_url:1:10')
36+
end
37+
end
38+
39+
def test_sentinels_connection_string
40+
config_obj = {
41+
url: 'redis://master-group-name',
42+
sentinels: ',redis://127.0.0.1:26379, , , 127.0.0.1:36379,'
43+
}
44+
45+
conn = StorageAsync::Client.send :new, Storage::Helpers.config_with(config_obj)
46+
assert_sentinel_client(conn)
47+
assert_sentinel_config(conn, url: config_obj[:url],
48+
sentinels: [{ host: '127.0.0.1', port: 26_379 },
49+
{ host: '127.0.0.1', port: 36_379 }])
50+
end
51+
52+
def test_sentinels_connection_array_strings
53+
config_obj = {
54+
url: 'redis://master-group-name',
55+
sentinels: ['redis://127.0.0.1:26379 ', ' 127.0.0.1:36379', nil]
56+
}
57+
58+
conn = StorageAsync::Client.send :new, Storage::Helpers.config_with(config_obj)
59+
assert_sentinel_client(conn)
60+
assert_sentinel_config(conn, url: config_obj[:url],
61+
sentinels: [{ host: '127.0.0.1', port: 26_379 },
62+
{ host: '127.0.0.1', port: 36_379 }])
63+
end
64+
65+
def test_sentinels_connection_array_hashes
66+
config_obj = {
67+
url: 'redis://master-group-name',
68+
sentinels: [{ host: '127.0.0.1', port: 26_379 },
69+
{},
70+
{ host: '127.0.0.1', port: 36_379 },
71+
nil]
72+
}
73+
74+
conn = StorageAsync::Client.send :new, Storage::Helpers.config_with(config_obj)
75+
assert_sentinel_client(conn)
76+
assert_sentinel_config(conn, url: config_obj[:url],
77+
sentinels: config_obj[:sentinels].compact.reject(&:empty?))
78+
end
79+
80+
def test_sentinels_malformed_url
81+
config_obj = {
82+
url: 'redis://master-group-name',
83+
sentinels: 'redis://127.0.0.1:26379,a_malformed_url:1:10'
84+
}
85+
assert_raise Storage::InvalidURI do
86+
StorageAsync::Client.send :new, Storage::Helpers.config_with(config_obj)
87+
end
88+
end
89+
90+
def test_sentinels_simple_url
91+
config_obj = {
92+
url: 'master-group-name', # url of the sentinel master name conf
93+
sentinels: 'redis://127.0.0.1:26379'
94+
}
95+
96+
conn = StorageAsync::Client.send :new, Storage::Helpers.config_with(config_obj)
97+
assert_sentinel_client(conn)
98+
assert_sentinel_config(conn, url: "redis://#{config_obj[:url]}",
99+
sentinels: [{ host: '127.0.0.1', port: 26_379 }])
100+
end
101+
102+
def test_sentinels_array_hashes_default_port
103+
default_sentinel_port = Storage::Helpers.singleton_class.const_get(:DEFAULT_SENTINEL_PORT)
104+
config_obj = {
105+
url: 'redis://master-group-name',
106+
sentinels: [{ host: '127.0.0.1' }, { host: '192.168.1.1' },
107+
{ host: '192.168.1.2', port: nil },
108+
{ host: '127.0.0.1', port: 36379 }]
109+
}
110+
111+
conn = StorageAsync::Client.send :new, Storage::Helpers.config_with(config_obj)
112+
assert_sentinel_client(conn)
113+
assert_sentinel_config(conn, url: config_obj[:url],
114+
sentinels: [{ host: '127.0.0.1', port: default_sentinel_port },
115+
{ host: '192.168.1.1', port: default_sentinel_port },
116+
{ host: '192.168.1.2', port: default_sentinel_port },
117+
{ host: '127.0.0.1', port: 36379 }])
118+
end
119+
120+
def test_sentinels_array_strings_default_port
121+
default_sentinel_port = Storage::Helpers.singleton_class.const_get(:DEFAULT_SENTINEL_PORT)
122+
config_obj = {
123+
url: 'redis://master-group-name',
124+
sentinels: ['127.0.0.2', 'redis://127.0.0.1',
125+
'192.168.1.1', '127.0.0.1:36379',
126+
'redis://127.0.0.1:46379']
127+
}
128+
129+
conn = StorageAsync::Client.send :new, Storage::Helpers.config_with(config_obj)
130+
assert_sentinel_client(conn)
131+
assert_sentinel_config(conn, url: config_obj[:url],
132+
sentinels: [{ host: '127.0.0.2', port: default_sentinel_port },
133+
{ host: '127.0.0.1', port: default_sentinel_port },
134+
{ host: '192.168.1.1', port: default_sentinel_port },
135+
{ host: '127.0.0.1', port: 36379 },
136+
{ host: '127.0.0.1', port: 46379 }])
137+
end
138+
139+
def test_sentinels_correct_role
140+
%i[master slave].each do |role|
141+
config_obj = {
142+
url: 'redis://master-group-name',
143+
sentinels: 'redis://127.0.0.1:26379',
144+
role: role
145+
}
146+
147+
conn = StorageAsync::Client.send :new, Storage::Helpers.config_with(config_obj)
148+
assert_sentinel_client(conn)
149+
assert_sentinel_config(conn, url: config_obj[:url],
150+
sentinels: [{ host: '127.0.0.1', port: 26_379 }],
151+
role: role)
152+
end
153+
end
154+
155+
def test_sentinels_role_empty
156+
[''.to_sym, '', nil].each do |role|
157+
config_obj = {
158+
url: 'redis://master-group-name',
159+
sentinels: 'redis://127.0.0.1:26379',
160+
role: role
161+
}
162+
redis_cfg = Storage::Helpers.config_with(config_obj)
163+
refute redis_cfg.key?(:role)
164+
end
165+
end
166+
167+
def test_role_empty_when_sentinels_does_not_exist
168+
config_obj = {
169+
url: 'redis://127.0.0.1:6379/0',
170+
role: :master
171+
}
172+
redis_cfg = Storage::Helpers.config_with(config_obj)
173+
refute redis_cfg.key?(:role)
174+
end
175+
176+
def test_sentinels_empty
177+
['', []].each do |sentinels_val|
178+
config_obj = {
179+
url: 'redis://master-group-name',
180+
sentinels: sentinels_val
181+
}
182+
redis_cfg = Storage::Helpers.config_with(config_obj)
183+
refute redis_cfg.key?(:sentinels)
184+
end
185+
end
186+
187+
def test_redis_no_scheme
188+
assert_nothing_raised do
189+
StorageAsync::Client.send :new, url('backend-redis:6379')
190+
end
191+
end
192+
193+
private
194+
195+
def assert_connection(client)
196+
client.flushdb
197+
client.set('foo', 'bar')
198+
assert_equal 'bar', client.get('foo')
199+
end
200+
201+
def assert_sentinel_client(client)
202+
inner_client = client.instance_variable_get(:@inner).instance_variable_get(:@redis_async)
203+
assert_instance_of Async::Redis::SentinelsClient, inner_client
204+
end
205+
206+
def assert_sentinel_config(conn, url:, **conf)
207+
client = conn.instance_variable_get(:@inner).instance_variable_get(:@redis_async)
208+
uri = URI(url || '')
209+
name = uri.host
210+
role = conf[:role] || :master
211+
password = client.instance_variable_get(:@protocol).instance_variable_get(:@password)
212+
213+
assert_equal name, client.instance_variable_get(:@master_name)
214+
assert_equal role, client.instance_variable_get(:@role)
215+
216+
assert_equal conf[:sentinels].size, client.instance_variable_get(:@sentinel_endpoints).size
217+
client.instance_variable_get(:@sentinel_endpoints).each_with_index do |endpoint, i|
218+
host, port = endpoint.address
219+
assert_equal conf[:sentinels][i][:host], host
220+
assert_equal conf[:sentinels][i][:port], port
221+
assert_equal(conf[:sentinels][i][:password], password) if conf[:sentinels][i].key? :password
222+
end unless conf[:sentinels].empty?
223+
end
224+
225+
def url(url)
226+
Storage::Helpers.config_with({ url: url })
227+
end
228+
end

0 commit comments

Comments
 (0)