discourse-redis 3.2.2

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