beetle 0.4.3 → 0.4.4

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.
@@ -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