beetle 1.0.4 → 2.0.0rc1
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/README.rdoc +15 -7
- data/REDIS_AUTO_FAILOVER.rdoc +8 -16
- data/RELEASE_NOTES.rdoc +5 -0
- data/beetle.gemspec +22 -10
- data/features/support/system_notification_logger +29 -12
- data/features/support/test_daemons/redis.rb +1 -1
- data/features/support/test_daemons/redis_configuration_client.rb +3 -3
- data/features/support/test_daemons/redis_configuration_server.rb +2 -2
- data/lib/beetle/deduplication_store.rb +0 -66
- data/lib/beetle/redis_master_file.rb +0 -5
- data/lib/beetle/version.rb +1 -1
- data/test/beetle/deduplication_store_test.rb +0 -48
- data/test/beetle/message_test.rb +0 -30
- metadata +165 -25
- data/bin/beetle +0 -9
- data/lib/beetle/commands/configuration_client.rb +0 -98
- data/lib/beetle/commands/configuration_server.rb +0 -98
- data/lib/beetle/commands/garbage_collect_deduplication_store.rb +0 -52
- data/lib/beetle/commands.rb +0 -35
- data/lib/beetle/redis_configuration_client.rb +0 -157
- data/lib/beetle/redis_configuration_http_server.rb +0 -152
- data/lib/beetle/redis_configuration_server.rb +0 -438
- data/lib/beetle/redis_server_info.rb +0 -66
- data/test/beetle/redis_configuration_client_test.rb +0 -118
- data/test/beetle/redis_configuration_server_test.rb +0 -381
@@ -1,157 +0,0 @@
|
|
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 fully qualified hostname)
|
27
|
-
def id
|
28
|
-
@id ||= Beetle.hostname
|
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
|
-
client_started!
|
41
|
-
logger.info "RedisConfigurationClient starting (client id: #{id})"
|
42
|
-
determine_initial_master
|
43
|
-
clear_redis_master_file unless current_master.try(:master?)
|
44
|
-
logger.info "Listening"
|
45
|
-
beetle.listen do
|
46
|
-
EventMachine.add_periodic_timer(Beetle.config.redis_failover_client_heartbeat_interval) do
|
47
|
-
heartbeat!
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# called by the message dispatcher when a "pong" message from RedisConfigurationServer is received
|
53
|
-
def ping(payload)
|
54
|
-
token = payload["token"]
|
55
|
-
logger.info "Received ping message with token '#{token}'"
|
56
|
-
pong! if redeem_token(token)
|
57
|
-
end
|
58
|
-
|
59
|
-
# called by the message dispatcher when a "invalidate" message from RedisConfigurationServer is received
|
60
|
-
def invalidate(payload)
|
61
|
-
token = payload["token"]
|
62
|
-
logger.info "Received invalidate message with token '#{token}'"
|
63
|
-
invalidate! if redeem_token(token) && !current_master.try(:master?)
|
64
|
-
end
|
65
|
-
|
66
|
-
# called by the message dispatcher when a "reconfigure"" message from RedisConfigurationServer is received
|
67
|
-
def reconfigure(payload)
|
68
|
-
server = payload["server"]
|
69
|
-
token = payload["token"]
|
70
|
-
logger.info "Received reconfigure message with server '#{server}' and token '#{token}'"
|
71
|
-
return unless redeem_token(token)
|
72
|
-
unless server == read_redis_master_file
|
73
|
-
new_master!(server)
|
74
|
-
write_redis_master_file(server)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# Beetle::Client instance for communication with the RedisConfigurationServer
|
79
|
-
def beetle
|
80
|
-
@beetle ||= build_beetle
|
81
|
-
end
|
82
|
-
|
83
|
-
def config #:nodoc:
|
84
|
-
beetle.config
|
85
|
-
end
|
86
|
-
|
87
|
-
private
|
88
|
-
|
89
|
-
def determine_initial_master
|
90
|
-
if master_file_exists? && server = read_redis_master_file
|
91
|
-
new_master!(server)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def new_master!(server)
|
96
|
-
@current_master = Redis.from_server_string(server, :timeout => 3)
|
97
|
-
end
|
98
|
-
|
99
|
-
def build_beetle
|
100
|
-
system = Beetle.config.system_name
|
101
|
-
Beetle::Client.new.configure :exchange => system, :auto_delete => true do |config|
|
102
|
-
# messages sent
|
103
|
-
config.message :pong
|
104
|
-
config.message :client_started
|
105
|
-
config.message :client_invalidated
|
106
|
-
config.message :heartbeat
|
107
|
-
# messages received
|
108
|
-
config.message :ping
|
109
|
-
config.message :invalidate
|
110
|
-
config.message :reconfigure
|
111
|
-
# queue setup
|
112
|
-
config.queue :client, :key => 'ping', :amqp_name => "#{system}_configuration_client_#{id}"
|
113
|
-
config.binding :client, :key => 'invalidate'
|
114
|
-
config.binding :client, :key => 'reconfigure'
|
115
|
-
config.handler :client, MessageDispatcher
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def redeem_token(token)
|
120
|
-
@current_token = token if @current_token.nil? || token > @current_token
|
121
|
-
token_valid = token >= @current_token
|
122
|
-
logger.info "Ignored message (token was '#{token}', but expected to be >= '#{@current_token}')" unless token_valid
|
123
|
-
token_valid
|
124
|
-
end
|
125
|
-
|
126
|
-
def pong!
|
127
|
-
logger.info "Sending pong message with id '#{id}' and token '#{@current_token}'"
|
128
|
-
beetle.publish(:pong, {"id" => id, "token" => @current_token}.to_json)
|
129
|
-
end
|
130
|
-
|
131
|
-
def client_started!
|
132
|
-
logger.info "Sending client_started message with id '#{id}'"
|
133
|
-
beetle.publish(:client_started, {"id" => id}.to_json)
|
134
|
-
end
|
135
|
-
|
136
|
-
def heartbeat!
|
137
|
-
logger.info "Sending heartbeat message with id '#{id}'"
|
138
|
-
beetle.publish(:heartbeat, {"id" => id}.to_json)
|
139
|
-
end
|
140
|
-
|
141
|
-
def invalidate!
|
142
|
-
@current_master = nil
|
143
|
-
clear_redis_master_file
|
144
|
-
logger.info "Sending client_invalidated message with id '#{id}' and token '#{@current_token}'"
|
145
|
-
beetle.publish(:client_invalidated, {"id" => id, "token" => @current_token}.to_json)
|
146
|
-
end
|
147
|
-
|
148
|
-
|
149
|
-
# Dispatches messages from the queue to methods in RedisConfigurationClient
|
150
|
-
class MessageDispatcher < Beetle::Handler #:nodoc:
|
151
|
-
cattr_accessor :configuration_client
|
152
|
-
def process
|
153
|
-
@@configuration_client.__send__(message.header.routing_key, ActiveSupport::JSON.decode(message.data))
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
@@ -1,152 +0,0 @@
|
|
1
|
-
require 'evma_httpserver'
|
2
|
-
|
3
|
-
module Beetle
|
4
|
-
class RedisConfigurationHttpServer < EM::Connection
|
5
|
-
include EM::HttpServer
|
6
|
-
|
7
|
-
def post_init
|
8
|
-
super
|
9
|
-
no_environment_strings
|
10
|
-
end
|
11
|
-
|
12
|
-
cattr_accessor :config_server
|
13
|
-
|
14
|
-
def process_http_request
|
15
|
-
# the http request details are available via the following instance variables:
|
16
|
-
# @http_protocol
|
17
|
-
# @http_request_method
|
18
|
-
# @http_cookie
|
19
|
-
# @http_if_none_match
|
20
|
-
# @http_content_type
|
21
|
-
# @http_path_info
|
22
|
-
# @http_request_uri
|
23
|
-
# @http_query_string
|
24
|
-
# @http_post_content
|
25
|
-
# @http_headers
|
26
|
-
response = EM::DelegatedHttpResponse.new(self)
|
27
|
-
response.headers['Refresh'] = '3; url=/'
|
28
|
-
# headers = @http_headers.split("\0").inject({}){|h, s| (s =~ /^([^:]+): (.*)$/ && (h[$1] = $2)); h }
|
29
|
-
|
30
|
-
case @http_request_uri
|
31
|
-
when '/', '/.html'
|
32
|
-
response.content_type 'text/html'
|
33
|
-
server_status(response, "html")
|
34
|
-
when "/.json"
|
35
|
-
response.content_type 'application/json'
|
36
|
-
server_status(response, "json")
|
37
|
-
when "/.txt"
|
38
|
-
response.content_type 'text/plain'
|
39
|
-
server_status(response, "plain")
|
40
|
-
when '/initiate_master_switch'
|
41
|
-
initiate_master_switch(response)
|
42
|
-
when '/brokers'
|
43
|
-
list_brokers(response)
|
44
|
-
else
|
45
|
-
not_found(response)
|
46
|
-
end
|
47
|
-
response.send_response
|
48
|
-
end
|
49
|
-
|
50
|
-
def list_brokers(response)
|
51
|
-
brokers = config_server.config.brokers
|
52
|
-
response.status = 200
|
53
|
-
if @http_headers =~ %r(application/json)
|
54
|
-
response.content_type 'application/json'
|
55
|
-
response.content = brokers.to_json
|
56
|
-
else
|
57
|
-
response.content_type 'text/yaml'
|
58
|
-
response.content = brokers.to_yaml
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def server_status(response, type)
|
63
|
-
response.status = 200
|
64
|
-
status = config_server.status
|
65
|
-
response.content =
|
66
|
-
case type
|
67
|
-
when "plain"
|
68
|
-
plain_text_response(status)
|
69
|
-
when "json"
|
70
|
-
status.to_json
|
71
|
-
when "html"
|
72
|
-
html_response(status)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def plain_text_response(status)
|
77
|
-
status.keys.sort_by{|k| k.to_s}.reverse.map do |k|
|
78
|
-
name = k.to_s # .split('_').join(" ")
|
79
|
-
if (value = status[k]).is_a?(Array)
|
80
|
-
value = value.empty? ? "none" : value.join(", ")
|
81
|
-
end
|
82
|
-
"#{name}: #{value}"
|
83
|
-
end.join("\n")
|
84
|
-
end
|
85
|
-
|
86
|
-
def html_response(status)
|
87
|
-
b = "<!doctype html>"
|
88
|
-
b << "<html><head><title>Beetle Configuration Server Status</title>#{html_styles(status)}</head>"
|
89
|
-
b << "<body><h1>Beetle Configuration Server Status</h1>"
|
90
|
-
unless status[:redis_master_available?]
|
91
|
-
b << "<form name='masterswitch' method='post' action='/initiate_master_switch'>"
|
92
|
-
b << "Master down! "
|
93
|
-
b << "<a href='javascript: document.masterswitch.submit();'>Initiate master switch</a> "
|
94
|
-
b << "or wait until system performs it automatically."
|
95
|
-
b << "</form>"
|
96
|
-
end
|
97
|
-
b << "<table cellspacing=0>\n"
|
98
|
-
plain_text_response(status).split("\n").compact.each do |row|
|
99
|
-
row =~/(^[^:]+): (.*)$/
|
100
|
-
name, value = $1, $2
|
101
|
-
if value =~ /,/
|
102
|
-
value = "<ul>" << value.split(/\s*,\s*/).map{|s| "<li>#{s}</li>"}.join << "</ul>"
|
103
|
-
end
|
104
|
-
b << "<tr><td>#{name}</td><td>#{value}</td></tr>\n"
|
105
|
-
end
|
106
|
-
b << "</table>"
|
107
|
-
b << "</body></html>"
|
108
|
-
end
|
109
|
-
|
110
|
-
def html_styles(status)
|
111
|
-
warn_color = status[:redis_master_available?] ? "#5780b2" : "#A52A2A"
|
112
|
-
<<"EOS"
|
113
|
-
<style media="screen" type="text/css">
|
114
|
-
html { font: 1.25em/1.5 arial, sans-serif;}
|
115
|
-
body { margin: 1em; }
|
116
|
-
table tr:nth-child(2n+1){ background-color: #ffffff; }
|
117
|
-
td { padding: 0.1em 0.2em; vertical-align: top; }
|
118
|
-
ul { list-style-type: none; margin: 0; padding: 0;}
|
119
|
-
li { }
|
120
|
-
h1 { color: #{warn_color}; margin-bottom: 0.2em;}
|
121
|
-
a:link, a:visited {text-decoration:none; color:#A52A2A;}
|
122
|
-
a:hover, a:active {text-decoration:none; color:#FF0000;}
|
123
|
-
a {
|
124
|
-
padding: 10px; background: #cdcdcd;
|
125
|
-
-moz-border-radius: 5px;
|
126
|
-
border-radius: 5px;
|
127
|
-
-moz-box-shadow: 2px 2px 2px #bbb;
|
128
|
-
-webkit-box-shadow: 2px 2px 2px #bbb;
|
129
|
-
box-shadow: 2px 2px 2px #bbb;
|
130
|
-
}
|
131
|
-
form { font-size: 1em; margin-bottom: 1em; }
|
132
|
-
</style>
|
133
|
-
EOS
|
134
|
-
end
|
135
|
-
|
136
|
-
def initiate_master_switch(response)
|
137
|
-
response.content_type 'text/plain'
|
138
|
-
if config_server.initiate_master_switch
|
139
|
-
response.status = 201
|
140
|
-
response.content = "Master switch initiated"
|
141
|
-
else
|
142
|
-
response.status = 200
|
143
|
-
response.content = "No master switch necessary"
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
def not_found(response)
|
148
|
-
response.content_type 'text/plain'
|
149
|
-
response.status = 404
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|