discourse-redis 3.2.2

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