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.
- checksums.yaml +4 -4
- data/RELEASE_NOTES.rdoc +6 -0
- data/Rakefile +12 -10
- data/features/support/test_daemons/redis.conf.erb +721 -73
- data/lib/beetle/commands.rb +6 -1
- data/lib/beetle/configuration.rb +10 -0
- data/lib/beetle/redis_configuration_client.rb +11 -1
- data/lib/beetle/redis_configuration_http_server.rb +1 -1
- data/lib/beetle/redis_configuration_server.rb +62 -0
- data/lib/beetle/version.rb +1 -1
- data/test/beetle/redis_configuration_client_test.rb +12 -1
- data/test/beetle/redis_configuration_server_test.rb +41 -3
- data/test/test_helper.rb +1 -0
- metadata +2 -2
data/lib/beetle/commands.rb
CHANGED
data/lib/beetle/configuration.rb
CHANGED
@@ -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}"
|
data/lib/beetle/version.rb
CHANGED
@@ -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.
|
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-
|
15
|
+
date: 2016-03-04 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: uuid4r
|