finsync_redis 3.3.5

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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.travis/Gemfile +11 -0
  4. data/.travis.yml +89 -0
  5. data/.yardopts +3 -0
  6. data/CHANGELOG.md +373 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +20 -0
  9. data/README.md +410 -0
  10. data/Rakefile +87 -0
  11. data/benchmarking/logging.rb +71 -0
  12. data/benchmarking/pipeline.rb +51 -0
  13. data/benchmarking/speed.rb +21 -0
  14. data/benchmarking/suite.rb +24 -0
  15. data/benchmarking/worker.rb +71 -0
  16. data/examples/basic.rb +15 -0
  17. data/examples/consistency.rb +114 -0
  18. data/examples/dist_redis.rb +43 -0
  19. data/examples/incr-decr.rb +17 -0
  20. data/examples/list.rb +26 -0
  21. data/examples/pubsub.rb +37 -0
  22. data/examples/sentinel/sentinel.conf +9 -0
  23. data/examples/sentinel/start +49 -0
  24. data/examples/sentinel.rb +41 -0
  25. data/examples/sets.rb +36 -0
  26. data/examples/unicorn/config.ru +3 -0
  27. data/examples/unicorn/unicorn.rb +20 -0
  28. data/lib/redis/client.rb +590 -0
  29. data/lib/redis/connection/command_helper.rb +44 -0
  30. data/lib/redis/connection/hiredis.rb +66 -0
  31. data/lib/redis/connection/registry.rb +12 -0
  32. data/lib/redis/connection/ruby.rb +429 -0
  33. data/lib/redis/connection/synchrony.rb +133 -0
  34. data/lib/redis/connection.rb +9 -0
  35. data/lib/redis/distributed.rb +873 -0
  36. data/lib/redis/errors.rb +40 -0
  37. data/lib/redis/hash_ring.rb +132 -0
  38. data/lib/redis/pipeline.rb +141 -0
  39. data/lib/redis/subscribe.rb +91 -0
  40. data/lib/redis/version.rb +3 -0
  41. data/lib/redis.rb +2788 -0
  42. data/redis.gemspec +44 -0
  43. data/test/bitpos_test.rb +69 -0
  44. data/test/blocking_commands_test.rb +42 -0
  45. data/test/client_test.rb +59 -0
  46. data/test/command_map_test.rb +30 -0
  47. data/test/commands_on_hashes_test.rb +21 -0
  48. data/test/commands_on_hyper_log_log_test.rb +21 -0
  49. data/test/commands_on_lists_test.rb +20 -0
  50. data/test/commands_on_sets_test.rb +77 -0
  51. data/test/commands_on_sorted_sets_test.rb +137 -0
  52. data/test/commands_on_strings_test.rb +101 -0
  53. data/test/commands_on_value_types_test.rb +133 -0
  54. data/test/connection_handling_test.rb +277 -0
  55. data/test/connection_test.rb +57 -0
  56. data/test/db/.gitkeep +0 -0
  57. data/test/distributed_blocking_commands_test.rb +46 -0
  58. data/test/distributed_commands_on_hashes_test.rb +10 -0
  59. data/test/distributed_commands_on_hyper_log_log_test.rb +33 -0
  60. data/test/distributed_commands_on_lists_test.rb +22 -0
  61. data/test/distributed_commands_on_sets_test.rb +83 -0
  62. data/test/distributed_commands_on_sorted_sets_test.rb +18 -0
  63. data/test/distributed_commands_on_strings_test.rb +59 -0
  64. data/test/distributed_commands_on_value_types_test.rb +95 -0
  65. data/test/distributed_commands_requiring_clustering_test.rb +164 -0
  66. data/test/distributed_connection_handling_test.rb +23 -0
  67. data/test/distributed_internals_test.rb +79 -0
  68. data/test/distributed_key_tags_test.rb +52 -0
  69. data/test/distributed_persistence_control_commands_test.rb +26 -0
  70. data/test/distributed_publish_subscribe_test.rb +92 -0
  71. data/test/distributed_remote_server_control_commands_test.rb +66 -0
  72. data/test/distributed_scripting_test.rb +102 -0
  73. data/test/distributed_sorting_test.rb +20 -0
  74. data/test/distributed_test.rb +58 -0
  75. data/test/distributed_transactions_test.rb +32 -0
  76. data/test/encoding_test.rb +18 -0
  77. data/test/error_replies_test.rb +59 -0
  78. data/test/fork_safety_test.rb +65 -0
  79. data/test/helper.rb +232 -0
  80. data/test/helper_test.rb +24 -0
  81. data/test/internals_test.rb +417 -0
  82. data/test/lint/blocking_commands.rb +150 -0
  83. data/test/lint/hashes.rb +162 -0
  84. data/test/lint/hyper_log_log.rb +60 -0
  85. data/test/lint/lists.rb +143 -0
  86. data/test/lint/sets.rb +140 -0
  87. data/test/lint/sorted_sets.rb +316 -0
  88. data/test/lint/strings.rb +260 -0
  89. data/test/lint/value_types.rb +122 -0
  90. data/test/persistence_control_commands_test.rb +26 -0
  91. data/test/pipelining_commands_test.rb +242 -0
  92. data/test/publish_subscribe_test.rb +282 -0
  93. data/test/remote_server_control_commands_test.rb +118 -0
  94. data/test/scanning_test.rb +413 -0
  95. data/test/scripting_test.rb +78 -0
  96. data/test/sentinel_command_test.rb +80 -0
  97. data/test/sentinel_test.rb +255 -0
  98. data/test/sorting_test.rb +59 -0
  99. data/test/ssl_test.rb +73 -0
  100. data/test/support/connection/hiredis.rb +1 -0
  101. data/test/support/connection/ruby.rb +1 -0
  102. data/test/support/connection/synchrony.rb +17 -0
  103. data/test/support/redis_mock.rb +130 -0
  104. data/test/support/ssl/gen_certs.sh +31 -0
  105. data/test/support/ssl/trusted-ca.crt +25 -0
  106. data/test/support/ssl/trusted-ca.key +27 -0
  107. data/test/support/ssl/trusted-cert.crt +81 -0
  108. data/test/support/ssl/trusted-cert.key +28 -0
  109. data/test/support/ssl/untrusted-ca.crt +26 -0
  110. data/test/support/ssl/untrusted-ca.key +27 -0
  111. data/test/support/ssl/untrusted-cert.crt +82 -0
  112. data/test/support/ssl/untrusted-cert.key +28 -0
  113. data/test/support/wire/synchrony.rb +24 -0
  114. data/test/support/wire/thread.rb +5 -0
  115. data/test/synchrony_driver.rb +88 -0
  116. data/test/test.conf.erb +9 -0
  117. data/test/thread_safety_test.rb +62 -0
  118. data/test/transactions_test.rb +264 -0
  119. data/test/unknown_commands_test.rb +14 -0
  120. data/test/url_param_test.rb +138 -0
  121. metadata +202 -0
@@ -0,0 +1,590 @@
1
+ require "redis/errors"
2
+ require "socket"
3
+ require "cgi"
4
+
5
+ class Redis
6
+ class Client
7
+
8
+ DEFAULTS = {
9
+ :url => lambda { ENV["REDIS_URL"] },
10
+ :scheme => "redis",
11
+ :host => "127.0.0.1",
12
+ :port => 6379,
13
+ :path => nil,
14
+ :timeout => 5.0,
15
+ :password => nil,
16
+ :db => 0,
17
+ :driver => nil,
18
+ :id => nil,
19
+ :tcp_keepalive => 0,
20
+ :reconnect_attempts => 1,
21
+ :inherit_socket => false
22
+ }
23
+
24
+ def options
25
+ Marshal.load(Marshal.dump(@options))
26
+ end
27
+
28
+ def scheme
29
+ @options[:scheme]
30
+ end
31
+
32
+ def host
33
+ @options[:host]
34
+ end
35
+
36
+ def port
37
+ @options[:port]
38
+ end
39
+
40
+ def path
41
+ @options[:path]
42
+ end
43
+
44
+ def read_timeout
45
+ @options[:read_timeout]
46
+ end
47
+
48
+ def connect_timeout
49
+ @options[:connect_timeout]
50
+ end
51
+
52
+ def timeout
53
+ @options[:read_timeout]
54
+ end
55
+
56
+ def password
57
+ @options[:password]
58
+ end
59
+
60
+ def db
61
+ @options[:db]
62
+ end
63
+
64
+ def db=(db)
65
+ @options[:db] = db.to_i
66
+ end
67
+
68
+ def driver
69
+ @options[:driver]
70
+ end
71
+
72
+ def inherit_socket?
73
+ @options[:inherit_socket]
74
+ end
75
+
76
+ attr_accessor :logger
77
+ attr_reader :connection
78
+ attr_reader :command_map
79
+
80
+ def initialize(options = {})
81
+ @options = _parse_options(options)
82
+ @reconnect = true
83
+ @logger = @options[:logger]
84
+ @connection = nil
85
+ @command_map = {}
86
+
87
+ @pending_reads = 0
88
+
89
+ if options.include?(:sentinels)
90
+ @connector = Connector::Sentinel.new(@options)
91
+ else
92
+ @connector = Connector.new(@options)
93
+ end
94
+ end
95
+
96
+ def connect
97
+ @pid = Process.pid
98
+
99
+ # Don't try to reconnect when the connection is fresh
100
+ with_reconnect(false) do
101
+ establish_connection
102
+ call [:auth, password] if password
103
+ call [:select, db] if db != 0
104
+ call [:client, :setname, @options[:id]] if @options[:id]
105
+ @connector.check(self)
106
+ end
107
+
108
+ self
109
+ end
110
+
111
+ def id
112
+ @options[:id] || "redis://#{location}/#{db}"
113
+ end
114
+
115
+ def location
116
+ path || "#{host}:#{port}"
117
+ end
118
+
119
+ def call(command)
120
+ reply = process([command]) { read }
121
+ raise reply if reply.is_a?(CommandError)
122
+
123
+ if block_given?
124
+ yield reply
125
+ else
126
+ reply
127
+ end
128
+ end
129
+
130
+ def call_loop(command, timeout = 0)
131
+ error = nil
132
+
133
+ result = with_socket_timeout(timeout) do
134
+ process([command]) do
135
+ loop do
136
+ reply = read
137
+ if reply.is_a?(CommandError)
138
+ error = reply
139
+ break
140
+ else
141
+ yield reply
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ # Raise error when previous block broke out of the loop.
148
+ raise error if error
149
+
150
+ # Result is set to the value that the provided block used to break.
151
+ result
152
+ end
153
+
154
+ def call_pipeline(pipeline)
155
+ with_reconnect pipeline.with_reconnect? do
156
+ begin
157
+ pipeline.finish(call_pipelined(pipeline.commands)).tap do
158
+ self.db = pipeline.db if pipeline.db
159
+ end
160
+ rescue ConnectionError => e
161
+ return nil if pipeline.shutdown?
162
+ # Assume the pipeline was sent in one piece, but execution of
163
+ # SHUTDOWN caused none of the replies for commands that were executed
164
+ # prior to it from coming back around.
165
+ raise e
166
+ end
167
+ end
168
+ end
169
+
170
+ def call_pipelined(commands)
171
+ return [] if commands.empty?
172
+
173
+ # The method #ensure_connected (called from #process) reconnects once on
174
+ # I/O errors. To make an effort in making sure that commands are not
175
+ # executed more than once, only allow reconnection before the first reply
176
+ # has been read. When an error occurs after the first reply has been
177
+ # read, retrying would re-execute the entire pipeline, thus re-issuing
178
+ # already successfully executed commands. To circumvent this, don't retry
179
+ # after the first reply has been read successfully.
180
+
181
+ result = Array.new(commands.size)
182
+ reconnect = @reconnect
183
+
184
+ begin
185
+ exception = nil
186
+
187
+ process(commands) do
188
+ result[0] = read
189
+
190
+ @reconnect = false
191
+
192
+ (commands.size - 1).times do |i|
193
+ reply = read
194
+ result[i + 1] = reply
195
+ exception = reply if exception.nil? && reply.is_a?(CommandError)
196
+ end
197
+ end
198
+
199
+ raise exception if exception
200
+ ensure
201
+ @reconnect = reconnect
202
+ end
203
+
204
+ result
205
+ end
206
+
207
+ def call_with_timeout(command, timeout, &blk)
208
+ with_socket_timeout(timeout) do
209
+ call(command, &blk)
210
+ end
211
+ rescue ConnectionError
212
+ retry
213
+ end
214
+
215
+ def call_without_timeout(command, &blk)
216
+ call_with_timeout(command, 0, &blk)
217
+ end
218
+
219
+ def process(commands)
220
+ logging(commands) do
221
+ ensure_connected do
222
+ commands.each do |command|
223
+ if command_map[command.first]
224
+ command = command.dup
225
+ command[0] = command_map[command.first]
226
+ end
227
+
228
+ write(command)
229
+ end
230
+
231
+ yield if block_given?
232
+ end
233
+ end
234
+ end
235
+
236
+ def connected?
237
+ !! (connection && connection.connected?)
238
+ end
239
+
240
+ def disconnect
241
+ connection.disconnect if connected?
242
+ end
243
+
244
+ def reconnect
245
+ disconnect
246
+ connect
247
+ end
248
+
249
+ def io
250
+ yield
251
+ rescue TimeoutError => e1
252
+ # Add a message to the exception without destroying the original stack
253
+ e2 = TimeoutError.new("Connection timed out")
254
+ e2.set_backtrace(e1.backtrace)
255
+ raise e2
256
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
257
+ raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
258
+ end
259
+
260
+ def read
261
+ io do
262
+ value = connection.read
263
+ @pending_reads -= 1
264
+ value
265
+ end
266
+ end
267
+
268
+ def write(command)
269
+ io do
270
+ @pending_reads += 1
271
+ connection.write(command)
272
+ end
273
+ end
274
+
275
+ def with_socket_timeout(timeout)
276
+ connect unless connected?
277
+
278
+ begin
279
+ connection.timeout = timeout
280
+ yield
281
+ ensure
282
+ connection.timeout = self.timeout if connected?
283
+ end
284
+ end
285
+
286
+ def without_socket_timeout(&blk)
287
+ with_socket_timeout(0, &blk)
288
+ end
289
+
290
+ def with_reconnect(val=true)
291
+ begin
292
+ original, @reconnect = @reconnect, val
293
+ yield
294
+ ensure
295
+ @reconnect = original
296
+ end
297
+ end
298
+
299
+ def without_reconnect(&blk)
300
+ with_reconnect(false, &blk)
301
+ end
302
+
303
+ protected
304
+
305
+ def logging(commands)
306
+ return yield unless @logger && @logger.debug?
307
+
308
+ begin
309
+ commands.each do |name, *args|
310
+ logged_args = args.map do |a|
311
+ case
312
+ when a.respond_to?(:inspect) then a.inspect
313
+ when a.respond_to?(:to_s) then a.to_s
314
+ else
315
+ # handle poorly-behaved descendants of BasicObject
316
+ klass = a.instance_exec { (class << self; self end).superclass }
317
+ "\#<#{klass}:#{a.__id__}>"
318
+ end
319
+ end
320
+ @logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}")
321
+ end
322
+
323
+ t1 = Time.now
324
+ yield
325
+ ensure
326
+ @logger.debug("[Redis] call_time=%0.2f ms" % ((Time.now - t1) * 1000)) if t1
327
+ end
328
+ end
329
+
330
+ def establish_connection
331
+ server = @connector.resolve.dup
332
+
333
+ @options[:host] = server[:host]
334
+ @options[:port] = Integer(server[:port]) if server.include?(:port)
335
+
336
+ @connection = @options[:driver].connect(@options)
337
+ @pending_reads = 0
338
+ rescue TimeoutError,
339
+ Errno::ECONNREFUSED,
340
+ Errno::EHOSTDOWN,
341
+ Errno::EHOSTUNREACH,
342
+ Errno::ENETUNREACH,
343
+ Errno::ETIMEDOUT
344
+
345
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
346
+ end
347
+
348
+ def ensure_connected
349
+ disconnect if @pending_reads > 0
350
+
351
+ attempts = 0
352
+
353
+ begin
354
+ attempts += 1
355
+
356
+ if connected?
357
+ unless inherit_socket? || Process.pid == @pid
358
+ raise InheritedError,
359
+ "Tried to use a connection from a child process without reconnecting. " +
360
+ "You need to reconnect to Redis after forking " +
361
+ "or set :inherit_socket to true."
362
+ end
363
+ else
364
+ connect
365
+ end
366
+
367
+ yield
368
+ rescue BaseConnectionError
369
+ disconnect
370
+
371
+ if attempts <= @options[:reconnect_attempts] && @reconnect
372
+ retry
373
+ else
374
+ raise
375
+ end
376
+ rescue Exception
377
+ disconnect
378
+ raise
379
+ end
380
+ end
381
+
382
+ def _parse_options(options)
383
+ return options if options[:_parsed]
384
+
385
+ defaults = DEFAULTS.dup
386
+ options = options.dup
387
+
388
+ defaults.keys.each do |key|
389
+ # Fill in defaults if needed
390
+ if defaults[key].respond_to?(:call)
391
+ defaults[key] = defaults[key].call
392
+ end
393
+
394
+ # Symbolize only keys that are needed
395
+ options[key] = options[key.to_s] if options.has_key?(key.to_s)
396
+ end
397
+
398
+ url = options[:url] || defaults[:url]
399
+
400
+ # Override defaults from URL if given
401
+ if url
402
+ require "uri"
403
+
404
+ uri = URI(url)
405
+
406
+ if uri.scheme == "unix"
407
+ defaults[:path] = uri.path
408
+ elsif uri.scheme == "redis" || uri.scheme == "rediss"
409
+ defaults[:scheme] = uri.scheme
410
+ defaults[:host] = uri.host if uri.host
411
+ defaults[:port] = uri.port if uri.port
412
+ defaults[:password] = CGI.unescape(uri.password) if uri.password
413
+ defaults[:db] = uri.path[1..-1].to_i if uri.path
414
+ defaults[:role] = :master
415
+ else
416
+ raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
417
+ end
418
+
419
+ defaults[:ssl] = true if uri.scheme == "rediss"
420
+ end
421
+
422
+ # Use default when option is not specified or nil
423
+ defaults.keys.each do |key|
424
+ options[key] = defaults[key] if options[key].nil?
425
+ end
426
+
427
+ if options[:path]
428
+ # Unix socket
429
+ options[:scheme] = "unix"
430
+ options.delete(:host)
431
+ options.delete(:port)
432
+ else
433
+ # TCP socket
434
+ options[:host] = options[:host].to_s
435
+ options[:port] = options[:port].to_i
436
+ end
437
+
438
+ if options.has_key?(:timeout)
439
+ options[:connect_timeout] ||= options[:timeout]
440
+ options[:read_timeout] ||= options[:timeout]
441
+ options[:write_timeout] ||= options[:timeout]
442
+ end
443
+
444
+ options[:connect_timeout] = Float(options[:connect_timeout])
445
+ options[:read_timeout] = Float(options[:read_timeout])
446
+ options[:write_timeout] = Float(options[:write_timeout])
447
+
448
+ options[:db] = options[:db].to_i
449
+ options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
450
+
451
+ case options[:tcp_keepalive]
452
+ when Hash
453
+ [:time, :intvl, :probes].each do |key|
454
+ unless options[:tcp_keepalive][key].is_a?(Integer)
455
+ raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
456
+ end
457
+ end
458
+
459
+ when Integer
460
+ if options[:tcp_keepalive] >= 60
461
+ options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
462
+
463
+ elsif options[:tcp_keepalive] >= 30
464
+ options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
465
+
466
+ elsif options[:tcp_keepalive] >= 5
467
+ options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
468
+ end
469
+ end
470
+
471
+ options[:_parsed] = true
472
+
473
+ options
474
+ end
475
+
476
+ def _parse_driver(driver)
477
+ driver = driver.to_s if driver.is_a?(Symbol)
478
+
479
+ if driver.kind_of?(String)
480
+ begin
481
+ require "redis/connection/#{driver}"
482
+ driver = Connection.const_get(driver.capitalize)
483
+ rescue LoadError, NameError
484
+ raise RuntimeError, "Cannot load driver #{driver.inspect}"
485
+ end
486
+ end
487
+
488
+ driver
489
+ end
490
+
491
+ class Connector
492
+ def initialize(options)
493
+ @options = options.dup
494
+ end
495
+
496
+ def resolve
497
+ @options
498
+ end
499
+
500
+ def check(client)
501
+ end
502
+
503
+ class Sentinel < Connector
504
+ def initialize(options)
505
+ super(options)
506
+
507
+ @options[:password] = DEFAULTS.fetch(:password)
508
+ @options[:db] = DEFAULTS.fetch(:db)
509
+
510
+ @sentinels = @options.delete(:sentinels).dup
511
+ @role = @options.fetch(:role, "master").to_s
512
+ @master = @options[:host]
513
+ end
514
+
515
+ def check(client)
516
+ # Check the instance is really of the role we are looking for.
517
+ # We can't assume the command is supported since it was introduced
518
+ # recently and this client should work with old stuff.
519
+ begin
520
+ role = client.call([:role])[0]
521
+ rescue Redis::CommandError
522
+ # Assume the test is passed if we can't get a reply from ROLE...
523
+ role = @role
524
+ end
525
+
526
+ if role != @role
527
+ client.disconnect
528
+ raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
529
+ end
530
+ end
531
+
532
+ def resolve
533
+ result = case @role
534
+ when "master"
535
+ resolve_master
536
+ when "slave"
537
+ resolve_slave
538
+ else
539
+ raise ArgumentError, "Unknown instance role #{@role}"
540
+ end
541
+
542
+ result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
543
+ end
544
+
545
+ def sentinel_detect
546
+ @sentinels.each do |sentinel|
547
+ client = Client.new(@options.merge({
548
+ :host => sentinel[:host],
549
+ :port => sentinel[:port],
550
+ :reconnect_attempts => 0,
551
+ }))
552
+
553
+ begin
554
+ if result = yield(client)
555
+ # This sentinel responded. Make sure we ask it first next time.
556
+ @sentinels.delete(sentinel)
557
+ @sentinels.unshift(sentinel)
558
+
559
+ return result
560
+ end
561
+ rescue BaseConnectionError
562
+ ensure
563
+ client.disconnect
564
+ end
565
+ end
566
+
567
+ raise CannotConnectError, "No sentinels available."
568
+ end
569
+
570
+ def resolve_master
571
+ sentinel_detect do |client|
572
+ if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
573
+ {:host => reply[0], :port => reply[1]}
574
+ end
575
+ end
576
+ end
577
+
578
+ def resolve_slave
579
+ sentinel_detect do |client|
580
+ if reply = client.call(["sentinel", "slaves", @master])
581
+ slave = Hash[*reply.sample]
582
+
583
+ {:host => slave.fetch("ip"), :port => slave.fetch("port")}
584
+ end
585
+ end
586
+ end
587
+ end
588
+ end
589
+ end
590
+ end
@@ -0,0 +1,44 @@
1
+ class Redis
2
+ module Connection
3
+ module CommandHelper
4
+
5
+ COMMAND_DELIMITER = "\r\n"
6
+
7
+ def build_command(args)
8
+ command = [nil]
9
+
10
+ args.each do |i|
11
+ if i.is_a? Array
12
+ i.each do |j|
13
+ j = j.to_s
14
+ command << "$#{j.bytesize}"
15
+ command << j
16
+ end
17
+ else
18
+ i = i.to_s
19
+ command << "$#{i.bytesize}"
20
+ command << i
21
+ end
22
+ end
23
+
24
+ command[0] = "*#{(command.length - 1) / 2}"
25
+
26
+ # Trailing delimiter
27
+ command << ""
28
+ command.join(COMMAND_DELIMITER)
29
+ end
30
+
31
+ protected
32
+
33
+ if defined?(Encoding::default_external)
34
+ def encode(string)
35
+ string.force_encoding(Encoding::default_external)
36
+ end
37
+ else
38
+ def encode(string)
39
+ string
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ require "redis/connection/registry"
2
+ require "redis/errors"
3
+ require "hiredis/connection"
4
+ require "timeout"
5
+
6
+ class Redis
7
+ module Connection
8
+ class Hiredis
9
+
10
+ def self.connect(config)
11
+ connection = ::Hiredis::Connection.new
12
+ connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i
13
+
14
+ if config[:scheme] == "unix"
15
+ connection.connect_unix(config[:path], connect_timeout)
16
+ elsif config[:scheme] == "rediss" || config[:ssl]
17
+ raise NotImplementedError, "SSL not supported by hiredis driver"
18
+ else
19
+ connection.connect(config[:host], config[:port], connect_timeout)
20
+ end
21
+
22
+ instance = new(connection)
23
+ instance.timeout = config[:read_timeout]
24
+ instance
25
+ rescue Errno::ETIMEDOUT
26
+ raise TimeoutError
27
+ end
28
+
29
+ def initialize(connection)
30
+ @connection = connection
31
+ end
32
+
33
+ def connected?
34
+ @connection && @connection.connected?
35
+ end
36
+
37
+ def timeout=(timeout)
38
+ # Hiredis works with microsecond timeouts
39
+ @connection.timeout = Integer(timeout * 1_000_000)
40
+ end
41
+
42
+ def disconnect
43
+ @connection.disconnect
44
+ @connection = nil
45
+ end
46
+
47
+ def write(command)
48
+ @connection.write(command.flatten(1))
49
+ rescue Errno::EAGAIN
50
+ raise TimeoutError
51
+ end
52
+
53
+ def read
54
+ reply = @connection.read
55
+ reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError)
56
+ reply
57
+ rescue Errno::EAGAIN
58
+ raise TimeoutError
59
+ rescue RuntimeError => err
60
+ raise ProtocolError.new(err.message)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ Redis::Connection.drivers << Redis::Connection::Hiredis
@@ -0,0 +1,12 @@
1
+ class Redis
2
+ module Connection
3
+
4
+ # Store a list of loaded connection drivers in the Connection module.
5
+ # Redis::Client uses the last required driver by default, and will be aware
6
+ # of the loaded connection drivers if the user chooses to override the
7
+ # default connection driver.
8
+ def self.drivers
9
+ @drivers ||= []
10
+ end
11
+ end
12
+ end