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.
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