beetle 0.4.3 → 0.4.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|