beetle 0.1 → 0.2.0
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.
- data/README.rdoc +18 -8
- data/beetle.gemspec +37 -121
- data/bin/beetle +9 -0
- data/examples/README.rdoc +0 -2
- data/examples/rpc.rb +3 -2
- data/ext/mkrf_conf.rb +19 -0
- data/lib/beetle/base.rb +1 -8
- data/lib/beetle/client.rb +16 -14
- data/lib/beetle/commands/configuration_client.rb +73 -0
- data/lib/beetle/commands/configuration_server.rb +85 -0
- data/lib/beetle/commands.rb +30 -0
- data/lib/beetle/configuration.rb +70 -7
- data/lib/beetle/deduplication_store.rb +50 -38
- data/lib/beetle/handler.rb +2 -5
- data/lib/beetle/logging.rb +7 -0
- data/lib/beetle/message.rb +11 -13
- data/lib/beetle/publisher.rb +2 -2
- data/lib/beetle/r_c.rb +2 -1
- data/lib/beetle/redis_configuration_client.rb +136 -0
- data/lib/beetle/redis_configuration_server.rb +301 -0
- data/lib/beetle/redis_ext.rb +79 -0
- data/lib/beetle/redis_master_file.rb +35 -0
- data/lib/beetle/redis_server_info.rb +65 -0
- data/lib/beetle/subscriber.rb +4 -1
- data/lib/beetle.rb +2 -2
- data/test/beetle/configuration_test.rb +14 -2
- data/test/beetle/deduplication_store_test.rb +61 -43
- data/test/beetle/message_test.rb +28 -4
- data/test/beetle/redis_configuration_client_test.rb +97 -0
- data/test/beetle/redis_configuration_server_test.rb +278 -0
- data/test/beetle/redis_ext_test.rb +71 -0
- data/test/beetle/redis_master_file_test.rb +39 -0
- data/test/test_helper.rb +13 -1
- metadata +59 -50
- data/.gitignore +0 -5
- data/MIT-LICENSE +0 -20
- data/Rakefile +0 -114
- data/TODO +0 -7
- data/doc/redundant_queues.graffle +0 -7744
- data/etc/redis-master.conf +0 -189
- data/etc/redis-slave.conf +0 -189
- data/examples/redis_failover.rb +0 -65
- data/script/start_rabbit +0 -29
- data/snafu.rb +0 -55
- data/test/beetle/bla.rb +0 -0
- data/test/beetle.yml +0 -81
- data/tmp/master/.gitignore +0 -2
- data/tmp/slave/.gitignore +0 -3
@@ -0,0 +1,301 @@
|
|
1
|
+
module Beetle
|
2
|
+
# A RedisConfigurationServer is the supervisor part of beetle's
|
3
|
+
# redis failover solution
|
4
|
+
#
|
5
|
+
# An single instance of RedisConfigurationServer works as a supervisor for
|
6
|
+
# several RedisConfigurationClient instances. It is responsible for watching
|
7
|
+
# the redis master and electing and publishing a new master in case of failure.
|
8
|
+
#
|
9
|
+
# It will make sure that all configured RedisConfigurationClient instances
|
10
|
+
# do not use the old master anymore before making a switch, to prevent
|
11
|
+
# inconsistent data.
|
12
|
+
#
|
13
|
+
# Usually started via <tt>beetle configuration_server</tt> command.
|
14
|
+
class RedisConfigurationServer
|
15
|
+
include Logging
|
16
|
+
include RedisMasterFile
|
17
|
+
|
18
|
+
# the current redis master
|
19
|
+
attr_reader :current_master
|
20
|
+
|
21
|
+
# the current token used to detect correct message order
|
22
|
+
attr_reader :current_token
|
23
|
+
|
24
|
+
def initialize #:nodoc:
|
25
|
+
@client_ids = Set.new(config.redis_configuration_client_ids.split(","))
|
26
|
+
@current_token = (Time.now.to_f * 1000).to_i
|
27
|
+
@client_pong_ids_received = Set.new
|
28
|
+
@client_invalidated_ids_received = Set.new
|
29
|
+
MessageDispatcher.configuration_server = self
|
30
|
+
end
|
31
|
+
|
32
|
+
# Redis system status information (an instance of class RedisServerInfo)
|
33
|
+
def redis
|
34
|
+
@redis ||= RedisServerInfo.new(config, :timeout => 3)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Beetle::Client instance for communication with the RedisConfigurationServer
|
38
|
+
def beetle
|
39
|
+
@beetle ||= build_beetle
|
40
|
+
end
|
41
|
+
|
42
|
+
def config #:nodoc:
|
43
|
+
beetle.config
|
44
|
+
end
|
45
|
+
|
46
|
+
# start watching redis
|
47
|
+
def start
|
48
|
+
verify_redis_master_file_string
|
49
|
+
check_redis_configuration
|
50
|
+
redis.refresh
|
51
|
+
determine_initial_master
|
52
|
+
log_start
|
53
|
+
beetle.listen do
|
54
|
+
master_watcher.watch
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# test if redis is currently being watched
|
59
|
+
def paused?
|
60
|
+
master_watcher.paused?
|
61
|
+
end
|
62
|
+
|
63
|
+
# called by the message dispatcher when a "pong" message from a RedisConfigurationClient is received
|
64
|
+
def pong(payload)
|
65
|
+
id = payload["id"]
|
66
|
+
token = payload["token"]
|
67
|
+
logger.info "Received pong message from id '#{id}' with token '#{token}'"
|
68
|
+
return unless redeem_token(token)
|
69
|
+
@client_pong_ids_received << id
|
70
|
+
if all_client_pong_ids_received?
|
71
|
+
logger.debug "All client pong messages received"
|
72
|
+
@available_timer.cancel if @available_timer
|
73
|
+
invalidate_current_master
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# called by the message dispatcher when a "client_invalidated" message from a RedisConfigurationClient is received
|
78
|
+
def client_invalidated(payload)
|
79
|
+
id = payload["id"]
|
80
|
+
token = payload["token"]
|
81
|
+
logger.info "Received client_invalidated message from id '#{id}' with token '#{token}'"
|
82
|
+
return unless redeem_token(token)
|
83
|
+
@client_invalidated_ids_received << id
|
84
|
+
if all_client_invalidated_ids_received?
|
85
|
+
logger.debug "All client invalidated messages received"
|
86
|
+
@invalidate_timer.cancel if @invalidate_timer
|
87
|
+
switch_master
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# called from RedisWatcher when watched redis becomes unavailable
|
92
|
+
def master_unavailable!
|
93
|
+
msg = "Redis master '#{current_master.server}' not available"
|
94
|
+
master_watcher.pause
|
95
|
+
logger.warn(msg)
|
96
|
+
beetle.publish(:system_notification, {"message" => msg}.to_json)
|
97
|
+
|
98
|
+
if @client_ids.empty?
|
99
|
+
switch_master
|
100
|
+
else
|
101
|
+
start_invalidation
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# called from RedisWatcher when watched redis is available
|
106
|
+
def master_available!
|
107
|
+
publish_master(current_master)
|
108
|
+
configure_slaves(current_master)
|
109
|
+
end
|
110
|
+
|
111
|
+
# check whether the current master is still online and still a master
|
112
|
+
def master_available?
|
113
|
+
redis.masters.include?(current_master)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def check_redis_configuration
|
119
|
+
raise ConfigurationError.new("Redis failover needs two or more redis servers") if redis.instances.size < 2
|
120
|
+
end
|
121
|
+
|
122
|
+
def log_start
|
123
|
+
logger.info "RedisConfigurationServer starting"
|
124
|
+
logger.info "AMQP servers : #{config.servers}"
|
125
|
+
logger.info "Client ids : #{config.redis_configuration_client_ids}"
|
126
|
+
logger.info "Redis servers : #{config.redis_servers}"
|
127
|
+
logger.info "Redis master : #{current_master.server}"
|
128
|
+
end
|
129
|
+
|
130
|
+
def build_beetle
|
131
|
+
system = Beetle.config.system_name
|
132
|
+
Beetle::Client.new.configure :exchange => system, :auto_delete => true do |config|
|
133
|
+
config.message :client_invalidated
|
134
|
+
config.queue :client_invalidated, :amqp_name => "#{system}_client_invalidated"
|
135
|
+
config.message :pong
|
136
|
+
config.queue :pong, :amqp_name => "#{system}_pong"
|
137
|
+
config.message :ping
|
138
|
+
config.message :invalidate
|
139
|
+
config.message :reconfigure
|
140
|
+
config.message :system_notification
|
141
|
+
|
142
|
+
config.handler [:pong, :client_invalidated], MessageDispatcher
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def master_watcher
|
147
|
+
@master_watcher ||= RedisWatcher.new(self)
|
148
|
+
end
|
149
|
+
|
150
|
+
def determine_initial_master
|
151
|
+
if master_file_exists? && @current_master = redis_master_from_master_file
|
152
|
+
if redis.slaves.include?(current_master)
|
153
|
+
master_unavailable!
|
154
|
+
elsif redis.unknowns.include?(current_master)
|
155
|
+
master_unavailable!
|
156
|
+
end
|
157
|
+
else
|
158
|
+
write_redis_master_file(current_master.server) if @current_master = redis.auto_detect_master
|
159
|
+
end
|
160
|
+
current_master or raise NoRedisMaster.new("failed to determine initial redis master")
|
161
|
+
end
|
162
|
+
|
163
|
+
def determine_new_master
|
164
|
+
redis.unknowns.include?(current_master) ? redis.slaves_of(current_master).first : current_master
|
165
|
+
end
|
166
|
+
|
167
|
+
def redeem_token(token)
|
168
|
+
valid_token = token == @current_token
|
169
|
+
logger.info "Ignored message (token was '#{token.inspect}', but expected '#{@current_token.inspect}')" unless valid_token
|
170
|
+
valid_token
|
171
|
+
end
|
172
|
+
|
173
|
+
def start_invalidation
|
174
|
+
@client_pong_ids_received.clear
|
175
|
+
@client_invalidated_ids_received.clear
|
176
|
+
check_all_clients_available
|
177
|
+
end
|
178
|
+
|
179
|
+
def check_all_clients_available
|
180
|
+
generate_new_token
|
181
|
+
beetle.publish(:ping, payload_with_current_token)
|
182
|
+
@available_timer = EM::Timer.new(config.redis_configuration_client_timeout) { cancel_invalidation }
|
183
|
+
end
|
184
|
+
|
185
|
+
def invalidate_current_master
|
186
|
+
generate_new_token
|
187
|
+
beetle.publish(:invalidate, payload_with_current_token)
|
188
|
+
@invalidate_timer = EM::Timer.new(config.redis_configuration_client_timeout) { cancel_invalidation }
|
189
|
+
end
|
190
|
+
|
191
|
+
def cancel_invalidation
|
192
|
+
logger.warn "Redis master invalidation cancelled: 'pong' received from '#{@client_pong_ids_received.to_a.join(',')}', 'client_invalidated' received from '#{@client_invalidated_ids_received.to_a.join(',')}'"
|
193
|
+
generate_new_token
|
194
|
+
master_watcher.continue
|
195
|
+
end
|
196
|
+
|
197
|
+
def generate_new_token
|
198
|
+
@current_token += 1
|
199
|
+
end
|
200
|
+
|
201
|
+
def payload_with_current_token(message = {})
|
202
|
+
message["token"] = @current_token
|
203
|
+
message.to_json
|
204
|
+
end
|
205
|
+
|
206
|
+
def all_client_pong_ids_received?
|
207
|
+
@client_ids == @client_pong_ids_received
|
208
|
+
end
|
209
|
+
|
210
|
+
def all_client_invalidated_ids_received?
|
211
|
+
@client_ids == @client_invalidated_ids_received
|
212
|
+
end
|
213
|
+
|
214
|
+
def switch_master
|
215
|
+
if new_master = determine_new_master
|
216
|
+
msg = "Setting redis master to '#{new_master.server}' (was '#{current_master.server}')"
|
217
|
+
logger.warn(msg)
|
218
|
+
beetle.publish(:system_notification, {"message" => msg}.to_json)
|
219
|
+
|
220
|
+
new_master.master!
|
221
|
+
@current_master = new_master
|
222
|
+
else
|
223
|
+
msg = "Redis master could not be switched, no slave available to become new master, promoting old master"
|
224
|
+
logger.error(msg)
|
225
|
+
beetle.publish(:system_notification, {"message" => msg}.to_json)
|
226
|
+
end
|
227
|
+
|
228
|
+
publish_master(current_master)
|
229
|
+
master_watcher.continue
|
230
|
+
end
|
231
|
+
|
232
|
+
def publish_master(master)
|
233
|
+
logger.info "Publishing reconfigure message with server '#{master.server}'"
|
234
|
+
beetle.publish(:reconfigure, payload_with_current_token({"server" => master.server}))
|
235
|
+
end
|
236
|
+
|
237
|
+
def configure_slaves(master)
|
238
|
+
(redis.masters-[master]).each do |r|
|
239
|
+
logger.info "Reconfiguring '#{r.server}' as a slave of '#{master.server}'"
|
240
|
+
r.slave_of!(master.host, master.port)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Periodically checks a redis server for availability
|
245
|
+
class RedisWatcher #:nodoc:
|
246
|
+
include Logging
|
247
|
+
|
248
|
+
def initialize(configuration_server)
|
249
|
+
@configuration_server = configuration_server
|
250
|
+
@retries = 0
|
251
|
+
@paused = true
|
252
|
+
@master_retry_interval = configuration_server.config.redis_configuration_master_retry_interval
|
253
|
+
@master_retries = configuration_server.config.redis_configuration_master_retries
|
254
|
+
end
|
255
|
+
|
256
|
+
def pause
|
257
|
+
logger.info "Pause checking availability of redis servers"
|
258
|
+
@watch_timer.cancel if @watch_timer
|
259
|
+
@watch_timer = nil
|
260
|
+
@paused = true
|
261
|
+
end
|
262
|
+
|
263
|
+
def watch
|
264
|
+
@watch_timer ||=
|
265
|
+
begin
|
266
|
+
logger.info "Start watching redis servers every #{@master_retry_interval} seconds"
|
267
|
+
EventMachine::add_periodic_timer(@master_retry_interval) { check_availability }
|
268
|
+
end
|
269
|
+
@paused = false
|
270
|
+
end
|
271
|
+
alias continue watch
|
272
|
+
|
273
|
+
def paused?
|
274
|
+
@paused
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
|
279
|
+
def check_availability
|
280
|
+
@configuration_server.redis.refresh
|
281
|
+
if @configuration_server.master_available?
|
282
|
+
@configuration_server.master_available!
|
283
|
+
else
|
284
|
+
logger.warn "Redis master not available! (Retries left: #{@master_retries - (@retries + 1)})"
|
285
|
+
if (@retries+=1) >= @master_retries
|
286
|
+
@retries = 0
|
287
|
+
@configuration_server.master_unavailable!
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Dispatches messages from the queue to methods in RedisConfigurationServer
|
294
|
+
class MessageDispatcher < Beetle::Handler #:nodoc:
|
295
|
+
cattr_accessor :configuration_server
|
296
|
+
def process
|
297
|
+
@@configuration_server.__send__(message.header.routing_key, ActiveSupport::JSON.decode(message.data))
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Redis convenience and compatibility layer
|
2
|
+
class Redis #:nodoc:
|
3
|
+
def self.from_server_string(server_string, options = {})
|
4
|
+
host, port = server_string.split(':')
|
5
|
+
options = {:host => host, :port => port}.update(options)
|
6
|
+
new(options)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Redis 2 removed some useful methods. add them back.
|
10
|
+
def host; @client.host; end
|
11
|
+
def port; @client.port; end
|
12
|
+
def server; "#{host}:#{port}"; end
|
13
|
+
|
14
|
+
def master!
|
15
|
+
slaveof("no", "one")
|
16
|
+
end
|
17
|
+
|
18
|
+
def slave_of!(host, port)
|
19
|
+
slaveof(host, port)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Redis 2 tries to establish a connection on inspect. this is evil!
|
23
|
+
def inspect
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def info_with_rescue
|
28
|
+
info
|
29
|
+
rescue Exception
|
30
|
+
{}
|
31
|
+
end
|
32
|
+
|
33
|
+
def available?
|
34
|
+
info_with_rescue != {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def role
|
38
|
+
info_with_rescue["role"] || "unknown"
|
39
|
+
end
|
40
|
+
|
41
|
+
def master?
|
42
|
+
role == "master"
|
43
|
+
end
|
44
|
+
|
45
|
+
def slave?
|
46
|
+
role == "slave"
|
47
|
+
end
|
48
|
+
|
49
|
+
def slave_of?(host, port)
|
50
|
+
info = info_with_rescue
|
51
|
+
info["role"] == "slave" && info["master_host"] == host && info["master_port"] == port.to_s
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Redis::Client #:nodoc:
|
56
|
+
protected
|
57
|
+
def connect_to(host, port)
|
58
|
+
if @timeout != 0 and Redis::Timer
|
59
|
+
begin
|
60
|
+
Redis::Timer.timeout(@timeout){ @sock = TCPSocket.new(host, port) }
|
61
|
+
rescue Timeout::Error
|
62
|
+
@sock = nil
|
63
|
+
raise Timeout::Error, "Timeout connecting to the server"
|
64
|
+
end
|
65
|
+
else
|
66
|
+
@sock = TCPSocket.new(host, port)
|
67
|
+
end
|
68
|
+
|
69
|
+
@sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
70
|
+
|
71
|
+
# If the timeout is set we set the low level socket options in order
|
72
|
+
# to make sure a blocking read will return after the specified number
|
73
|
+
# of seconds. This hack is from memcached ruby client.
|
74
|
+
self.timeout = @timeout
|
75
|
+
|
76
|
+
rescue Errno::ECONNREFUSED
|
77
|
+
raise Errno::ECONNREFUSED, "Unable to connect to Redis on #{host}:#{port}"
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Beetle
|
2
|
+
module RedisMasterFile #:nodoc:
|
3
|
+
private
|
4
|
+
def master_file_exists?
|
5
|
+
File.exist?(master_file)
|
6
|
+
end
|
7
|
+
|
8
|
+
def redis_master_from_master_file
|
9
|
+
redis.find(read_redis_master_file)
|
10
|
+
end
|
11
|
+
|
12
|
+
def clear_redis_master_file
|
13
|
+
write_redis_master_file("")
|
14
|
+
end
|
15
|
+
|
16
|
+
def read_redis_master_file
|
17
|
+
File.read(master_file).chomp
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_redis_master_file(redis_server_string)
|
21
|
+
logger.warn "Writing '#{redis_server_string}' to redis master file '#{master_file}'"
|
22
|
+
File.open(master_file, "w"){|f| f.puts redis_server_string }
|
23
|
+
end
|
24
|
+
|
25
|
+
def master_file
|
26
|
+
config.redis_server
|
27
|
+
end
|
28
|
+
|
29
|
+
def verify_redis_master_file_string
|
30
|
+
if master_file =~ /^[0-9a-z.]+:[0-9]+$/
|
31
|
+
raise ConfigurationError.new("To use the redis failover, redis_server config option must point to a file")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Beetle
|
2
|
+
# class used by the RedisConfigurationServer to hold information about the current state of the configured redis servers
|
3
|
+
class RedisServerInfo
|
4
|
+
include Logging
|
5
|
+
|
6
|
+
def initialize(config, options) #:nodoc:
|
7
|
+
@config = config
|
8
|
+
@options = options
|
9
|
+
reset
|
10
|
+
end
|
11
|
+
|
12
|
+
# all configured redis servers
|
13
|
+
def instances
|
14
|
+
@instances ||= @config.redis_servers.split(/ *, */).map{|s| Redis.from_server_string(s, @options)}
|
15
|
+
end
|
16
|
+
|
17
|
+
# fetches the server from the insatnces whith the given <tt>server</tt> string
|
18
|
+
def find(server)
|
19
|
+
instances.find{|r| r.server == server}
|
20
|
+
end
|
21
|
+
|
22
|
+
# refresh connectivity/role information
|
23
|
+
def refresh
|
24
|
+
logger.debug "Updating redis server info"
|
25
|
+
reset
|
26
|
+
instances.each {|r| @server_info[r.role] << r}
|
27
|
+
end
|
28
|
+
|
29
|
+
# subset of instances which are masters
|
30
|
+
def masters
|
31
|
+
@server_info["master"]
|
32
|
+
end
|
33
|
+
|
34
|
+
# subset of instances which are slaves
|
35
|
+
def slaves
|
36
|
+
@server_info["slave"]
|
37
|
+
end
|
38
|
+
|
39
|
+
# subset of instances which are not reachable
|
40
|
+
def unknowns
|
41
|
+
@server_info["unknown"]
|
42
|
+
end
|
43
|
+
|
44
|
+
# subset of instances which are set up as slaves of the given <tt>master</tt>
|
45
|
+
def slaves_of(master)
|
46
|
+
slaves.select{|r| r.slave_of?(master.host, master.port)}
|
47
|
+
end
|
48
|
+
|
49
|
+
# determine a master if we have one master and all other servers are slaves
|
50
|
+
def auto_detect_master
|
51
|
+
return nil unless master_and_slaves_reachable?
|
52
|
+
masters.first
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def master_and_slaves_reachable?
|
58
|
+
masters.size == 1 && slaves.size == instances.size - 1
|
59
|
+
end
|
60
|
+
|
61
|
+
def reset
|
62
|
+
@server_info = Hash.new {|h,k| h[k]= []}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/beetle/subscriber.rb
CHANGED
@@ -10,7 +10,7 @@ module Beetle
|
|
10
10
|
@mqs = {}
|
11
11
|
end
|
12
12
|
|
13
|
-
# the client calls this method to
|
13
|
+
# the client calls this method to subscribe to all queues on all servers which have
|
14
14
|
# handlers registered for the given list of messages. this method does the following
|
15
15
|
# things:
|
16
16
|
#
|
@@ -105,6 +105,9 @@ module Beetle
|
|
105
105
|
sleep 1
|
106
106
|
mq(server).recover
|
107
107
|
elsif reply_to = header.properties[:reply_to]
|
108
|
+
# require 'ruby-debug'
|
109
|
+
# Debugger.start
|
110
|
+
# debugger
|
108
111
|
status = result == Beetle::RC::OK ? "OK" : "FAILED"
|
109
112
|
exchange = MQ::Exchange.new(mq(server), :direct, "", :key => reply_to)
|
110
113
|
exchange.publish(m.handler_result.to_s, :headers => {:status => status})
|
data/lib/beetle.rb
CHANGED
@@ -17,8 +17,6 @@ module Beetle
|
|
17
17
|
class UnknownQueue < Error; end
|
18
18
|
# raised when no redis master server can be found
|
19
19
|
class NoRedisMaster < Error; end
|
20
|
-
# raised when two redis master servers are found
|
21
|
-
class TwoRedisMasters < Error; end
|
22
20
|
|
23
21
|
# AMQP options for exchange creation
|
24
22
|
EXCHANGE_CREATION_KEYS = [:auto_delete, :durable, :internal, :nowait, :passive]
|
@@ -37,6 +35,8 @@ module Beetle
|
|
37
35
|
autoload File.basename(libfile)[/^(.*)\.rb$/, 1].classify, libfile
|
38
36
|
end
|
39
37
|
|
38
|
+
require "#{lib_dir}/redis_ext"
|
39
|
+
|
40
40
|
# returns the default configuration object and yields it if a block is given
|
41
41
|
def self.config
|
42
42
|
#:yields: config
|
@@ -1,5 +1,17 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
|
-
|
2
|
+
require 'tempfile'
|
3
3
|
|
4
4
|
module Beetle
|
5
|
-
|
5
|
+
class ConfigurationTest < Test::Unit::TestCase
|
6
|
+
test "should load it's settings from a config file if that file exists" do
|
7
|
+
config = Configuration.new
|
8
|
+
old_value = config.gc_threshold
|
9
|
+
new_value = old_value + 1
|
10
|
+
config_file_content = "gc_threshold: #{new_value}\n"
|
11
|
+
IO.expects(:read).returns(config_file_content)
|
12
|
+
|
13
|
+
config.config_file = "some/path/to/a/file"
|
14
|
+
assert_equal new_value, config.gc_threshold
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -14,75 +14,93 @@ module Beetle
|
|
14
14
|
end
|
15
15
|
|
16
16
|
test "msetnx returns 0 or 1" do
|
17
|
-
assert_equal 1, @r.msetnx("a"
|
17
|
+
assert_equal 1, @r.msetnx("a", 1, "b", 2)
|
18
18
|
assert_equal "1", @r.get("a")
|
19
19
|
assert_equal "2", @r.get("b")
|
20
|
-
assert_equal 0, @r.msetnx("a"
|
20
|
+
assert_equal 0, @r.msetnx("a", 3, "b", 4)
|
21
21
|
assert_equal "1", @r.get("a")
|
22
22
|
assert_equal "2", @r.get("b")
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
class
|
26
|
+
class RedisServerStringTest < Test::Unit::TestCase
|
27
|
+
def setup
|
28
|
+
@original_redis_server = Beetle.config.redis_server
|
29
|
+
@store = DeduplicationStore.new
|
30
|
+
@server_string = "my_test_host_from_file:9999"
|
31
|
+
Beetle.config.redis_server = @server_string
|
32
|
+
end
|
33
|
+
|
34
|
+
def teardown
|
35
|
+
Beetle.config.redis_server = @original_redis_server
|
36
|
+
end
|
37
|
+
|
38
|
+
test "redis should match the redis server string" do
|
39
|
+
assert_equal @server_string, @store.redis.server
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class RedisServerFileTest < Test::Unit::TestCase
|
27
44
|
def setup
|
28
|
-
@
|
45
|
+
@original_redis_server = Beetle.config.redis_server
|
46
|
+
@store = DeduplicationStore.new
|
47
|
+
@server_string = "my_test_host_from_file:6379"
|
48
|
+
Beetle.config.redis_server = redis_test_master_file(@server_string)
|
49
|
+
end
|
50
|
+
|
51
|
+
def teardown
|
52
|
+
Beetle.config.redis_server = @original_redis_server
|
53
|
+
end
|
54
|
+
|
55
|
+
test "redis should match the redis master file" do
|
56
|
+
assert_equal @server_string, @store.redis.server
|
29
57
|
end
|
30
58
|
|
31
|
-
test "redis
|
32
|
-
|
33
|
-
|
59
|
+
test "redis should be nil if the redis master file is blank" do
|
60
|
+
redis_test_master_file("")
|
61
|
+
assert_nil @store.redis
|
34
62
|
end
|
35
63
|
|
36
|
-
test "
|
37
|
-
|
38
|
-
|
39
|
-
instances.second.expects(:info).returns(:role => "master")
|
40
|
-
assert_equal instances.second, @store.redis
|
64
|
+
test "should keep using the current redis if the redis master file hasn't changed since the last request" do
|
65
|
+
@store.expects(:read_master_file).once.returns("localhost:1")
|
66
|
+
2.times { @store.redis }
|
41
67
|
end
|
42
68
|
|
43
|
-
test "
|
44
|
-
|
45
|
-
|
46
|
-
instances.second.expects(:info).returns(:role => "master")
|
47
|
-
assert_equal instances.second, @store.redis
|
69
|
+
test "should return nil if the master file doesn't exist" do
|
70
|
+
Beetle.config.redis_server = "/tmp/__i_don_not_exist__.txt"
|
71
|
+
assert_equal nil, @store.redis_master_from_master_file
|
48
72
|
end
|
49
73
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
74
|
+
private
|
75
|
+
def redis_test_master_file(server_string)
|
76
|
+
path = File.expand_path("../../../tmp/redis-master-for-unit-tests", __FILE__)
|
77
|
+
File.open(path, "w"){|f| f.puts server_string}
|
78
|
+
path
|
55
79
|
end
|
80
|
+
end
|
56
81
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
assert_raises(TwoRedisMasters) { @store.find_redis_master }
|
82
|
+
class RedisFailoverTest < Test::Unit::TestCase
|
83
|
+
def setup
|
84
|
+
@store = DeduplicationStore.new
|
85
|
+
Beetle.config.expects(:redis_failover_timeout).returns(1)
|
62
86
|
end
|
63
87
|
|
64
88
|
test "a redis operation protected with a redis failover block should succeed if it can find a new master" do
|
65
|
-
|
89
|
+
redis1 = stub("redis 1")
|
90
|
+
redis2 = stub("redis 2")
|
66
91
|
s = sequence("redis accesses")
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
instances.first.expects(:info).raises("disconnected").in_sequence(s)
|
72
|
-
instances.second.expects(:info).returns(:role => "master").in_sequence(s)
|
73
|
-
instances.second.expects(:get).with("foo:x").returns("42").in_sequence(s)
|
92
|
+
@store.expects(:redis).returns(redis1).in_sequence(s)
|
93
|
+
redis1.expects(:get).with("foo:x").raises("disconnected").in_sequence(s)
|
94
|
+
@store.expects(:redis).returns(redis2).in_sequence(s)
|
95
|
+
redis2.expects(:get).with("foo:x").returns("42").in_sequence(s)
|
74
96
|
assert_equal("42", @store.get("foo", "x"))
|
75
97
|
end
|
76
98
|
|
77
99
|
test "a redis operation protected with a redis failover block should fail if it cannot find a new master" do
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
instances.first.stubs(:get).with("foo:x").raises("disconnected")
|
83
|
-
instances.first.stubs(:info).raises("disconnected")
|
84
|
-
instances.second.stubs(:info).returns(:role => "slave")
|
85
|
-
@store.expects(:sleep).times(119)
|
100
|
+
redis1 = stub()
|
101
|
+
@store.stubs(:redis).returns(redis1)
|
102
|
+
redis1.stubs(:get).with("foo:x").raises("disconnected")
|
103
|
+
@store.stubs(:sleep)
|
86
104
|
assert_raises(NoRedisMaster) { @store.get("foo", "x") }
|
87
105
|
end
|
88
106
|
end
|