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,40 @@
1
+ class Redis
2
+ # Base error for all redis-rb errors.
3
+ class BaseError < RuntimeError
4
+ end
5
+
6
+ # Raised by the connection when a protocol error occurs.
7
+ class ProtocolError < BaseError
8
+ def initialize(reply_type)
9
+ super(<<-EOS.gsub(/(?:^|\n)\s*/, " "))
10
+ Got '#{reply_type}' as initial reply byte.
11
+ If you're in a forking environment, such as Unicorn, you need to
12
+ connect to Redis after forking.
13
+ EOS
14
+ end
15
+ end
16
+
17
+ # Raised by the client when command execution returns an error reply.
18
+ class CommandError < BaseError
19
+ end
20
+
21
+ # Base error for connection related errors.
22
+ class BaseConnectionError < BaseError
23
+ end
24
+
25
+ # Raised when connection to a Redis server cannot be made.
26
+ class CannotConnectError < BaseConnectionError
27
+ end
28
+
29
+ # Raised when connection to a Redis server is lost.
30
+ class ConnectionError < BaseConnectionError
31
+ end
32
+
33
+ # Raised when performing I/O times out.
34
+ class TimeoutError < BaseConnectionError
35
+ end
36
+
37
+ # Raised when the connection was inherited by a child process.
38
+ class InheritedError < BaseConnectionError
39
+ end
40
+ end
@@ -0,0 +1,132 @@
1
+ require 'zlib'
2
+
3
+ class Redis
4
+ class HashRing
5
+
6
+ POINTS_PER_SERVER = 160 # this is the default in libmemcached
7
+
8
+ attr_reader :ring, :sorted_keys, :replicas, :nodes
9
+
10
+ # nodes is a list of objects that have a proper to_s representation.
11
+ # replicas indicates how many virtual points should be used pr. node,
12
+ # replicas are required to improve the distribution.
13
+ def initialize(nodes=[], replicas=POINTS_PER_SERVER)
14
+ @replicas = replicas
15
+ @ring = {}
16
+ @nodes = []
17
+ @sorted_keys = []
18
+ nodes.each do |node|
19
+ add_node(node)
20
+ end
21
+ end
22
+
23
+ # Adds a `node` to the hash ring (including a number of replicas).
24
+ def add_node(node)
25
+ @nodes << node
26
+ @replicas.times do |i|
27
+ key = Zlib.crc32("#{node.id}:#{i}")
28
+ raise "Node ID collision" if @ring.has_key?(key)
29
+ @ring[key] = node
30
+ @sorted_keys << key
31
+ end
32
+ @sorted_keys.sort!
33
+ end
34
+
35
+ def remove_node(node)
36
+ @nodes.reject!{|n| n.id == node.id}
37
+ @replicas.times do |i|
38
+ key = Zlib.crc32("#{node.id}:#{i}")
39
+ @ring.delete(key)
40
+ @sorted_keys.reject! {|k| k == key}
41
+ end
42
+ end
43
+
44
+ # get the node in the hash ring for this key
45
+ def get_node(key)
46
+ get_node_pos(key)[0]
47
+ end
48
+
49
+ def get_node_pos(key)
50
+ return [nil,nil] if @ring.size == 0
51
+ crc = Zlib.crc32(key)
52
+ idx = HashRing.binary_search(@sorted_keys, crc)
53
+ return [@ring[@sorted_keys[idx]], idx]
54
+ end
55
+
56
+ def iter_nodes(key)
57
+ return [nil,nil] if @ring.size == 0
58
+ _, pos = get_node_pos(key)
59
+ @ring.size.times do |n|
60
+ yield @ring[@sorted_keys[(pos+n) % @ring.size]]
61
+ end
62
+ end
63
+
64
+ class << self
65
+
66
+ # gem install RubyInline to use this code
67
+ # Native extension to perform the binary search within the hashring.
68
+ # There's a pure ruby version below so this is purely optional
69
+ # for performance. In testing 20k gets and sets, the native
70
+ # binary search shaved about 12% off the runtime (9sec -> 8sec).
71
+ begin
72
+ require 'inline'
73
+ inline do |builder|
74
+ builder.c <<-EOM
75
+ int binary_search(VALUE ary, unsigned int r) {
76
+ int upper = RARRAY_LEN(ary) - 1;
77
+ int lower = 0;
78
+ int idx = 0;
79
+
80
+ while (lower <= upper) {
81
+ idx = (lower + upper) / 2;
82
+
83
+ VALUE continuumValue = RARRAY_PTR(ary)[idx];
84
+ unsigned int l = NUM2UINT(continuumValue);
85
+ if (l == r) {
86
+ return idx;
87
+ }
88
+ else if (l > r) {
89
+ upper = idx - 1;
90
+ }
91
+ else {
92
+ lower = idx + 1;
93
+ }
94
+ }
95
+ if (upper < 0) {
96
+ upper = RARRAY_LEN(ary) - 1;
97
+ }
98
+ return upper;
99
+ }
100
+ EOM
101
+ end
102
+ rescue Exception
103
+ # Find the closest index in HashRing with value <= the given value
104
+ def binary_search(ary, value, &block)
105
+ upper = ary.size - 1
106
+ lower = 0
107
+ idx = 0
108
+
109
+ while(lower <= upper) do
110
+ idx = (lower + upper) / 2
111
+ comp = ary[idx] <=> value
112
+
113
+ if comp == 0
114
+ return idx
115
+ elsif comp > 0
116
+ upper = idx - 1
117
+ else
118
+ lower = idx + 1
119
+ end
120
+ end
121
+
122
+ if upper < 0
123
+ upper = ary.size - 1
124
+ end
125
+ return upper
126
+ end
127
+
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,141 @@
1
+ class Redis
2
+ unless defined?(::BasicObject)
3
+ class BasicObject
4
+ instance_methods.each { |meth| undef_method(meth) unless meth =~ /\A(__|instance_eval)/ }
5
+ end
6
+ end
7
+
8
+ class Pipeline
9
+ attr_accessor :db
10
+
11
+ attr :futures
12
+
13
+ def initialize
14
+ @with_reconnect = true
15
+ @shutdown = false
16
+ @futures = []
17
+ end
18
+
19
+ def with_reconnect?
20
+ @with_reconnect
21
+ end
22
+
23
+ def without_reconnect?
24
+ !@with_reconnect
25
+ end
26
+
27
+ def shutdown?
28
+ @shutdown
29
+ end
30
+
31
+ def call(command, &block)
32
+ # A pipeline that contains a shutdown should not raise ECONNRESET when
33
+ # the connection is gone.
34
+ @shutdown = true if command.first == :shutdown
35
+ future = Future.new(command, block)
36
+ @futures << future
37
+ future
38
+ end
39
+
40
+ def call_pipeline(pipeline)
41
+ @shutdown = true if pipeline.shutdown?
42
+ @futures.concat(pipeline.futures)
43
+ @db = pipeline.db
44
+ nil
45
+ end
46
+
47
+ def commands
48
+ @futures.map { |f| f._command }
49
+ end
50
+
51
+ def with_reconnect(val=true)
52
+ @with_reconnect = false unless val
53
+ yield
54
+ end
55
+
56
+ def without_reconnect(&blk)
57
+ with_reconnect(false, &blk)
58
+ end
59
+
60
+ def finish(replies, &blk)
61
+ if blk
62
+ futures.each_with_index.map do |future, i|
63
+ future._set(blk.call(replies[i]))
64
+ end
65
+ else
66
+ futures.each_with_index.map do |future, i|
67
+ future._set(replies[i])
68
+ end
69
+ end
70
+ end
71
+
72
+ class Multi < self
73
+ def finish(replies)
74
+ exec = replies.last
75
+
76
+ return if exec.nil? # The transaction failed because of WATCH.
77
+
78
+ # EXEC command failed.
79
+ raise exec if exec.is_a?(CommandError)
80
+
81
+ if exec.size < futures.size
82
+ # Some command wasn't recognized by Redis.
83
+ raise replies.detect { |r| r.is_a?(CommandError) }
84
+ end
85
+
86
+ super(exec) do |reply|
87
+ # Because an EXEC returns nested replies, hiredis won't be able to
88
+ # convert an error reply to a CommandError instance itself. This is
89
+ # specific to MULTI/EXEC, so we solve this here.
90
+ reply.is_a?(::RuntimeError) ? CommandError.new(reply.message) : reply
91
+ end
92
+ end
93
+
94
+ def commands
95
+ [[:multi]] + super + [[:exec]]
96
+ end
97
+ end
98
+ end
99
+
100
+ class FutureNotReady < RuntimeError
101
+ def initialize
102
+ super("Value will be available once the pipeline executes.")
103
+ end
104
+ end
105
+
106
+ class Future < BasicObject
107
+ FutureNotReady = ::Redis::FutureNotReady.new
108
+
109
+ def initialize(command, transformation)
110
+ @command = command
111
+ @transformation = transformation
112
+ @object = FutureNotReady
113
+ end
114
+
115
+ def inspect
116
+ "<Redis::Future #{@command.inspect}>"
117
+ end
118
+
119
+ def _set(object)
120
+ @object = @transformation ? @transformation.call(object) : object
121
+ value
122
+ end
123
+
124
+ def _command
125
+ @command
126
+ end
127
+
128
+ def value
129
+ ::Kernel.raise(@object) if @object.kind_of?(::RuntimeError)
130
+ @object
131
+ end
132
+
133
+ def is_a?(other)
134
+ self.class.ancestors.include?(other)
135
+ end
136
+
137
+ def class
138
+ Future
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,83 @@
1
+ class Redis
2
+ class SubscribedClient
3
+ def initialize(client)
4
+ @client = client
5
+ end
6
+
7
+ def call(command)
8
+ @client.process([command])
9
+ end
10
+
11
+ def subscribe(*channels, &block)
12
+ subscription("subscribe", "unsubscribe", channels, block)
13
+ end
14
+
15
+ def psubscribe(*channels, &block)
16
+ subscription("psubscribe", "punsubscribe", channels, block)
17
+ end
18
+
19
+ def unsubscribe(*channels)
20
+ call([:unsubscribe, *channels])
21
+ end
22
+
23
+ def punsubscribe(*channels)
24
+ call([:punsubscribe, *channels])
25
+ end
26
+
27
+ protected
28
+
29
+ def subscription(start, stop, channels, block)
30
+ sub = Subscription.new(&block)
31
+
32
+ unsubscribed = false
33
+
34
+ begin
35
+ @client.call_loop([start, *channels]) do |line|
36
+ type, *rest = line
37
+ sub.callbacks[type].call(*rest)
38
+ unsubscribed = type == stop && rest.last == 0
39
+ break if unsubscribed
40
+ end
41
+ ensure
42
+ # No need to unsubscribe here. The real client closes the connection
43
+ # whenever an exception is raised (see #ensure_connected).
44
+ end
45
+ end
46
+ end
47
+
48
+ class Subscription
49
+ attr :callbacks
50
+
51
+ def initialize
52
+ @callbacks = Hash.new do |hash, key|
53
+ hash[key] = lambda { |*_| }
54
+ end
55
+
56
+ yield(self)
57
+ end
58
+
59
+ def subscribe(&block)
60
+ @callbacks["subscribe"] = block
61
+ end
62
+
63
+ def unsubscribe(&block)
64
+ @callbacks["unsubscribe"] = block
65
+ end
66
+
67
+ def message(&block)
68
+ @callbacks["message"] = block
69
+ end
70
+
71
+ def psubscribe(&block)
72
+ @callbacks["psubscribe"] = block
73
+ end
74
+
75
+ def punsubscribe(&block)
76
+ @callbacks["punsubscribe"] = block
77
+ end
78
+
79
+ def pmessage(&block)
80
+ @callbacks["pmessage"] = block
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,3 @@
1
+ class Redis
2
+ VERSION = "3.2.2"
3
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $:.unshift File.expand_path("../lib", __FILE__)
4
+
5
+ require "redis/version"
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "discourse-redis"
9
+
10
+ s.version = Redis::VERSION
11
+
12
+ s.homepage = "https://github.com/discourse/redis-rb"
13
+
14
+ s.summary = "A Ruby client library for Redis"
15
+
16
+ s.description = <<-EOS
17
+ A Ruby client that tries to match Redis' API one-to-one, while still
18
+ providing an idiomatic interface. It features thread-safety,
19
+ client-side sharding, pipelining, and an obsession for performance.
20
+ EOS
21
+
22
+ s.license = "MIT"
23
+
24
+ s.authors = ["Alan Tan Guo Xiang"]
25
+
26
+ s.email = ["tgx_world@hotmail.com"]
27
+
28
+ s.files = `git ls-files`.split("\n")
29
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
30
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
31
+
32
+ s.add_development_dependency("rake")
33
+ s.add_development_dependency("test-unit")
34
+ end
@@ -0,0 +1,69 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path("helper", File.dirname(__FILE__))
4
+
5
+ unless defined?(Enumerator)
6
+ Enumerator = Enumerable::Enumerator
7
+ end
8
+
9
+ class TestBitpos < Test::Unit::TestCase
10
+
11
+ include Helper::Client
12
+
13
+ def test_bitpos_empty_zero
14
+ target_version "2.9.11" do
15
+ r.del "foo"
16
+ assert_equal 0, r.bitpos("foo", 0)
17
+ end
18
+ end
19
+
20
+ def test_bitpos_empty_one
21
+ target_version "2.9.11" do
22
+ r.del "foo"
23
+ assert_equal -1, r.bitpos("foo", 1)
24
+ end
25
+ end
26
+
27
+ def test_bitpos_zero
28
+ target_version "2.9.11" do
29
+ r.set "foo", "\xff\xf0\x00"
30
+ assert_equal 12, r.bitpos("foo", 0)
31
+ end
32
+ end
33
+
34
+ def test_bitpos_one
35
+ target_version "2.9.11" do
36
+ r.set "foo", "\x00\x0f\x00"
37
+ assert_equal 12, r.bitpos("foo", 1)
38
+ end
39
+ end
40
+
41
+ def test_bitpos_zero_end_is_given
42
+ target_version "2.9.11" do
43
+ r.set "foo", "\xff\xff\xff"
44
+ assert_equal 24, r.bitpos("foo", 0)
45
+ assert_equal 24, r.bitpos("foo", 0, 0)
46
+ assert_equal -1, r.bitpos("foo", 0, 0, -1)
47
+ end
48
+ end
49
+
50
+ def test_bitpos_one_intervals
51
+ target_version "2.9.11" do
52
+ r.set "foo", "\x00\xff\x00"
53
+ assert_equal 8, r.bitpos("foo", 1, 0, -1)
54
+ assert_equal 8, r.bitpos("foo", 1, 1, -1)
55
+ assert_equal -1, r.bitpos("foo", 1, 2, -1)
56
+ assert_equal -1, r.bitpos("foo", 1, 2, 200)
57
+ assert_equal 8, r.bitpos("foo", 1, 1, 1)
58
+ end
59
+ end
60
+
61
+ def test_bitpos_raise_exception_if_stop_not_start
62
+ target_version "2.9.11" do
63
+ assert_raises(ArgumentError) do
64
+ r.bitpos("foo", 0, nil, 2)
65
+ end
66
+ end
67
+ end
68
+
69
+ end