beetle 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -27,4 +27,9 @@ module Beetle
27
27
  end
28
28
  end
29
29
 
30
- Beetle::Commands.execute(ARGV.shift)
30
+ if ARGV.first.to_s.sub(/\A--/,'') == "version"
31
+ require 'beetle/version'
32
+ puts Beetle::VERSION
33
+ else
34
+ Beetle::Commands.execute(ARGV.shift)
35
+ end
@@ -27,6 +27,14 @@ module Beetle
27
27
  # handler timeout.
28
28
  attr_accessor :redis_failover_timeout
29
29
 
30
+ # how often heartbeat messages are exchanged between failover
31
+ # daemons. defaults to 10 seconds.
32
+ attr_accessor :redis_failover_client_heartbeat_interval
33
+
34
+ # how long to wait until a redis_failover client daeom can be considered
35
+ # dead. defaults to 60 seconds.
36
+ attr_accessor :redis_failover_client_dead_interval
37
+
30
38
  ## redis configuration server options
31
39
  # how often should the redis configuration server try to reach the redis master before nominating a new one (defaults to <tt>3</tt>)
32
40
  attr_accessor :redis_configuration_master_retries
@@ -99,6 +107,8 @@ module Beetle
99
107
  self.redis_servers = ""
100
108
  self.redis_db = 4
101
109
  self.redis_failover_timeout = 180.seconds
110
+ self.redis_failover_client_heartbeat_interval = 10.seconds
111
+ self.redis_failover_client_dead_interval = 60.seconds
102
112
 
103
113
  self.redis_configuration_master_retries = 3
104
114
  self.redis_configuration_master_retry_interval = 10.seconds
@@ -42,7 +42,11 @@ module Beetle
42
42
  determine_initial_master
43
43
  clear_redis_master_file unless current_master.try(:master?)
44
44
  logger.info "Listening"
45
- beetle.listen
45
+ beetle.listen do
46
+ EventMachine.add_periodic_timer(Beetle.config.redis_failover_client_heartbeat_interval) do
47
+ heartbeat!
48
+ end
49
+ end
46
50
  end
47
51
 
48
52
  # called by the message dispatcher when a "pong" message from RedisConfigurationServer is received
@@ -99,6 +103,7 @@ module Beetle
99
103
  config.message :pong
100
104
  config.message :client_started
101
105
  config.message :client_invalidated
106
+ config.message :heartbeat
102
107
  # messages received
103
108
  config.message :ping
104
109
  config.message :invalidate
@@ -128,6 +133,11 @@ module Beetle
128
133
  beetle.publish(:client_started, {"id" => id}.to_json)
129
134
  end
130
135
 
136
+ def heartbeat!
137
+ logger.info "Sending heartbeat message with id '#{id}'"
138
+ beetle.publish(:heartbeat, {"id" => id}.to_json)
139
+ end
140
+
131
141
  def invalidate!
132
142
  @current_master = nil
133
143
  clear_redis_master_file
@@ -77,7 +77,7 @@ module Beetle
77
77
  status.keys.sort_by{|k| k.to_s}.reverse.map do |k|
78
78
  name = k.to_s # .split('_').join(" ")
79
79
  if (value = status[k]).is_a?(Array)
80
- value = value.join(", ")
80
+ value = value.empty? ? "none" : value.join(", ")
81
81
  end
82
82
  "#{name}: #{value}"
83
83
  end.join("\n")
@@ -24,11 +24,23 @@ module Beetle
24
24
  # the list of known client ids
25
25
  attr_reader :client_ids
26
26
 
27
+ # the list of client ids we have received but we don't know about
28
+ attr_reader :unknown_client_ids
29
+
30
+ # clients last seen time stamps
31
+ attr_reader :clients_last_seen
32
+
33
+ # clients are presumed dead after this many seconds
34
+ attr_accessor :client_dead_threshold
35
+
27
36
  def initialize #:nodoc:
28
37
  @client_ids = Set.new(config.redis_configuration_client_ids.split(","))
38
+ @unknown_client_ids = Set.new
39
+ @clients_last_seen = {}
29
40
  @current_token = (Time.now.to_f * 1000).to_i
30
41
  @client_pong_ids_received = Set.new
31
42
  @client_invalidated_ids_received = Set.new
43
+ @client_dead_threshold = config.redis_failover_client_dead_interval
32
44
  MessageDispatcher.configuration_server = self
33
45
  end
34
46
 
@@ -57,9 +69,20 @@ module Beetle
57
69
  :redis_master_available? => master_available?,
58
70
  :redis_slaves_available => available_slaves.map(&:server),
59
71
  :switch_in_progress => paused?,
72
+ :unknown_client_ids => unknown_client_ids.to_a,
73
+ :unresponsive_clients => unresponsive_clients.map{|c,t| "#{c}:#{t.to_i}"},
60
74
  }
61
75
  end
62
76
 
77
+ # returns an array of [client_id, silent time in seconds] which haven't sent
78
+ # a message for more than <tt>client_dead_threshold</tt> seconds, sorted by
79
+ # silent time in descending order.
80
+ def unresponsive_clients
81
+ now = Time.now
82
+ threshold = now - client_dead_threshold
83
+ clients_last_seen.select{|_,t| t < threshold}.map{|c,t| [c, now - t]}.sort_by{|_,t| t}.reverse
84
+ end
85
+
63
86
  # start watching redis
64
87
  def start
65
88
  verify_redis_master_file_string
@@ -80,6 +103,7 @@ module Beetle
80
103
  # called by the message dispatcher when a "pong" message from a RedisConfigurationClient is received
81
104
  def pong(payload)
82
105
  id = payload["id"]
106
+ client_seen(id)
83
107
  token = payload["token"]
84
108
  return unless validate_pong_client_id(id)
85
109
  logger.info "Received pong message from id '#{id}' with token '#{token}'"
@@ -95,19 +119,37 @@ module Beetle
95
119
  # called by the message dispatcher when a "client_started" message from a RedisConfigurationClient is received
96
120
  def client_started(payload)
97
121
  id = payload["id"]
122
+ client_seen(id)
98
123
  if client_id_valid?(id)
99
124
  logger.info "Received client_started message from id '#{id}'"
100
125
  else
126
+ add_unknown_client_id(id)
101
127
  msg = "Received client_started message from unknown id '#{id}'"
102
128
  logger.error msg
103
129
  beetle.publish(:system_notification, {"message" => msg}.to_json)
104
130
  end
105
131
  end
106
132
 
133
+ # called by the message dispatcher when a "heartbeat" message from a RedisConfigurationClient is received
134
+ def heartbeat(payload)
135
+ id = payload["id"]
136
+ client_seen(id)
137
+ if client_id_valid?(id)
138
+ logger.info "Received heartbeat message from id '#{id}'"
139
+ else
140
+ add_unknown_client_id(id)
141
+ msg = "Received heartbeat message from unknown id '#{id}'"
142
+ logger.error msg
143
+ beetle.publish(:system_notification, {"message" => msg}.to_json)
144
+ end
145
+ end
146
+
107
147
  # called by the message dispatcher when a "client_invalidated" message from a RedisConfigurationClient is received
108
148
  def client_invalidated(payload)
109
149
  id = payload["id"]
150
+ client_seen(id)
110
151
  token = payload["token"]
152
+ add_unknown_client_id(id) unless client_id_valid?(id)
111
153
  logger.info "Received client_invalidated message from id '#{id}' with token '#{token}'"
112
154
  return unless redeem_token(token)
113
155
  @client_invalidated_ids_received << id
@@ -161,6 +203,23 @@ module Beetle
161
203
 
162
204
  private
163
205
 
206
+ # prevent memory overflows caused by evil or buggy clients
207
+ MAX_UNKNOWN_CLIENT_IDS = 20
208
+
209
+ def add_unknown_client_id(id)
210
+ ids = @unknown_client_ids
211
+ while ids.size >= MAX_UNKNOWN_CLIENT_IDS
212
+ old_id = ids.first
213
+ ids.delete(old_id)
214
+ clients_last_seen.delete(old_id)
215
+ end
216
+ ids << id
217
+ end
218
+
219
+ def client_seen(client_id)
220
+ clients_last_seen[client_id] = Time.now
221
+ end
222
+
164
223
  def check_redis_configuration
165
224
  raise ConfigurationError.new("Redis failover needs two or more redis servers") if redis.instances.size < 2
166
225
  end
@@ -185,10 +244,12 @@ module Beetle
185
244
  config.message :client_started
186
245
  config.message :pong
187
246
  config.message :client_invalidated
247
+ config.message :heartbeat
188
248
  # queue setup
189
249
  config.queue :server, :key => 'pong', :amqp_name => "#{system}_configuration_server"
190
250
  config.binding :server, :key => 'client_started'
191
251
  config.binding :server, :key => 'client_invalidated'
252
+ config.binding :server, :key => 'heartbeat'
192
253
  config.handler :server, MessageDispatcher
193
254
  end
194
255
  end
@@ -216,6 +277,7 @@ module Beetle
216
277
 
217
278
  def validate_pong_client_id(client_id)
218
279
  unless known_client = client_id_valid?(client_id)
280
+ add_unknown_client_id(client_id)
219
281
  msg = "Received pong message from unknown id '#{client_id}'"
220
282
  logger.error msg
221
283
  logger.info "Sending system_notification message with text: #{msg}"
@@ -1,3 +1,3 @@
1
1
  module Beetle
2
- VERSION = "0.4.3"
2
+ VERSION = "0.4.4"
3
3
  end
@@ -5,7 +5,6 @@ module Beetle
5
5
  def setup
6
6
  Beetle.config.redis_servers = "redis:0,redis:1"
7
7
  @client = RedisConfigurationClient.new
8
- Client.any_instance.stubs(:listen)
9
8
  @client.stubs(:touch_master_file)
10
9
  @client.stubs(:verify_redis_master_file_string)
11
10
  end
@@ -13,6 +12,16 @@ module Beetle
13
12
  test "should send a client_started message when started" do
14
13
  @client.stubs(:clear_redis_master_file)
15
14
  @client.beetle.expects(:publish).with(:client_started, {:id => @client.id}.to_json)
15
+ Client.any_instance.stubs(:listen)
16
+ @client.start
17
+ end
18
+
19
+ test "should install a periodic timer which sends a heartbeat message" do
20
+ @client.stubs(:clear_redis_master_file)
21
+ @client.beetle.expects(:publish).with(:client_started, {:id => @client.id}.to_json)
22
+ @client.beetle.expects(:listen).yields
23
+ EventMachine.expects(:add_periodic_timer).with(60).yields
24
+ @client.beetle.expects(:publish).with(:heartbeat, {:id => @client.id}.to_json)
16
25
  @client.start
17
26
  end
18
27
 
@@ -68,6 +77,7 @@ module Beetle
68
77
  Beetle::Client.any_instance.stubs(:publish)
69
78
  @client.expects(:clear_redis_master_file)
70
79
  @client.expects(:client_started!)
80
+ Client.any_instance.stubs(:listen)
71
81
  @client.start
72
82
  end
73
83
 
@@ -76,6 +86,7 @@ module Beetle
76
86
  Beetle::Client.any_instance.stubs(:publish)
77
87
  @client.expects(:clear_redis_master_file)
78
88
  @client.expects(:client_started!)
89
+ Client.any_instance.stubs(:listen)
79
90
  @client.start
80
91
  end
81
92
 
@@ -24,7 +24,6 @@ module Beetle
24
24
  test "should ignore outdated client_invalidated messages" do
25
25
  @server.instance_variable_set(:@current_token, 2)
26
26
  @server.client_invalidated("id" => "rc-client-1", "token" => 2)
27
- old_token = 1.minute.ago.to_f
28
27
  @server.client_invalidated("id" => "rc-client-2", "token" => 1)
29
28
 
30
29
  assert_equal(["rc-client-1"].to_set, @server.instance_variable_get(:@client_invalidated_ids_received))
@@ -33,7 +32,6 @@ module Beetle
33
32
  test "should ignore outdated pong messages" do
34
33
  @server.instance_variable_set(:@current_token, 2)
35
34
  @server.pong("id" => "rc-client-1", "token" => 2)
36
- old_token = 1.minute.ago.to_f
37
35
  @server.pong("id" => "rc-client-2", "token" => 1)
38
36
 
39
37
  assert_equal(["rc-client-1"].to_set, @server.instance_variable_get(:@client_pong_ids_received))
@@ -61,6 +59,8 @@ module Beetle
61
59
  end
62
60
 
63
61
  test "should be able to report current status" do
62
+ @server.expects(:unknown_client_ids).returns(Set.new ["x", "y"])
63
+ @server.expects(:unresponsive_clients).returns(["a", Time.now - 200])
64
64
  assert @server.status.is_a?(Hash)
65
65
  end
66
66
 
@@ -85,6 +85,24 @@ module Beetle
85
85
  assert @server.initiate_master_switch
86
86
  end
87
87
 
88
+ test "should put a limit on the number of stored unknown client ids" do
89
+ 1000.times do |i|
90
+ id = i.to_s
91
+ @server.send(:client_seen, id)
92
+ @server.send(:add_unknown_client_id, id)
93
+ end
94
+ assert @server.unknown_client_ids.size < 100
95
+ assert_equal @server.unknown_client_ids.size, @server.clients_last_seen.size
96
+ end
97
+
98
+ test "should assume clients to be unresponsive after specified interval time" do
99
+ @server.send(:client_seen, "1")
100
+ @server.send(:client_seen, "2")
101
+ @server.client_dead_threshold = 0
102
+ assert_equal %w(1 2), @server.unresponsive_clients.map(&:first)
103
+ @server.client_dead_threshold = 10
104
+ assert_equal [], @server.unresponsive_clients
105
+ end
88
106
  end
89
107
 
90
108
  class RedisConfigurationServerInvalidationTest < MiniTest::Unit::TestCase
@@ -316,6 +334,7 @@ module Beetle
316
334
  msg = "Received pong message from unknown id 'unknown-client'"
317
335
  @server.beetle.expects(:publish).with(:system_notification, ({:message => msg}).to_json)
318
336
  @server.pong(payload)
337
+ assert @server.unknown_client_ids.include?("unknown-client")
319
338
  end
320
339
 
321
340
  test "should send a system notification when receiving client_started message from unknown client" do
@@ -323,13 +342,32 @@ module Beetle
323
342
  msg = "Received client_started message from unknown id 'unknown-client'"
324
343
  @server.beetle.expects(:publish).with(:system_notification, ({:message => msg}).to_json)
325
344
  @server.client_started(payload)
345
+ assert @server.unknown_client_ids.include?("unknown-client")
326
346
  end
327
347
 
328
348
  test "should not send a system notification when receiving a client started message from a known client" do
329
349
  payload = {"id" => "rc-client-1"}
330
- msg = "Received client_started message from unknown id 'unknown-client'"
331
350
  @server.beetle.expects(:publish).never
351
+ @server.expects(:add_unknown_client_id).never
332
352
  @server.client_started(payload)
353
+ assert @server.clients_last_seen.include?("rc-client-1")
354
+ end
355
+
356
+ test "should send a system notification when receiving heartbeat message from unknown client" do
357
+ payload = {"id" => "unknown-client"}
358
+ msg = "Received heartbeat message from unknown id 'unknown-client'"
359
+ @server.beetle.expects(:publish).with(:system_notification, ({:message => msg}).to_json)
360
+ @server.heartbeat(payload)
361
+ assert @server.unknown_client_ids.include?("unknown-client")
333
362
  end
363
+
364
+ test "should not send a system notification when receiving a heartbeat message from a known client" do
365
+ payload = {"id" => "rc-client-1"}
366
+ @server.beetle.expects(:publish).never
367
+ @server.expects(:add_unknown_client_id).never
368
+ @server.heartbeat(payload)
369
+ assert @server.clients_last_seen.include?("rc-client-1")
370
+ end
371
+
334
372
  end
335
373
  end
data/test/test_helper.rb CHANGED
@@ -11,6 +11,7 @@ require 'minitest/pride' if ENV['RAINBOW_COLORED_TESTS'] == "1" && $stdout.tty?
11
11
  require 'mocha/setup'
12
12
 
13
13
  require File.expand_path(File.dirname(__FILE__) + '/../lib/beetle')
14
+ require 'eventmachine'
14
15
 
15
16
  class MiniTest::Unit::TestCase
16
17
  require "active_support/testing/declarative"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beetle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Kaes
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2016-02-22 00:00:00.000000000 Z
15
+ date: 2016-03-04 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: uuid4r