beetle 0.1 → 0.2.1
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.rb +2 -2
- data/lib/beetle/base.rb +1 -8
- data/lib/beetle/client.rb +16 -14
- data/lib/beetle/commands.rb +30 -0
- data/lib/beetle/commands/configuration_client.rb +73 -0
- data/lib/beetle/commands/configuration_server.rb +85 -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 +12 -4
- 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/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/publisher_test.rb +17 -3
- 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 +162 -69
- data/.gitignore +0 -5
- data/MIT-LICENSE +0 -20
- data/Rakefile +0 -114
- data/TODO +0 -7
- 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.yml +0 -81
- data/test/beetle/bla.rb +0 -0
- data/tmp/master/.gitignore +0 -2
- data/tmp/slave/.gitignore +0 -3
data/lib/beetle/r_c.rb
CHANGED
@@ -27,7 +27,7 @@ module Beetle
|
|
27
27
|
end
|
28
28
|
|
29
29
|
rc :OK
|
30
|
-
rc :Ancient
|
30
|
+
rc :Ancient
|
31
31
|
rc :AttemptsLimitReached, :failure
|
32
32
|
rc :ExceptionsLimitReached, :failure
|
33
33
|
rc :Delayed, :recover
|
@@ -35,6 +35,7 @@ module Beetle
|
|
35
35
|
rc :HandlerNotYetTimedOut, :recover
|
36
36
|
rc :MutexLocked, :recover
|
37
37
|
rc :InternalError, :recover
|
38
|
+
rc :DecodingError, :failure
|
38
39
|
|
39
40
|
end
|
40
41
|
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Beetle
|
2
|
+
# A RedisConfigurationClient is the subordinate part of beetle's
|
3
|
+
# redis failover solution
|
4
|
+
#
|
5
|
+
# An instance of RedisConfigurationClient lives on every server that
|
6
|
+
# hosts message consumers (worker server).
|
7
|
+
#
|
8
|
+
# It is responsible for determining an initial redis master and reacting to redis master
|
9
|
+
# switches initiated by the RedisConfigurationServer.
|
10
|
+
#
|
11
|
+
# It will write the current redis master host:port string to a file specified via a
|
12
|
+
# Configuration, which is then read by DeduplicationStore on redis access.
|
13
|
+
#
|
14
|
+
# Usually started via <tt>beetle configuration_client</tt> command.
|
15
|
+
class RedisConfigurationClient
|
16
|
+
include Logging
|
17
|
+
include RedisMasterFile
|
18
|
+
|
19
|
+
# Set a custom unique id for this instance. Must match an entry in
|
20
|
+
# Configuration#redis_configuration_client_ids.
|
21
|
+
attr_writer :id
|
22
|
+
|
23
|
+
# The current redis master
|
24
|
+
attr_reader :current_master
|
25
|
+
|
26
|
+
# Unique id for this instance (defaults to the hostname)
|
27
|
+
def id
|
28
|
+
@id ||= `hostname`.chomp
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize #:nodoc:
|
32
|
+
@current_token = nil
|
33
|
+
MessageDispatcher.configuration_client = self
|
34
|
+
end
|
35
|
+
|
36
|
+
# determinines the initial redis master (if possible), then enters a messaging event
|
37
|
+
# loop, reacting to failover related messages sent by RedisConfigurationServer.
|
38
|
+
def start
|
39
|
+
verify_redis_master_file_string
|
40
|
+
logger.info "RedisConfigurationClient starting (client id: #{id})"
|
41
|
+
determine_initial_master
|
42
|
+
clear_redis_master_file unless current_master.try(:master?)
|
43
|
+
logger.info "Listening"
|
44
|
+
beetle.listen
|
45
|
+
end
|
46
|
+
|
47
|
+
# called by the message dispatcher when a "pong" message from RedisConfigurationServer is received
|
48
|
+
def ping(payload)
|
49
|
+
token = payload["token"]
|
50
|
+
logger.info "Received ping message with token '#{token}'"
|
51
|
+
pong! if redeem_token(token)
|
52
|
+
end
|
53
|
+
|
54
|
+
# called by the message dispatcher when a "invalidate" message from RedisConfigurationServer is received
|
55
|
+
def invalidate(payload)
|
56
|
+
token = payload["token"]
|
57
|
+
logger.info "Received invalidate message with token '#{token}'"
|
58
|
+
invalidate! if redeem_token(token) && !current_master.try(:master?)
|
59
|
+
end
|
60
|
+
|
61
|
+
# called by the message dispatcher when a "reconfigure"" message from RedisConfigurationServer is received
|
62
|
+
def reconfigure(payload)
|
63
|
+
server = payload["server"]
|
64
|
+
token = payload["token"]
|
65
|
+
logger.info "Received reconfigure message with server '#{server}' and token '#{token}'"
|
66
|
+
return unless redeem_token(token)
|
67
|
+
unless server == read_redis_master_file
|
68
|
+
new_master!(server)
|
69
|
+
write_redis_master_file(server)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Beetle::Client instance for communication with the RedisConfigurationServer
|
74
|
+
def beetle
|
75
|
+
@beetle ||= build_beetle
|
76
|
+
end
|
77
|
+
|
78
|
+
def config #:nodoc:
|
79
|
+
beetle.config
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def determine_initial_master
|
85
|
+
if master_file_exists? && server = read_redis_master_file
|
86
|
+
new_master!(server)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def new_master!(server)
|
91
|
+
@current_master = Redis.from_server_string(server, :timeout => 3)
|
92
|
+
end
|
93
|
+
|
94
|
+
def build_beetle
|
95
|
+
system = Beetle.config.system_name
|
96
|
+
Beetle::Client.new.configure :exchange => system, :auto_delete => true do |config|
|
97
|
+
config.message :ping
|
98
|
+
config.queue :ping, :amqp_name => "#{system}_ping_#{id}"
|
99
|
+
config.message :pong
|
100
|
+
config.message :invalidate
|
101
|
+
config.queue :invalidate, :amqp_name => "#{system}_invalidate_#{id}"
|
102
|
+
config.message :client_invalidated
|
103
|
+
config.message :reconfigure
|
104
|
+
config.queue :reconfigure, :amqp_name => "#{system}_reconfigure_#{id}"
|
105
|
+
|
106
|
+
config.handler [:ping, :invalidate, :reconfigure], MessageDispatcher
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def redeem_token(token)
|
111
|
+
@current_token = token if @current_token.nil? || token > @current_token
|
112
|
+
token_valid = token >= @current_token
|
113
|
+
logger.info "Ignored message (token was '#{token}', but expected to be >= '#{@current_token}')" unless token_valid
|
114
|
+
token_valid
|
115
|
+
end
|
116
|
+
|
117
|
+
def pong!
|
118
|
+
beetle.publish(:pong, {"id" => id, "token" => @current_token}.to_json)
|
119
|
+
end
|
120
|
+
|
121
|
+
def invalidate!
|
122
|
+
@current_master = nil
|
123
|
+
clear_redis_master_file
|
124
|
+
beetle.publish(:client_invalidated, {"id" => id, "token" => @current_token}.to_json)
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
# Dispatches messages from the queue to methods in RedisConfigurationClient
|
129
|
+
class MessageDispatcher < Beetle::Handler #:nodoc:
|
130
|
+
cattr_accessor :configuration_client
|
131
|
+
def process
|
132
|
+
@@configuration_client.__send__(message.header.routing_key, ActiveSupport::JSON.decode(message.data))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -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
|