redis-client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +190 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +23 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.md +21 -0
- data/README.md +347 -0
- data/Rakefile +86 -0
- data/ext/redis_client/hiredis/extconf.rb +54 -0
- data/ext/redis_client/hiredis/hiredis_connection.c +696 -0
- data/ext/redis_client/hiredis/vendor/.gitignore +9 -0
- data/ext/redis_client/hiredis/vendor/.travis.yml +131 -0
- data/ext/redis_client/hiredis/vendor/CHANGELOG.md +364 -0
- data/ext/redis_client/hiredis/vendor/CMakeLists.txt +165 -0
- data/ext/redis_client/hiredis/vendor/COPYING +29 -0
- data/ext/redis_client/hiredis/vendor/Makefile +308 -0
- data/ext/redis_client/hiredis/vendor/README.md +664 -0
- data/ext/redis_client/hiredis/vendor/adapters/ae.h +130 -0
- data/ext/redis_client/hiredis/vendor/adapters/glib.h +156 -0
- data/ext/redis_client/hiredis/vendor/adapters/ivykis.h +84 -0
- data/ext/redis_client/hiredis/vendor/adapters/libev.h +179 -0
- data/ext/redis_client/hiredis/vendor/adapters/libevent.h +175 -0
- data/ext/redis_client/hiredis/vendor/adapters/libuv.h +117 -0
- data/ext/redis_client/hiredis/vendor/adapters/macosx.h +115 -0
- data/ext/redis_client/hiredis/vendor/adapters/qt.h +135 -0
- data/ext/redis_client/hiredis/vendor/alloc.c +86 -0
- data/ext/redis_client/hiredis/vendor/alloc.h +91 -0
- data/ext/redis_client/hiredis/vendor/appveyor.yml +24 -0
- data/ext/redis_client/hiredis/vendor/async.c +887 -0
- data/ext/redis_client/hiredis/vendor/async.h +147 -0
- data/ext/redis_client/hiredis/vendor/async_private.h +75 -0
- data/ext/redis_client/hiredis/vendor/dict.c +352 -0
- data/ext/redis_client/hiredis/vendor/dict.h +126 -0
- data/ext/redis_client/hiredis/vendor/fmacros.h +12 -0
- data/ext/redis_client/hiredis/vendor/hiredis-config.cmake.in +13 -0
- data/ext/redis_client/hiredis/vendor/hiredis.c +1174 -0
- data/ext/redis_client/hiredis/vendor/hiredis.h +336 -0
- data/ext/redis_client/hiredis/vendor/hiredis.pc.in +12 -0
- data/ext/redis_client/hiredis/vendor/hiredis_ssl-config.cmake.in +13 -0
- data/ext/redis_client/hiredis/vendor/hiredis_ssl.h +157 -0
- data/ext/redis_client/hiredis/vendor/hiredis_ssl.pc.in +12 -0
- data/ext/redis_client/hiredis/vendor/net.c +612 -0
- data/ext/redis_client/hiredis/vendor/net.h +56 -0
- data/ext/redis_client/hiredis/vendor/read.c +739 -0
- data/ext/redis_client/hiredis/vendor/read.h +129 -0
- data/ext/redis_client/hiredis/vendor/sds.c +1289 -0
- data/ext/redis_client/hiredis/vendor/sds.h +278 -0
- data/ext/redis_client/hiredis/vendor/sdsalloc.h +44 -0
- data/ext/redis_client/hiredis/vendor/sockcompat.c +248 -0
- data/ext/redis_client/hiredis/vendor/sockcompat.h +92 -0
- data/ext/redis_client/hiredis/vendor/ssl.c +544 -0
- data/ext/redis_client/hiredis/vendor/test.c +1401 -0
- data/ext/redis_client/hiredis/vendor/test.sh +78 -0
- data/ext/redis_client/hiredis/vendor/win32.h +56 -0
- data/lib/redis-client.rb +3 -0
- data/lib/redis_client/buffered_io.rb +149 -0
- data/lib/redis_client/config.rb +174 -0
- data/lib/redis_client/connection.rb +86 -0
- data/lib/redis_client/hiredis_connection.rb +78 -0
- data/lib/redis_client/pooled.rb +86 -0
- data/lib/redis_client/resp3.rb +225 -0
- data/lib/redis_client/sentinel_config.rb +134 -0
- data/lib/redis_client/version.rb +5 -0
- data/lib/redis_client.rb +438 -0
- data/redis-client.gemspec +34 -0
- metadata +125 -0
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
class RedisClient
|
6
|
+
module RESP3
|
7
|
+
module_function
|
8
|
+
|
9
|
+
Error = Class.new(RedisClient::Error)
|
10
|
+
UnknownType = Class.new(Error)
|
11
|
+
SyntaxError = Class.new(Error)
|
12
|
+
|
13
|
+
EOL = "\r\n".b.freeze
|
14
|
+
EOL_SIZE = EOL.bytesize
|
15
|
+
DUMP_TYPES = {
|
16
|
+
String => :dump_string,
|
17
|
+
Symbol => :dump_symbol,
|
18
|
+
Integer => :dump_numeric,
|
19
|
+
Float => :dump_numeric,
|
20
|
+
}.freeze
|
21
|
+
PARSER_TYPES = {
|
22
|
+
'#' => :parse_boolean,
|
23
|
+
'$' => :parse_blob,
|
24
|
+
'+' => :parse_string,
|
25
|
+
'=' => :parse_verbatim_string,
|
26
|
+
'-' => :parse_error,
|
27
|
+
':' => :parse_integer,
|
28
|
+
'(' => :parse_integer,
|
29
|
+
',' => :parse_double,
|
30
|
+
'_' => :parse_null,
|
31
|
+
'*' => :parse_array,
|
32
|
+
'%' => :parse_map,
|
33
|
+
'~' => :parse_set,
|
34
|
+
'>' => :parse_array,
|
35
|
+
}.transform_keys(&:ord).freeze
|
36
|
+
INTEGER_RANGE = ((((2**64) / 2) * -1)..(((2**64) / 2) - 1)).freeze
|
37
|
+
|
38
|
+
def dump(command, buffer = nil)
|
39
|
+
buffer ||= new_buffer
|
40
|
+
command = command.flat_map do |element|
|
41
|
+
case element
|
42
|
+
when Hash
|
43
|
+
element.flatten
|
44
|
+
when Set
|
45
|
+
element.to_a
|
46
|
+
else
|
47
|
+
element
|
48
|
+
end
|
49
|
+
end
|
50
|
+
dump_array(command, buffer)
|
51
|
+
end
|
52
|
+
|
53
|
+
def load(io)
|
54
|
+
parse(io)
|
55
|
+
end
|
56
|
+
|
57
|
+
def new_buffer
|
58
|
+
String.new(encoding: Encoding::BINARY, capacity: 128)
|
59
|
+
end
|
60
|
+
|
61
|
+
def coerce_command!(command)
|
62
|
+
command = command.flat_map do |element|
|
63
|
+
case element
|
64
|
+
when Hash
|
65
|
+
element.flatten
|
66
|
+
when Set
|
67
|
+
element.to_a
|
68
|
+
else
|
69
|
+
element
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
command.map! do |element|
|
74
|
+
case element
|
75
|
+
when String
|
76
|
+
element
|
77
|
+
when Integer, Float, Symbol
|
78
|
+
element.to_s
|
79
|
+
else
|
80
|
+
raise TypeError, "Unsupported command argument type: #{element.class}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
command
|
85
|
+
end
|
86
|
+
|
87
|
+
def dump_any(object, buffer)
|
88
|
+
method = DUMP_TYPES.fetch(object.class) do
|
89
|
+
raise TypeError, "Unsupported command argument type: #{object.class}"
|
90
|
+
end
|
91
|
+
send(method, object, buffer)
|
92
|
+
end
|
93
|
+
|
94
|
+
def dump_array(array, buffer)
|
95
|
+
buffer << '*' << array.size.to_s << EOL
|
96
|
+
array.each do |item|
|
97
|
+
dump_any(item, buffer)
|
98
|
+
end
|
99
|
+
buffer
|
100
|
+
end
|
101
|
+
|
102
|
+
def dump_set(set, buffer)
|
103
|
+
buffer << '~' << set.size.to_s << EOL
|
104
|
+
set.each do |item|
|
105
|
+
dump_any(item, buffer)
|
106
|
+
end
|
107
|
+
buffer
|
108
|
+
end
|
109
|
+
|
110
|
+
def dump_hash(hash, buffer)
|
111
|
+
buffer << '%' << hash.size.to_s << EOL
|
112
|
+
hash.each_pair do |key, value|
|
113
|
+
dump_any(key, buffer)
|
114
|
+
dump_any(value, buffer)
|
115
|
+
end
|
116
|
+
buffer
|
117
|
+
end
|
118
|
+
|
119
|
+
def dump_numeric(numeric, buffer)
|
120
|
+
dump_string(numeric.to_s, buffer)
|
121
|
+
end
|
122
|
+
|
123
|
+
def dump_string(string, buffer)
|
124
|
+
string = string.b unless string.ascii_only?
|
125
|
+
buffer << '$' << string.bytesize.to_s << EOL << string << EOL
|
126
|
+
end
|
127
|
+
|
128
|
+
if Symbol.method_defined?(:name)
|
129
|
+
def dump_symbol(symbol, buffer)
|
130
|
+
dump_string(symbol.name, buffer)
|
131
|
+
end
|
132
|
+
else
|
133
|
+
def dump_symbol(symbol, buffer)
|
134
|
+
dump_string(symbol.to_s, buffer)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def parse(io)
|
139
|
+
type = io.getbyte
|
140
|
+
method = PARSER_TYPES.fetch(type) do
|
141
|
+
raise UnknownType, "Unknown sigil type: #{type.chr.inspect}"
|
142
|
+
end
|
143
|
+
send(method, io)
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_string(io)
|
147
|
+
str = io.gets_chomp
|
148
|
+
str.force_encoding(Encoding.default_external)
|
149
|
+
str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
|
150
|
+
str
|
151
|
+
end
|
152
|
+
|
153
|
+
def parse_error(io)
|
154
|
+
CommandError.parse(parse_string(io))
|
155
|
+
end
|
156
|
+
|
157
|
+
def parse_boolean(io)
|
158
|
+
case value = io.gets_chomp
|
159
|
+
when "t"
|
160
|
+
true
|
161
|
+
when "f"
|
162
|
+
false
|
163
|
+
else
|
164
|
+
raise SyntaxError, "Expected `t` or `f` after `#`, got: #{value}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def parse_array(io)
|
169
|
+
parse_sequence(io, parse_integer(io))
|
170
|
+
end
|
171
|
+
|
172
|
+
def parse_set(io)
|
173
|
+
parse_sequence(io, parse_integer(io)).to_set
|
174
|
+
end
|
175
|
+
|
176
|
+
def parse_map(io)
|
177
|
+
Hash[*parse_sequence(io, parse_integer(io) * 2)]
|
178
|
+
end
|
179
|
+
|
180
|
+
def parse_push(io)
|
181
|
+
parse_array(io)
|
182
|
+
end
|
183
|
+
|
184
|
+
def parse_sequence(io, size)
|
185
|
+
array = Array.new(size)
|
186
|
+
size.times do |index|
|
187
|
+
array[index] = parse(io)
|
188
|
+
end
|
189
|
+
array
|
190
|
+
end
|
191
|
+
|
192
|
+
def parse_integer(io)
|
193
|
+
Integer(io.gets_chomp)
|
194
|
+
end
|
195
|
+
|
196
|
+
def parse_double(io)
|
197
|
+
case value = io.gets_chomp
|
198
|
+
when "inf"
|
199
|
+
Float::INFINITY
|
200
|
+
when "-inf"
|
201
|
+
-Float::INFINITY
|
202
|
+
else
|
203
|
+
Float(value)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def parse_null(io)
|
208
|
+
io.skip(EOL_SIZE)
|
209
|
+
nil
|
210
|
+
end
|
211
|
+
|
212
|
+
def parse_blob(io)
|
213
|
+
bytesize = parse_integer(io)
|
214
|
+
str = io.read_chomp(bytesize)
|
215
|
+
str.force_encoding(Encoding.default_external)
|
216
|
+
str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
|
217
|
+
str
|
218
|
+
end
|
219
|
+
|
220
|
+
def parse_verbatim_string(io)
|
221
|
+
blob = parse_blob(io)
|
222
|
+
blob.byteslice(4..-1)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisClient
|
4
|
+
class SentinelConfig
|
5
|
+
include Config::Common
|
6
|
+
|
7
|
+
SENTINEL_DELAY = 0.25
|
8
|
+
DEFAULT_RECONNECT_ATTEMPTS = 2
|
9
|
+
|
10
|
+
def initialize(name:, sentinels:, role: :master, **client_config)
|
11
|
+
unless %i(master replica slave).include?(role)
|
12
|
+
raise ArgumentError, "Expected role to be either :master or :replica, got: #{role.inspect}"
|
13
|
+
end
|
14
|
+
|
15
|
+
@name = name
|
16
|
+
@sentinel_configs = sentinels.map { |s| Config.new(**s) }
|
17
|
+
@sentinels = {}.compare_by_identity
|
18
|
+
@role = role
|
19
|
+
@mutex = Mutex.new
|
20
|
+
@config = nil
|
21
|
+
|
22
|
+
client_config[:reconnect_attempts] ||= DEFAULT_RECONNECT_ATTEMPTS
|
23
|
+
@client_config = client_config || {}
|
24
|
+
super(**client_config)
|
25
|
+
end
|
26
|
+
|
27
|
+
def sentinels
|
28
|
+
@mutex.synchronize do
|
29
|
+
@sentinel_configs.dup
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset
|
34
|
+
@mutex.synchronize do
|
35
|
+
@config = nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def host
|
40
|
+
config.host
|
41
|
+
end
|
42
|
+
|
43
|
+
def port
|
44
|
+
config.port
|
45
|
+
end
|
46
|
+
|
47
|
+
def path
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def retry_connecting?(attempt, error)
|
52
|
+
reset unless error.is_a?(TimeoutError)
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
56
|
+
def sentinel?
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_role!(role)
|
61
|
+
if @role == :master
|
62
|
+
unless role == "master"
|
63
|
+
sleep SENTINEL_DELAY
|
64
|
+
raise FailoverError, "Expected to connect to a master, but the server is a replica"
|
65
|
+
end
|
66
|
+
else
|
67
|
+
unless role == "slave"
|
68
|
+
sleep SENTINEL_DELAY
|
69
|
+
raise FailoverError, "Expected to connect to a replica, but the server is a master"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def config
|
77
|
+
@mutex.synchronize do
|
78
|
+
@config ||= if @role == :master
|
79
|
+
resolve_master
|
80
|
+
else
|
81
|
+
resolve_replica
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def resolve_master
|
87
|
+
each_sentinel do |sentinel_client|
|
88
|
+
host, port = sentinel_client.call("SENTINEL", "get-master-addr-by-name", @name)
|
89
|
+
if host && port
|
90
|
+
return Config.new(host: host, port: Integer(port), **@client_config)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
raise ConnectionError, "Couldn't locate a master for role: #{@name}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def sentinel_client(sentinel_config)
|
97
|
+
@sentinels[sentinel_config] ||= sentinel_config.new_client
|
98
|
+
end
|
99
|
+
|
100
|
+
def resolve_replica
|
101
|
+
each_sentinel do |sentinel_client|
|
102
|
+
replicas = sentinel_client.call("SENTINEL", "replicas", @name)
|
103
|
+
next if replicas.empty?
|
104
|
+
|
105
|
+
replica = replicas.reject { |r| r["flags"].to_s.split(",").include?("disconnected") }.sample
|
106
|
+
replica ||= replicas.sample
|
107
|
+
return Config.new(host: replica["ip"], port: Integer(replica["port"]), **@client_config)
|
108
|
+
end
|
109
|
+
raise ConnectionError, "Couldn't locate a replica for role: #{@name}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def each_sentinel
|
113
|
+
last_error = nil
|
114
|
+
|
115
|
+
@sentinel_configs.dup.each do |sentinel_config|
|
116
|
+
sentinel_client = sentinel_client(sentinel_config)
|
117
|
+
success = true
|
118
|
+
begin
|
119
|
+
yield sentinel_client
|
120
|
+
rescue RedisClient::Error => error
|
121
|
+
last_error = error
|
122
|
+
success = false
|
123
|
+
sleep SENTINEL_DELAY
|
124
|
+
ensure
|
125
|
+
if success
|
126
|
+
@sentinel_configs.unshift(@sentinel_configs.delete(sentinel_config))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
raise last_error if last_error
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|