redis-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +190 -0
  3. data/CHANGELOG.md +3 -0
  4. data/Gemfile +23 -0
  5. data/Gemfile.lock +67 -0
  6. data/LICENSE.md +21 -0
  7. data/README.md +347 -0
  8. data/Rakefile +86 -0
  9. data/ext/redis_client/hiredis/extconf.rb +54 -0
  10. data/ext/redis_client/hiredis/hiredis_connection.c +696 -0
  11. data/ext/redis_client/hiredis/vendor/.gitignore +9 -0
  12. data/ext/redis_client/hiredis/vendor/.travis.yml +131 -0
  13. data/ext/redis_client/hiredis/vendor/CHANGELOG.md +364 -0
  14. data/ext/redis_client/hiredis/vendor/CMakeLists.txt +165 -0
  15. data/ext/redis_client/hiredis/vendor/COPYING +29 -0
  16. data/ext/redis_client/hiredis/vendor/Makefile +308 -0
  17. data/ext/redis_client/hiredis/vendor/README.md +664 -0
  18. data/ext/redis_client/hiredis/vendor/adapters/ae.h +130 -0
  19. data/ext/redis_client/hiredis/vendor/adapters/glib.h +156 -0
  20. data/ext/redis_client/hiredis/vendor/adapters/ivykis.h +84 -0
  21. data/ext/redis_client/hiredis/vendor/adapters/libev.h +179 -0
  22. data/ext/redis_client/hiredis/vendor/adapters/libevent.h +175 -0
  23. data/ext/redis_client/hiredis/vendor/adapters/libuv.h +117 -0
  24. data/ext/redis_client/hiredis/vendor/adapters/macosx.h +115 -0
  25. data/ext/redis_client/hiredis/vendor/adapters/qt.h +135 -0
  26. data/ext/redis_client/hiredis/vendor/alloc.c +86 -0
  27. data/ext/redis_client/hiredis/vendor/alloc.h +91 -0
  28. data/ext/redis_client/hiredis/vendor/appveyor.yml +24 -0
  29. data/ext/redis_client/hiredis/vendor/async.c +887 -0
  30. data/ext/redis_client/hiredis/vendor/async.h +147 -0
  31. data/ext/redis_client/hiredis/vendor/async_private.h +75 -0
  32. data/ext/redis_client/hiredis/vendor/dict.c +352 -0
  33. data/ext/redis_client/hiredis/vendor/dict.h +126 -0
  34. data/ext/redis_client/hiredis/vendor/fmacros.h +12 -0
  35. data/ext/redis_client/hiredis/vendor/hiredis-config.cmake.in +13 -0
  36. data/ext/redis_client/hiredis/vendor/hiredis.c +1174 -0
  37. data/ext/redis_client/hiredis/vendor/hiredis.h +336 -0
  38. data/ext/redis_client/hiredis/vendor/hiredis.pc.in +12 -0
  39. data/ext/redis_client/hiredis/vendor/hiredis_ssl-config.cmake.in +13 -0
  40. data/ext/redis_client/hiredis/vendor/hiredis_ssl.h +157 -0
  41. data/ext/redis_client/hiredis/vendor/hiredis_ssl.pc.in +12 -0
  42. data/ext/redis_client/hiredis/vendor/net.c +612 -0
  43. data/ext/redis_client/hiredis/vendor/net.h +56 -0
  44. data/ext/redis_client/hiredis/vendor/read.c +739 -0
  45. data/ext/redis_client/hiredis/vendor/read.h +129 -0
  46. data/ext/redis_client/hiredis/vendor/sds.c +1289 -0
  47. data/ext/redis_client/hiredis/vendor/sds.h +278 -0
  48. data/ext/redis_client/hiredis/vendor/sdsalloc.h +44 -0
  49. data/ext/redis_client/hiredis/vendor/sockcompat.c +248 -0
  50. data/ext/redis_client/hiredis/vendor/sockcompat.h +92 -0
  51. data/ext/redis_client/hiredis/vendor/ssl.c +544 -0
  52. data/ext/redis_client/hiredis/vendor/test.c +1401 -0
  53. data/ext/redis_client/hiredis/vendor/test.sh +78 -0
  54. data/ext/redis_client/hiredis/vendor/win32.h +56 -0
  55. data/lib/redis-client.rb +3 -0
  56. data/lib/redis_client/buffered_io.rb +149 -0
  57. data/lib/redis_client/config.rb +174 -0
  58. data/lib/redis_client/connection.rb +86 -0
  59. data/lib/redis_client/hiredis_connection.rb +78 -0
  60. data/lib/redis_client/pooled.rb +86 -0
  61. data/lib/redis_client/resp3.rb +225 -0
  62. data/lib/redis_client/sentinel_config.rb +134 -0
  63. data/lib/redis_client/version.rb +5 -0
  64. data/lib/redis_client.rb +438 -0
  65. data/redis-client.gemspec +34 -0
  66. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisClient
4
+ VERSION = "0.1.0"
5
+ end