redis-client 0.1.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 +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
|