modesty 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. data/Gemfile +13 -0
  2. data/Gemfile.lock +18 -0
  3. data/LICENSE +21 -0
  4. data/README.md +121 -0
  5. data/Rakefile +29 -0
  6. data/VERSION +1 -0
  7. data/init.rb +1 -0
  8. data/lib/modesty.rb +26 -0
  9. data/lib/modesty/api.rb +14 -0
  10. data/lib/modesty/core_ext.rb +5 -0
  11. data/lib/modesty/core_ext/array.rb +21 -0
  12. data/lib/modesty/core_ext/fixnum.rb +5 -0
  13. data/lib/modesty/core_ext/hash.rb +39 -0
  14. data/lib/modesty/core_ext/string.rb +9 -0
  15. data/lib/modesty/core_ext/symbol.rb +33 -0
  16. data/lib/modesty/datastore.rb +51 -0
  17. data/lib/modesty/datastore/redis.rb +180 -0
  18. data/lib/modesty/experiment.rb +87 -0
  19. data/lib/modesty/experiment/base.rb +47 -0
  20. data/lib/modesty/experiment/builder.rb +48 -0
  21. data/lib/modesty/experiment/console.rb +4 -0
  22. data/lib/modesty/experiment/data.rb +75 -0
  23. data/lib/modesty/experiment/interface.rb +29 -0
  24. data/lib/modesty/experiment/significance.rb +376 -0
  25. data/lib/modesty/experiment/stats.rb +163 -0
  26. data/lib/modesty/frameworks/rails.rb +27 -0
  27. data/lib/modesty/identity.rb +32 -0
  28. data/lib/modesty/load.rb +80 -0
  29. data/lib/modesty/load/load_experiments.rb +14 -0
  30. data/lib/modesty/load/load_metrics.rb +17 -0
  31. data/lib/modesty/metric.rb +56 -0
  32. data/lib/modesty/metric/base.rb +38 -0
  33. data/lib/modesty/metric/builder.rb +23 -0
  34. data/lib/modesty/metric/data.rb +133 -0
  35. data/modesty.gemspec +192 -0
  36. data/spec/core_ext_spec.rb +17 -0
  37. data/spec/experiment_spec.rb +239 -0
  38. data/spec/identity_spec.rb +161 -0
  39. data/spec/load_spec.rb +87 -0
  40. data/spec/metric_spec.rb +176 -0
  41. data/spec/rails_spec.rb +48 -0
  42. data/spec/redis_spec.rb +29 -0
  43. data/spec/significance_spec.rb +147 -0
  44. data/spec/spec.opts +1 -0
  45. data/test/myapp/config/modesty.yml +9 -0
  46. data/test/myapp/modesty/experiments/cookbook.rb +4 -0
  47. data/test/myapp/modesty/metrics/kitchen_metrics.rb +9 -0
  48. data/test/myapp/modesty/metrics/stove/burner_metrics.rb +2 -0
  49. data/vendor/.piston.yml +8 -0
  50. data/vendor/mock_redis/.gitignore +2 -0
  51. data/vendor/mock_redis/README +8 -0
  52. data/vendor/mock_redis/lib/mock_redis.rb +10 -0
  53. data/vendor/mock_redis/lib/mock_redis/hash.rb +61 -0
  54. data/vendor/mock_redis/lib/mock_redis/list.rb +6 -0
  55. data/vendor/mock_redis/lib/mock_redis/misc.rb +69 -0
  56. data/vendor/mock_redis/lib/mock_redis/set.rb +108 -0
  57. data/vendor/mock_redis/lib/mock_redis/string.rb +32 -0
  58. data/vendor/redis-rb/.gitignore +8 -0
  59. data/vendor/redis-rb/LICENSE +20 -0
  60. data/vendor/redis-rb/README.markdown +129 -0
  61. data/vendor/redis-rb/Rakefile +155 -0
  62. data/vendor/redis-rb/benchmarking/logging.rb +62 -0
  63. data/vendor/redis-rb/benchmarking/pipeline.rb +51 -0
  64. data/vendor/redis-rb/benchmarking/speed.rb +21 -0
  65. data/vendor/redis-rb/benchmarking/suite.rb +24 -0
  66. data/vendor/redis-rb/benchmarking/thread_safety.rb +38 -0
  67. data/vendor/redis-rb/benchmarking/worker.rb +71 -0
  68. data/vendor/redis-rb/examples/basic.rb +15 -0
  69. data/vendor/redis-rb/examples/dist_redis.rb +43 -0
  70. data/vendor/redis-rb/examples/incr-decr.rb +17 -0
  71. data/vendor/redis-rb/examples/list.rb +26 -0
  72. data/vendor/redis-rb/examples/pubsub.rb +31 -0
  73. data/vendor/redis-rb/examples/sets.rb +36 -0
  74. data/vendor/redis-rb/examples/unicorn/config.ru +3 -0
  75. data/vendor/redis-rb/examples/unicorn/unicorn.rb +20 -0
  76. data/vendor/redis-rb/lib/redis.rb +676 -0
  77. data/vendor/redis-rb/lib/redis/client.rb +201 -0
  78. data/vendor/redis-rb/lib/redis/compat.rb +21 -0
  79. data/vendor/redis-rb/lib/redis/connection.rb +134 -0
  80. data/vendor/redis-rb/lib/redis/distributed.rb +526 -0
  81. data/vendor/redis-rb/lib/redis/hash_ring.rb +131 -0
  82. data/vendor/redis-rb/lib/redis/pipeline.rb +13 -0
  83. data/vendor/redis-rb/lib/redis/subscribe.rb +79 -0
  84. data/vendor/redis-rb/redis.gemspec +29 -0
  85. data/vendor/redis-rb/test/commands_on_hashes_test.rb +46 -0
  86. data/vendor/redis-rb/test/commands_on_lists_test.rb +50 -0
  87. data/vendor/redis-rb/test/commands_on_sets_test.rb +78 -0
  88. data/vendor/redis-rb/test/commands_on_sorted_sets_test.rb +109 -0
  89. data/vendor/redis-rb/test/commands_on_strings_test.rb +70 -0
  90. data/vendor/redis-rb/test/commands_on_value_types_test.rb +88 -0
  91. data/vendor/redis-rb/test/connection_handling_test.rb +87 -0
  92. data/vendor/redis-rb/test/db/.gitignore +1 -0
  93. data/vendor/redis-rb/test/distributd_key_tags_test.rb +53 -0
  94. data/vendor/redis-rb/test/distributed_blocking_commands_test.rb +54 -0
  95. data/vendor/redis-rb/test/distributed_commands_on_hashes_test.rb +12 -0
  96. data/vendor/redis-rb/test/distributed_commands_on_lists_test.rb +18 -0
  97. data/vendor/redis-rb/test/distributed_commands_on_sets_test.rb +85 -0
  98. data/vendor/redis-rb/test/distributed_commands_on_strings_test.rb +50 -0
  99. data/vendor/redis-rb/test/distributed_commands_on_value_types_test.rb +73 -0
  100. data/vendor/redis-rb/test/distributed_commands_requiring_clustering_test.rb +141 -0
  101. data/vendor/redis-rb/test/distributed_connection_handling_test.rb +25 -0
  102. data/vendor/redis-rb/test/distributed_internals_test.rb +18 -0
  103. data/vendor/redis-rb/test/distributed_persistence_control_commands_test.rb +24 -0
  104. data/vendor/redis-rb/test/distributed_publish_subscribe_test.rb +90 -0
  105. data/vendor/redis-rb/test/distributed_remote_server_control_commands_test.rb +31 -0
  106. data/vendor/redis-rb/test/distributed_sorting_test.rb +21 -0
  107. data/vendor/redis-rb/test/distributed_test.rb +60 -0
  108. data/vendor/redis-rb/test/distributed_transactions_test.rb +34 -0
  109. data/vendor/redis-rb/test/encoding_test.rb +16 -0
  110. data/vendor/redis-rb/test/helper.rb +86 -0
  111. data/vendor/redis-rb/test/internals_test.rb +27 -0
  112. data/vendor/redis-rb/test/lint/hashes.rb +90 -0
  113. data/vendor/redis-rb/test/lint/internals.rb +53 -0
  114. data/vendor/redis-rb/test/lint/lists.rb +93 -0
  115. data/vendor/redis-rb/test/lint/sets.rb +66 -0
  116. data/vendor/redis-rb/test/lint/sorted_sets.rb +132 -0
  117. data/vendor/redis-rb/test/lint/strings.rb +98 -0
  118. data/vendor/redis-rb/test/lint/value_types.rb +84 -0
  119. data/vendor/redis-rb/test/persistence_control_commands_test.rb +22 -0
  120. data/vendor/redis-rb/test/pipelining_commands_test.rb +78 -0
  121. data/vendor/redis-rb/test/publish_subscribe_test.rb +151 -0
  122. data/vendor/redis-rb/test/redis_mock.rb +64 -0
  123. data/vendor/redis-rb/test/remote_server_control_commands_test.rb +56 -0
  124. data/vendor/redis-rb/test/sorting_test.rb +44 -0
  125. data/vendor/redis-rb/test/test.conf +8 -0
  126. data/vendor/redis-rb/test/thread_safety_test.rb +34 -0
  127. data/vendor/redis-rb/test/transactions_test.rb +91 -0
  128. data/vendor/redis-rb/test/unknown_commands_test.rb +14 -0
  129. data/vendor/redis-rb/test/url_param_test.rb +52 -0
  130. metadata +277 -0
@@ -0,0 +1,201 @@
1
+ class Redis
2
+ class Client
3
+ attr_accessor :db, :host, :port, :password, :logger
4
+ attr :timeout
5
+ attr :connection
6
+
7
+ def initialize(options = {})
8
+ @host = options[:host] || "127.0.0.1"
9
+ @port = (options[:port] || 6379).to_i
10
+ @db = (options[:db] || 0).to_i
11
+ @timeout = (options[:timeout] || 5).to_i
12
+ @password = options[:password]
13
+ @logger = options[:logger]
14
+ @connection = Connection.new
15
+ end
16
+
17
+ def connect
18
+ connect_to(@host, @port)
19
+ call(:auth, @password) if @password
20
+ call(:select, @db) if @db != 0
21
+ self
22
+ end
23
+
24
+ def id
25
+ "redis://#{host}:#{port}/#{db}"
26
+ end
27
+
28
+ def call(*args)
29
+ process(args) do
30
+ read
31
+ end
32
+ end
33
+
34
+ def call_loop(*args)
35
+ without_socket_timeout do
36
+ process(args) do
37
+ loop { yield(read) }
38
+ end
39
+ end
40
+ end
41
+
42
+ def call_pipelined(commands)
43
+ process(*commands) do
44
+ Array.new(commands.size) { read }
45
+ end
46
+ end
47
+
48
+ def call_without_timeout(*args)
49
+ without_socket_timeout do
50
+ call(*args)
51
+ end
52
+ rescue Errno::ECONNRESET
53
+ retry
54
+ end
55
+
56
+ def process(*commands)
57
+ logging(commands) do
58
+ ensure_connected do
59
+ commands.each do |command|
60
+ connection.write(command)
61
+ end
62
+
63
+ yield if block_given?
64
+ end
65
+ end
66
+ end
67
+
68
+ def connected?
69
+ connection.connected?
70
+ end
71
+
72
+ def disconnect
73
+ connection.disconnect if connection.connected?
74
+ end
75
+
76
+ def reconnect
77
+ disconnect
78
+ connect
79
+ end
80
+
81
+ def read
82
+ begin
83
+ connection.read
84
+
85
+ rescue Errno::EAGAIN
86
+ # We want to make sure it reconnects on the next command after the
87
+ # timeout. Otherwise the server may reply in the meantime leaving
88
+ # the protocol in a desync status.
89
+ disconnect
90
+
91
+ raise Errno::EAGAIN, "Timeout reading from the socket"
92
+
93
+ rescue Errno::ECONNRESET
94
+ raise Errno::ECONNRESET, "Connection lost"
95
+ end
96
+ end
97
+
98
+ def without_socket_timeout
99
+ connect unless connected?
100
+
101
+ begin
102
+ self.timeout = 0
103
+ yield
104
+ ensure
105
+ self.timeout = @timeout if connected?
106
+ end
107
+ end
108
+
109
+ protected
110
+
111
+ def deprecated(old, new = nil, trace = caller[0])
112
+ message = "The method #{old} is deprecated and will be removed in 2.0"
113
+ message << " - use #{new} instead" if new
114
+ Redis.deprecate(message, trace)
115
+ end
116
+
117
+ def logging(commands)
118
+ return yield unless @logger && @logger.debug?
119
+
120
+ begin
121
+ commands.each do |name, *args|
122
+ @logger.debug("Redis >> #{name.to_s.upcase} #{args.join(" ")}")
123
+ end
124
+
125
+ t1 = Time.now
126
+ yield
127
+ ensure
128
+ @logger.debug("Redis >> %0.2fms" % ((Time.now - t1) * 1000))
129
+ end
130
+ end
131
+
132
+ def connect_to(host, port)
133
+ with_timeout(@timeout) do
134
+ connection.connect(host, port)
135
+ end
136
+
137
+ # If the timeout is set we set the low level socket options in order
138
+ # to make sure a blocking read will return after the specified number
139
+ # of seconds. This hack is from memcached ruby client.
140
+ self.timeout = @timeout
141
+
142
+ rescue Errno::ECONNREFUSED
143
+ raise Errno::ECONNREFUSED, "Unable to connect to Redis on #{host}:#{port}"
144
+ end
145
+
146
+ def timeout=(timeout)
147
+ connection.timeout = Integer(timeout * 1_000_000)
148
+ end
149
+
150
+ def ensure_connected
151
+ connect unless connected?
152
+
153
+ begin
154
+ yield
155
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF
156
+ if reconnect
157
+ yield
158
+ else
159
+ raise Errno::ECONNRESET
160
+ end
161
+ rescue Exception
162
+ disconnect
163
+ raise
164
+ end
165
+ end
166
+
167
+ class ThreadSafe < self
168
+ def initialize(*args)
169
+ require "monitor"
170
+
171
+ super(*args)
172
+ @mutex = ::Monitor.new
173
+ end
174
+
175
+ def synchronize(&block)
176
+ @mutex.synchronize(&block)
177
+ end
178
+
179
+ def ensure_connected(&block)
180
+ synchronize { super }
181
+ end
182
+ end
183
+
184
+ begin
185
+ require "system_timer"
186
+
187
+ def with_timeout(seconds, &block)
188
+ SystemTimer.timeout_after(seconds, &block)
189
+ end
190
+
191
+ rescue LoadError
192
+ warn "WARNING: using the built-in Timeout class which is known to have issues when used for opening connections. Install the SystemTimer gem if you want to make sure the Redis client will not hang." unless RUBY_VERSION >= "1.9" || RUBY_PLATFORM =~ /java/
193
+
194
+ require "timeout"
195
+
196
+ def with_timeout(seconds, &block)
197
+ Timeout.timeout(seconds, &block)
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,21 @@
1
+ # This file contains core methods that are present in
2
+ # Ruby 1.9 and not in earlier versions.
3
+
4
+ unless [].respond_to?(:product)
5
+ class Array
6
+ def product(*enums)
7
+ enums.unshift self
8
+ result = [[]]
9
+ while [] != enums
10
+ t, result = result, []
11
+ b, *enums = enums
12
+ t.each do |a|
13
+ b.each do |n|
14
+ result << a + [n]
15
+ end
16
+ end
17
+ end
18
+ result
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,134 @@
1
+ class Redis
2
+ class Connection
3
+ MINUS = "-".freeze
4
+ PLUS = "+".freeze
5
+ COLON = ":".freeze
6
+ DOLLAR = "$".freeze
7
+ ASTERISK = "*".freeze
8
+
9
+ def initialize
10
+ @sock = nil
11
+ end
12
+
13
+ def connected?
14
+ !! @sock
15
+ end
16
+
17
+ def connect(host, port)
18
+ @sock = TCPSocket.new(host, port)
19
+ @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
20
+ end
21
+
22
+ def disconnect
23
+ @sock.close
24
+ rescue
25
+ ensure
26
+ @sock = nil
27
+ end
28
+
29
+ def timeout=(usecs)
30
+ secs = Integer(usecs / 1_000_000)
31
+ usecs = Integer(usecs - (secs * 1_000_000)) # 0 - 999_999
32
+
33
+ optval = [secs, usecs].pack("l_2")
34
+
35
+ begin
36
+ @sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
37
+ @sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
38
+ rescue Errno::ENOPROTOOPT
39
+ end
40
+ end
41
+
42
+ COMMAND_DELIMITER = "\r\n"
43
+
44
+ def write(command)
45
+ @sock.write(build_command(*command).join(COMMAND_DELIMITER))
46
+ @sock.write(COMMAND_DELIMITER)
47
+ end
48
+
49
+ def build_command(name, *args)
50
+ command = []
51
+ command << "*#{args.size + 1}"
52
+ command << "$#{string_size name}"
53
+ command << name
54
+
55
+ args.each do |arg|
56
+ arg = arg.to_s
57
+ command << "$#{string_size arg}"
58
+ command << arg
59
+ end
60
+
61
+ command
62
+ end
63
+
64
+ def read
65
+ # We read the first byte using read() mainly because gets() is
66
+ # immune to raw socket timeouts.
67
+ reply_type = @sock.read(1)
68
+
69
+ raise Errno::ECONNRESET unless reply_type
70
+
71
+ format_reply(reply_type, @sock.gets)
72
+ end
73
+
74
+ def format_reply(reply_type, line)
75
+ case reply_type
76
+ when MINUS then format_error_reply(line)
77
+ when PLUS then format_status_reply(line)
78
+ when COLON then format_integer_reply(line)
79
+ when DOLLAR then format_bulk_reply(line)
80
+ when ASTERISK then format_multi_bulk_reply(line)
81
+ else raise ProtocolError.new(reply_type)
82
+ end
83
+ end
84
+
85
+ def format_error_reply(line)
86
+ raise "-" + line.strip
87
+ end
88
+
89
+ def format_status_reply(line)
90
+ line.strip
91
+ end
92
+
93
+ def format_integer_reply(line)
94
+ line.to_i
95
+ end
96
+
97
+ def format_bulk_reply(line)
98
+ bulklen = line.to_i
99
+ return if bulklen == -1
100
+ reply = encode(@sock.read(bulklen))
101
+ @sock.read(2) # Discard CRLF.
102
+ reply
103
+ end
104
+
105
+ def format_multi_bulk_reply(line)
106
+ n = line.to_i
107
+ return if n == -1
108
+
109
+ Array.new(n) { read }
110
+ end
111
+
112
+ protected
113
+
114
+ if "".respond_to?(:bytesize)
115
+ def string_size(string)
116
+ string.to_s.bytesize
117
+ end
118
+ else
119
+ def string_size(string)
120
+ string.to_s.size
121
+ end
122
+ end
123
+
124
+ if defined?(Encoding::default_external)
125
+ def encode(string)
126
+ string.force_encoding(Encoding::default_external)
127
+ end
128
+ else
129
+ def encode(string)
130
+ string
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,526 @@
1
+ require "redis/hash_ring"
2
+
3
+ class Redis
4
+ class Distributed
5
+
6
+ class CannotDistribute < RuntimeError
7
+ def initialize(command)
8
+ @command = command
9
+ end
10
+
11
+ def message
12
+ "#{@command.to_s.upcase} cannot be used in Redis::Distributed because the keys involved need to be on the same server or because we cannot guarantee that the operation will be atomic."
13
+ end
14
+ end
15
+
16
+ attr_reader :ring
17
+
18
+ def initialize(urls, options = {})
19
+ @tag = options.delete(:tag) || /^\{(.+?)\}/
20
+ @default_options = options
21
+ @ring = HashRing.new urls.map { |url| Redis.connect(options.merge(:url => url)) }
22
+ @subscribed_node = nil
23
+ end
24
+
25
+ def node_for(key)
26
+ @ring.get_node(key_tag(key) || key)
27
+ end
28
+
29
+ def nodes
30
+ @ring.nodes
31
+ end
32
+
33
+ def add_node(url)
34
+ @ring.add_node Redis.connect(@default_options.merge(:url => url))
35
+ end
36
+
37
+ def quit
38
+ on_each_node :quit
39
+ end
40
+
41
+ def select(db)
42
+ on_each_node :select, db
43
+ end
44
+
45
+ def ping
46
+ on_each_node :ping
47
+ end
48
+
49
+ def flushall
50
+ on_each_node :flushall
51
+ end
52
+
53
+ def exists(key)
54
+ node_for(key).exists(key)
55
+ end
56
+
57
+ def del(*keys)
58
+ on_each_node(:del, *keys)
59
+ end
60
+
61
+ def type(key)
62
+ node_for(key).type(key)
63
+ end
64
+
65
+ def keys(glob = "*")
66
+ on_each_node(:keys, glob).flatten
67
+ end
68
+
69
+ def randomkey
70
+ raise CannotDistribute, :randomkey
71
+ end
72
+
73
+ def rename(old_name, new_name)
74
+ ensure_same_node(:rename, old_name, new_name) do |node|
75
+ node.rename(old_name, new_name)
76
+ end
77
+ end
78
+
79
+ def renamenx(old_name, new_name)
80
+ ensure_same_node(:renamenx, old_name, new_name) do |node|
81
+ node.renamenx(old_name, new_name)
82
+ end
83
+ end
84
+
85
+ def dbsize
86
+ on_each_node :dbsize
87
+ end
88
+
89
+ def expire(key, seconds)
90
+ node_for(key).expire(key, seconds)
91
+ end
92
+
93
+ def expireat(key, unix_time)
94
+ node_for(key).expireat(key, unix_time)
95
+ end
96
+
97
+ def persist(key)
98
+ node_for(key).persist(key)
99
+ end
100
+
101
+ def ttl(key)
102
+ node_for(key).ttl(key)
103
+ end
104
+
105
+ def move(key, db)
106
+ node_for(key).move(key, db)
107
+ end
108
+
109
+ def flushdb
110
+ on_each_node :flushdb
111
+ end
112
+
113
+ def set(key, value)
114
+ node_for(key).set(key, value)
115
+ end
116
+
117
+ def setex(key, ttl, value)
118
+ node_for(key).setex(key, ttl, value)
119
+ end
120
+
121
+ def get(key)
122
+ node_for(key).get(key)
123
+ end
124
+
125
+ def getset(key, value)
126
+ node_for(key).getset(key, value)
127
+ end
128
+
129
+ def [](key)
130
+ get(key)
131
+ end
132
+
133
+ def append(key, value)
134
+ node_for(key).append(key, value)
135
+ end
136
+
137
+ def substr(key, start, stop)
138
+ node_for(key).substr(key, start, stop)
139
+ end
140
+
141
+ def []=(key,value)
142
+ set(key, value)
143
+ end
144
+
145
+ def mget(*keys)
146
+ raise CannotDistribute, :mget
147
+ end
148
+
149
+ def mapped_mget(*keys)
150
+ raise CannotDistribute, :mapped_mget
151
+ end
152
+
153
+ def setnx(key, value)
154
+ node_for(key).setnx(key, value)
155
+ end
156
+
157
+ def mset(*args)
158
+ raise CannotDistribute, :mset
159
+ end
160
+
161
+ def mapped_mset(hash)
162
+ mset(*hash.to_a.flatten)
163
+ end
164
+
165
+ def msetnx(*args)
166
+ raise CannotDistribute, :msetnx
167
+ end
168
+
169
+ def mapped_msetnx(hash)
170
+ raise CannotDistribute, :mapped_msetnx
171
+ end
172
+
173
+ def incr(key)
174
+ node_for(key).incr(key)
175
+ end
176
+
177
+ def incrby(key, increment)
178
+ node_for(key).incrby(key, increment)
179
+ end
180
+
181
+ def decr(key)
182
+ node_for(key).decr(key)
183
+ end
184
+
185
+ def decrby(key, decrement)
186
+ node_for(key).decrby(key, decrement)
187
+ end
188
+
189
+ def rpush(key, value)
190
+ node_for(key).rpush(key, value)
191
+ end
192
+
193
+ def lpush(key, value)
194
+ node_for(key).lpush(key, value)
195
+ end
196
+
197
+ def llen(key)
198
+ node_for(key).llen(key)
199
+ end
200
+
201
+ def lrange(key, start, stop)
202
+ node_for(key).lrange(key, start, stop)
203
+ end
204
+
205
+ def ltrim(key, start, stop)
206
+ node_for(key).ltrim(key, start, stop)
207
+ end
208
+
209
+ def lindex(key, index)
210
+ node_for(key).lindex(key, index)
211
+ end
212
+
213
+ def lset(key, index, value)
214
+ node_for(key).lset(key, index, value)
215
+ end
216
+
217
+ def lrem(key, count, value)
218
+ node_for(key).lrem(key, count, value)
219
+ end
220
+
221
+ def lpop(key)
222
+ node_for(key).lpop(key)
223
+ end
224
+
225
+ def rpop(key)
226
+ node_for(key).rpop(key)
227
+ end
228
+
229
+ def rpoplpush(source, destination)
230
+ ensure_same_node(:rpoplpush, source, destination) do |node|
231
+ node.rpoplpush(source, destination)
232
+ end
233
+ end
234
+
235
+ def blpop(key, timeout)
236
+ node_for(key).blpop(key, timeout)
237
+ end
238
+
239
+ def brpop(key, timeout)
240
+ node_for(key).brpop(key, timeout)
241
+ end
242
+
243
+ def sadd(key, value)
244
+ node_for(key).sadd(key, value)
245
+ end
246
+
247
+ def srem(key, value)
248
+ node_for(key).srem(key, value)
249
+ end
250
+
251
+ def spop(key)
252
+ node_for(key).spop(key)
253
+ end
254
+
255
+ def smove(source, destination, member)
256
+ ensure_same_node(:smove, source, destination) do |node|
257
+ node.smove(source, destination, member)
258
+ end
259
+ end
260
+
261
+ def scard(key)
262
+ node_for(key).scard(key)
263
+ end
264
+
265
+ def sismember(key, member)
266
+ node_for(key).sismember(key, member)
267
+ end
268
+
269
+ def sinter(*keys)
270
+ ensure_same_node(:sinter, *keys) do |node|
271
+ node.sinter(*keys)
272
+ end
273
+ end
274
+
275
+ def sinterstore(destination, *keys)
276
+ ensure_same_node(:sinterstore, destination, *keys) do |node|
277
+ node.sinterstore(destination, *keys)
278
+ end
279
+ end
280
+
281
+ def sunion(*keys)
282
+ ensure_same_node(:sunion, *keys) do |node|
283
+ node.sunion(*keys)
284
+ end
285
+ end
286
+
287
+ def sunionstore(destination, *keys)
288
+ ensure_same_node(:sunionstore, destination, *keys) do |node|
289
+ node.sunionstore(destination, *keys)
290
+ end
291
+ end
292
+
293
+ def sdiff(*keys)
294
+ ensure_same_node(:sdiff, *keys) do |node|
295
+ node.sdiff(*keys)
296
+ end
297
+ end
298
+
299
+ def sdiffstore(destination, *keys)
300
+ ensure_same_node(:sdiffstore, destination, *keys) do |node|
301
+ node.sdiffstore(destination, *keys)
302
+ end
303
+ end
304
+
305
+ def smembers(key)
306
+ node_for(key).smembers(key)
307
+ end
308
+
309
+ def srandmember(key)
310
+ node_for(key).srandmember(key)
311
+ end
312
+
313
+ def zadd(key, score, member)
314
+ node_for(key).zadd(key, score, member)
315
+ end
316
+
317
+ def zrem(key, member)
318
+ node_for(key).zrem(key, member)
319
+ end
320
+
321
+ def zincrby(key, increment, member)
322
+ node_for(key).zincrby(key, increment, member)
323
+ end
324
+
325
+ def zrange(key, start, stop, options = {})
326
+ node_for(key).zrange(key, start, stop, options)
327
+ end
328
+
329
+ def zrank(key, member)
330
+ node_for(key).zrank(key, member)
331
+ end
332
+
333
+ def zrevrank(key, member)
334
+ node_for(key).zrevrank(key, member)
335
+ end
336
+
337
+ def zrevrange(key, start, stop, options = {})
338
+ node_for(key).zrevrange(key, start, stop, options)
339
+ end
340
+
341
+ def zremrangebyscore(key, min, max)
342
+ node_for(key).zremrangebyscore(key, min, max)
343
+ end
344
+
345
+ def zremrangebyrank(key, start, stop)
346
+ node_for(key).zremrangebyrank(key, start, stop)
347
+ end
348
+
349
+ def zrangebyscore(key, min, max, options = {})
350
+ node_for(key).zrangebyscore(key, min, max, options)
351
+ end
352
+
353
+ def zcard(key)
354
+ node_for(key).zcard(key)
355
+ end
356
+
357
+ def zscore(key, member)
358
+ node_for(key).zscore(key, member)
359
+ end
360
+
361
+ def zinterstore(destination, keys, options = {})
362
+ ensure_same_node(:zinterstore, destination, *keys) do |node|
363
+ node.zinterstore(destination, keys, options)
364
+ end
365
+ end
366
+
367
+ def zunionstore(destination, keys, options = {})
368
+ ensure_same_node(:zunionstore, destination, *keys) do |node|
369
+ node.zunionstore(destination, keys, options)
370
+ end
371
+ end
372
+
373
+ def hset(key, field, value)
374
+ node_for(key).hset(key, field, value)
375
+ end
376
+
377
+ def hget(key, field)
378
+ node_for(key).hget(key, field)
379
+ end
380
+
381
+ def hdel(key, field)
382
+ node_for(key).hdel(key, field)
383
+ end
384
+
385
+ def hexists(key, field)
386
+ node_for(key).hexists(key, field)
387
+ end
388
+
389
+ def hlen(key)
390
+ node_for(key).hlen(key)
391
+ end
392
+
393
+ def hkeys(key)
394
+ node_for(key).hkeys(key)
395
+ end
396
+
397
+ def hvals(key)
398
+ node_for(key).hvals(key)
399
+ end
400
+
401
+ def hgetall(key)
402
+ node_for(key).hgetall(key)
403
+ end
404
+
405
+ def hmset(key, *attrs)
406
+ node_for(key).hmset(key, *attrs)
407
+ end
408
+
409
+ def hincrby(key, field, increment)
410
+ node_for(key).hincrby(key, field, increment)
411
+ end
412
+
413
+ def sort(key, options = {})
414
+ keys = [key, options[:by], options[:store], *Array(options[:get])].compact
415
+
416
+ ensure_same_node(:sort, *keys) do |node|
417
+ node.sort(key, options)
418
+ end
419
+ end
420
+
421
+ def multi(&block)
422
+ raise CannotDistribute, :multi
423
+ end
424
+
425
+ def watch(*keys)
426
+ raise CannotDistribute, :watch
427
+ end
428
+
429
+ def unwatch
430
+ raise CannotDistribute, :unwatch
431
+ end
432
+
433
+ def exec
434
+ raise CannotDistribute, :exec
435
+ end
436
+
437
+ def discard
438
+ raise CannotDistribute, :discard
439
+ end
440
+
441
+ def publish(channel, message)
442
+ node_for(channel).publish(channel, message)
443
+ end
444
+
445
+ def subscribed?
446
+ !! @subscribed_node
447
+ end
448
+
449
+ def unsubscribe(*channels)
450
+ raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
451
+ @subscribed_node.unsubscribe(*channels)
452
+ end
453
+
454
+ def subscribe(channel, *channels, &block)
455
+ if channels.empty?
456
+ @subscribed_node = node_for(channel)
457
+ @subscribed_node.subscribe(channel, &block)
458
+ else
459
+ ensure_same_node(:subscribe, channel, *channels) do |node|
460
+ @subscribed_node = node
461
+ node.subscribe(channel, *channels, &block)
462
+ end
463
+ end
464
+ end
465
+
466
+ def punsubscribe(*channels)
467
+ raise NotImplementedError
468
+ end
469
+
470
+ def psubscribe(*channels, &block)
471
+ raise NotImplementedError
472
+ end
473
+
474
+ def save
475
+ on_each_node :save
476
+ end
477
+
478
+ def bgsave
479
+ on_each_node :bgsave
480
+ end
481
+
482
+ def lastsave
483
+ on_each_node :lastsave
484
+ end
485
+
486
+ def info
487
+ on_each_node :info
488
+ end
489
+
490
+ def monitor
491
+ raise NotImplementedError
492
+ end
493
+
494
+ def echo(value)
495
+ on_each_node :echo, value
496
+ end
497
+
498
+ def pipelined
499
+ raise CannotDistribute, :pipelined
500
+ end
501
+
502
+ protected
503
+
504
+ def on_each_node(command, *args)
505
+ nodes.map do |node|
506
+ node.send(command, *args)
507
+ end
508
+ end
509
+
510
+ def node_index_for(key)
511
+ nodes.index(node_for(key))
512
+ end
513
+
514
+ def key_tag(key)
515
+ key[@tag, 1] if @tag
516
+ end
517
+
518
+ def ensure_same_node(command, *keys)
519
+ tags = keys.map { |key| key_tag(key) }
520
+
521
+ raise CannotDistribute, command if !tags.all? || tags.uniq.size != 1
522
+
523
+ yield(node_for(keys.first))
524
+ end
525
+ end
526
+ end