beetle 0.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/README.rdoc +18 -8
  2. data/beetle.gemspec +37 -121
  3. data/bin/beetle +9 -0
  4. data/examples/README.rdoc +0 -2
  5. data/examples/rpc.rb +3 -2
  6. data/ext/mkrf_conf.rb +19 -0
  7. data/lib/beetle.rb +2 -2
  8. data/lib/beetle/base.rb +1 -8
  9. data/lib/beetle/client.rb +16 -14
  10. data/lib/beetle/commands.rb +30 -0
  11. data/lib/beetle/commands/configuration_client.rb +73 -0
  12. data/lib/beetle/commands/configuration_server.rb +85 -0
  13. data/lib/beetle/configuration.rb +70 -7
  14. data/lib/beetle/deduplication_store.rb +50 -38
  15. data/lib/beetle/handler.rb +2 -5
  16. data/lib/beetle/logging.rb +7 -0
  17. data/lib/beetle/message.rb +11 -13
  18. data/lib/beetle/publisher.rb +12 -4
  19. data/lib/beetle/r_c.rb +2 -1
  20. data/lib/beetle/redis_configuration_client.rb +136 -0
  21. data/lib/beetle/redis_configuration_server.rb +301 -0
  22. data/lib/beetle/redis_ext.rb +79 -0
  23. data/lib/beetle/redis_master_file.rb +35 -0
  24. data/lib/beetle/redis_server_info.rb +65 -0
  25. data/lib/beetle/subscriber.rb +4 -1
  26. data/test/beetle/configuration_test.rb +14 -2
  27. data/test/beetle/deduplication_store_test.rb +61 -43
  28. data/test/beetle/message_test.rb +28 -4
  29. data/test/beetle/publisher_test.rb +17 -3
  30. data/test/beetle/redis_configuration_client_test.rb +97 -0
  31. data/test/beetle/redis_configuration_server_test.rb +278 -0
  32. data/test/beetle/redis_ext_test.rb +71 -0
  33. data/test/beetle/redis_master_file_test.rb +39 -0
  34. data/test/test_helper.rb +13 -1
  35. metadata +162 -69
  36. data/.gitignore +0 -5
  37. data/MIT-LICENSE +0 -20
  38. data/Rakefile +0 -114
  39. data/TODO +0 -7
  40. data/etc/redis-master.conf +0 -189
  41. data/etc/redis-slave.conf +0 -189
  42. data/examples/redis_failover.rb +0 -65
  43. data/script/start_rabbit +0 -29
  44. data/snafu.rb +0 -55
  45. data/test/beetle.yml +0 -81
  46. data/test/beetle/bla.rb +0 -0
  47. data/tmp/master/.gitignore +0 -2
  48. 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, :failure
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