hot_potato 0.12.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/Rakefile +25 -0
- data/bin/hotpotato +14 -0
- data/hot_potato.gemspec +28 -0
- data/lib/hot_potato/admin/public/admin.css +30 -0
- data/lib/hot_potato/admin/views/index.erb +58 -0
- data/lib/hot_potato/admin.rb +67 -0
- data/lib/hot_potato/app_task.rb +42 -0
- data/lib/hot_potato/app_task_info.rb +80 -0
- data/lib/hot_potato/app_task_server.rb +92 -0
- data/lib/hot_potato/cache.rb +45 -0
- data/lib/hot_potato/core.rb +62 -0
- data/lib/hot_potato/dsl.rb +172 -0
- data/lib/hot_potato/faucet.rb +41 -0
- data/lib/hot_potato/generate.rb +55 -0
- data/lib/hot_potato/generate_app_task.rb +41 -0
- data/lib/hot_potato/queue_logger.rb +33 -0
- data/lib/hot_potato/sink.rb +33 -0
- data/lib/hot_potato/supervisor_info.rb +64 -0
- data/lib/hot_potato/supervisor_server.rb +225 -0
- data/lib/hot_potato/templates/Gemfile +6 -0
- data/lib/hot_potato/templates/Rakefile +9 -0
- data/lib/hot_potato/templates/admin +4 -0
- data/lib/hot_potato/templates/app_task +4 -0
- data/lib/hot_potato/templates/boot.rb +21 -0
- data/lib/hot_potato/templates/config.yml +11 -0
- data/lib/hot_potato/templates/development.rb +0 -0
- data/lib/hot_potato/templates/generate +4 -0
- data/lib/hot_potato/templates/production.rb +0 -0
- data/lib/hot_potato/templates/routes.rb +3 -0
- data/lib/hot_potato/templates/supervisor +4 -0
- data/lib/hot_potato/templates/template_faucet.rb +8 -0
- data/lib/hot_potato/templates/template_sink.rb +7 -0
- data/lib/hot_potato/templates/template_worker.rb +8 -0
- data/lib/hot_potato/templates/test.rb +0 -0
- data/lib/hot_potato/utils.rb +43 -0
- data/lib/hot_potato/version.rb +3 -0
- data/lib/hot_potato/worker.rb +40 -0
- data/lib/hot_potato.rb +20 -0
- data/readme.md +219 -0
- data/test/helper.rb +7 -0
- data/test/version_test.rb +9 -0
- metadata +166 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
module HotPotato
|
2
|
+
|
3
|
+
# The routes file (config/routes.rb) is a Ruby DSL that does the following:
|
4
|
+
#
|
5
|
+
# * Defines AppTasks (Faucets, Workers, Sinks)
|
6
|
+
# * Defines processing chain for AppTasks
|
7
|
+
# * Restrict AppTasks to a host group
|
8
|
+
# * Limit number of instances
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# HotPotato::Route.build do
|
13
|
+
#
|
14
|
+
# faucet :twitter_faucet
|
15
|
+
# worker :influencer, :source => :twitter_faucet
|
16
|
+
# sink :log_writer, :source => :influencer
|
17
|
+
#
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# Multiple sources can be attached to a worker or sink:
|
21
|
+
#
|
22
|
+
# worker :influencer, :source => [:twitter_faucet. :other_source]
|
23
|
+
#
|
24
|
+
# The number of instances is set to 1. This can be changed by setting the number of instances:
|
25
|
+
#
|
26
|
+
# worker :influencer, :source => :twitter_faucet, :instances => 2
|
27
|
+
#
|
28
|
+
# AppTasks can be limited to a specific server (or set of servers) by creating a group in the
|
29
|
+
# config/config.yml file:
|
30
|
+
#
|
31
|
+
# development:
|
32
|
+
# redis_hostname: localhost
|
33
|
+
# redis_port: 6379
|
34
|
+
# servers:
|
35
|
+
# - hostname: worker01
|
36
|
+
# group: incoming
|
37
|
+
# max_app_tasks: 15
|
38
|
+
# - hostname: worker02
|
39
|
+
# group: worker
|
40
|
+
# max_app_tasks: 15
|
41
|
+
#
|
42
|
+
# and specifying the group in the routes files:
|
43
|
+
#
|
44
|
+
# faucet :twitter_faucet, :group => :incoming
|
45
|
+
module Route
|
46
|
+
|
47
|
+
def self.build(&block)
|
48
|
+
@@routes = Routes.new
|
49
|
+
@@routes.instance_eval(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.routes
|
53
|
+
@@routes
|
54
|
+
end
|
55
|
+
|
56
|
+
class Routes
|
57
|
+
|
58
|
+
attr_accessor :faucets, :workers, :sinks
|
59
|
+
|
60
|
+
def initialize
|
61
|
+
@faucets = []
|
62
|
+
@workers = []
|
63
|
+
@sinks = []
|
64
|
+
end
|
65
|
+
|
66
|
+
def find(name)
|
67
|
+
app_tasks.each do |app_task|
|
68
|
+
if app_task.classname.to_s == name.to_s
|
69
|
+
return app_task
|
70
|
+
end
|
71
|
+
end
|
72
|
+
return nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def app_tasks
|
76
|
+
@faucets + @workers + @sinks
|
77
|
+
end
|
78
|
+
|
79
|
+
def faucet(classname, options = {}, &block)
|
80
|
+
f = Faucet.new(classname, options)
|
81
|
+
f.instance_eval(&block) if block
|
82
|
+
@faucets << f
|
83
|
+
end
|
84
|
+
|
85
|
+
def worker(classname, options = {}, &block)
|
86
|
+
w = Worker.new(classname, options)
|
87
|
+
w.instance_eval(&block) if block
|
88
|
+
@workers << w
|
89
|
+
end
|
90
|
+
|
91
|
+
def sink(classname, options = {}, &block)
|
92
|
+
s = Sink.new(classname, options)
|
93
|
+
s.instance_eval(&block) if block
|
94
|
+
@sinks << s
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_s
|
98
|
+
str = "Faucets\n"
|
99
|
+
@faucets.each do |f|
|
100
|
+
str << f.to_s
|
101
|
+
end
|
102
|
+
str << "Workers\n"
|
103
|
+
@workers.each do |f|
|
104
|
+
str << f.to_s
|
105
|
+
end
|
106
|
+
str << "Sinks\n"
|
107
|
+
@sinks.each do |f|
|
108
|
+
str << f.to_s
|
109
|
+
end
|
110
|
+
str
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class AppTask
|
115
|
+
|
116
|
+
include HotPotato::Core
|
117
|
+
|
118
|
+
attr_accessor :classname, :instances, :source, :group
|
119
|
+
def initialize(classname, options = {})
|
120
|
+
@classname = classname.to_s
|
121
|
+
@instances = options[:instances] || 1
|
122
|
+
@group = options[:group] || ""
|
123
|
+
if options[:source]
|
124
|
+
if options[:source].class == Array
|
125
|
+
@source = options[:source].map { |s| s.to_s }
|
126
|
+
else
|
127
|
+
@source = [options[:source].to_s]
|
128
|
+
end
|
129
|
+
else
|
130
|
+
@source = nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def allow_group(name)
|
135
|
+
return true if @group == ""
|
136
|
+
return name == @group.to_s
|
137
|
+
end
|
138
|
+
|
139
|
+
def running_instances
|
140
|
+
stat.keys("hotpotato.apptask.*.#{classify(@classname)}.*").count || 0
|
141
|
+
end
|
142
|
+
|
143
|
+
def type
|
144
|
+
return "Faucet" if Kernel.const_get(@data[:classname]).ancestors.include?(HotPotato::Faucet)
|
145
|
+
return "Worker" if Kernel.const_get(@data[:classname]).ancestors.include?(HotPotato::Worker)
|
146
|
+
return "Sink" if Kernel.const_get(@data[:classname]).ancestors.include?(HotPotato::Sink)
|
147
|
+
return "AppTask"
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
class Faucet < AppTask
|
153
|
+
def to_s
|
154
|
+
" Faucet class [#{@classname}]\n"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class Worker < AppTask
|
159
|
+
def to_s
|
160
|
+
" Worker class [#{@classname}]\n"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class Sink < AppTask
|
165
|
+
def to_s
|
166
|
+
" Sink class [#{@classname}]\n"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module HotPotato
|
2
|
+
|
3
|
+
# Faucets inject data into the system. Examples include: Twitter Reader, SMTP, and Tail Log File.
|
4
|
+
# Each faucet is a ruby file in the app directory that extends HotPotato::Faucet and implements
|
5
|
+
# the perform method. For each message received, the method should call the send_message to send
|
6
|
+
# it to the next AppTask.
|
7
|
+
#
|
8
|
+
# class TwitterFaucet < HotPotato::Faucet
|
9
|
+
#
|
10
|
+
# def perform
|
11
|
+
# TweetStream::Client.new("user", "secret").sample do |s|
|
12
|
+
# message = {}
|
13
|
+
# message["username"] = s.user.screen_name
|
14
|
+
# message["text"] = s.text
|
15
|
+
# send_message message
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# end
|
20
|
+
class Faucet
|
21
|
+
|
22
|
+
include HotPotato::AppTask
|
23
|
+
|
24
|
+
def start
|
25
|
+
@queue_out = underscore(self.class.name.to_sym)
|
26
|
+
if self.respond_to?('perform')
|
27
|
+
start_heartbeat_service
|
28
|
+
perform
|
29
|
+
else
|
30
|
+
log.error "The Faucet #{self.class.name} does not implement a perform method."
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def send_message(m)
|
35
|
+
queue_inject @queue_out, m
|
36
|
+
count_message_out
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module HotPotato
|
4
|
+
|
5
|
+
class Generate
|
6
|
+
|
7
|
+
def initialize(app_path)
|
8
|
+
@app_path = app_path
|
9
|
+
log "Generating application #{@app_path}..."
|
10
|
+
mkdir app_path
|
11
|
+
copy_file "Gemfile"
|
12
|
+
copy_file "Rakefile"
|
13
|
+
mkdir "app"
|
14
|
+
mkdir "bin"
|
15
|
+
copy_file "admin", 'bin'
|
16
|
+
copy_file "app_task", 'bin'
|
17
|
+
copy_file "generate", 'bin'
|
18
|
+
copy_file "supervisor", 'bin'
|
19
|
+
mkdir "config"
|
20
|
+
mkdir "config/environments"
|
21
|
+
copy_file "development.rb", 'config/environments'
|
22
|
+
copy_file "test.rb", 'config/environments'
|
23
|
+
copy_file "production.rb", 'config/environments'
|
24
|
+
copy_file "boot.rb", 'config'
|
25
|
+
copy_file "config.yml", 'config'
|
26
|
+
copy_file "routes.rb", 'config'
|
27
|
+
mkdir "docs"
|
28
|
+
mkdir "logs"
|
29
|
+
mkdir "test"
|
30
|
+
mkdir "tmp"
|
31
|
+
end
|
32
|
+
|
33
|
+
def mkdir(path)
|
34
|
+
dir = path == @app_path ? @app_path : "#{@app_path}/#{path}"
|
35
|
+
if Dir.exists?(dir)
|
36
|
+
log " exist #{dir}"
|
37
|
+
else
|
38
|
+
Dir.mkdir(dir)
|
39
|
+
log " create #{dir}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def copy_file(src, dest = "")
|
44
|
+
dest = "#{dest}/" unless dest == ""
|
45
|
+
log " add #{@app_path}/#{dest}#{src}"
|
46
|
+
FileUtils.cp "#{File.expand_path('..', __FILE__)}/templates/#{src}", "#{@app_path}/#{dest}#{src}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def log(message)
|
50
|
+
puts "#{message}"
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module HotPotato
|
2
|
+
|
3
|
+
class GenerateAppTask
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
usage unless ARGV[1]
|
7
|
+
name = underscore(ARGV[1])
|
8
|
+
|
9
|
+
case ARGV[0]
|
10
|
+
when "sink"
|
11
|
+
process_template "template_sink.rb", "#{name}.rb", classify(name)
|
12
|
+
when "faucet"
|
13
|
+
process_template "template_faucet.rb", "#{name}.rb", classify(name)
|
14
|
+
when "worker"
|
15
|
+
process_template "template_worker.rb", "#{name}.rb", classify(name)
|
16
|
+
else
|
17
|
+
usage
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def usage
|
22
|
+
puts "Usage: generate [faucet|worker|sink] name"
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def process_template(src, dest, name)
|
27
|
+
|
28
|
+
template_file = File.open("#{File.expand_path('..', __FILE__)}/templates/#{src}")
|
29
|
+
contents = ""
|
30
|
+
template_file.each { |line| contents << line}
|
31
|
+
|
32
|
+
result = contents.gsub("__NAME__", name)
|
33
|
+
dest_file = File.new("#{APP_PATH}/app/#{dest}", "w")
|
34
|
+
dest_file.write(result)
|
35
|
+
dest_file.close
|
36
|
+
puts "Writing #{APP_PATH}/app/#{dest}"
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module HotPotato
|
2
|
+
|
3
|
+
class QueueLogger
|
4
|
+
|
5
|
+
include HotPotato::Core
|
6
|
+
|
7
|
+
attr_accessor :level, :formatter
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@classname = options[:classname]
|
11
|
+
end
|
12
|
+
|
13
|
+
def log(message, severity)
|
14
|
+
log_entry = {}
|
15
|
+
log_entry[:created_at] = Time.now
|
16
|
+
log_entry[:message] = message
|
17
|
+
log_entry[:severity] = severity
|
18
|
+
log_entry[:host] = Socket.gethostname
|
19
|
+
log_entry[:pid] = Process.pid
|
20
|
+
log_entry[:classname] = @classname
|
21
|
+
|
22
|
+
queue_inject "hotpotato.log.#{log_entry[:host]}", log_entry.to_json
|
23
|
+
end
|
24
|
+
|
25
|
+
def debug(m); log m, __method__; end
|
26
|
+
def info(m); log m, __method__; end
|
27
|
+
def warn(m); log m, __method__; end
|
28
|
+
def error(m); log m, __method__; end
|
29
|
+
def fatal(m); log m, __method__; end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module HotPotato
|
2
|
+
|
3
|
+
# Sinks send data out of the system. Examples include: WebSocket, Database (Data Warehouse), and
|
4
|
+
# File Writer. Each sink is a ruby file in the app directory that extends HotPotato::Sink and implements
|
5
|
+
# the perform(message) method. There is no send_message for the sink to call since it is a final destination
|
6
|
+
# for the message.
|
7
|
+
#
|
8
|
+
# class LogWriter < HotPotato::Sink
|
9
|
+
#
|
10
|
+
# def perform(message)
|
11
|
+
# log.debug "#{message["username"]}:#{message["influence"]}"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# end
|
15
|
+
class Sink
|
16
|
+
|
17
|
+
include HotPotato::AppTask
|
18
|
+
|
19
|
+
def start(queue_in)
|
20
|
+
if !self.respond_to?('perform')
|
21
|
+
log.error "The Sink #{self.class.name} does not implement a perform method."
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
start_heartbeat_service
|
25
|
+
queue_subscribe(queue_in) do |m|
|
26
|
+
count_message_in
|
27
|
+
perform m
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "socket"
|
2
|
+
|
3
|
+
class SupervisorInfo
|
4
|
+
|
5
|
+
include HotPotato::Core
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@data = {}
|
9
|
+
@data[:started_at] = options[:started_at] || Time.now
|
10
|
+
@data[:updated_at] = options[:updated_at] || @data[:started_at]
|
11
|
+
@data[:hostname] = options[:hostname] || Socket.gethostname
|
12
|
+
@data[:pid] = options[:pid] || Process.pid
|
13
|
+
end
|
14
|
+
|
15
|
+
def key
|
16
|
+
"hotpotato.supervisor.#{@data[:hostname]}.#{@data[:pid]}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
@data.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def app_tasks
|
24
|
+
stat.keys("hotpotato.apptask.#{@data[:hostname]}.*").count
|
25
|
+
end
|
26
|
+
|
27
|
+
def hostname
|
28
|
+
@data[:hostname]
|
29
|
+
end
|
30
|
+
|
31
|
+
def pid
|
32
|
+
@data[:pid]
|
33
|
+
end
|
34
|
+
|
35
|
+
def started_at
|
36
|
+
return nil unless @data[:started_at]
|
37
|
+
DateTime.parse(@data[:started_at])
|
38
|
+
end
|
39
|
+
|
40
|
+
def updated_at
|
41
|
+
return nil unless @data[:updated_at]
|
42
|
+
DateTime.parse(@data[:updated_at])
|
43
|
+
end
|
44
|
+
|
45
|
+
def touch
|
46
|
+
@data[:updated_at] = Time.now
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_json(*a)
|
50
|
+
result = @data
|
51
|
+
result["json_class"] = self.class.name
|
52
|
+
result.to_json(*a)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.json_create(o)
|
56
|
+
options = {}
|
57
|
+
options[:started_at] = o["started_at"]
|
58
|
+
options[:updated_at] = o["updated_at"]
|
59
|
+
options[:pid] = o["pid"]
|
60
|
+
options[:hostname] = o["hostname"]
|
61
|
+
new options
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'optparse'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module HotPotato
|
6
|
+
|
7
|
+
# The supervisor is a process that runs on each machine that participates in the cluster.
|
8
|
+
# When it starts it does the following:
|
9
|
+
#
|
10
|
+
# 0. Read the routes file
|
11
|
+
# 1. Connect to the Redis server and get the appTask process ID table
|
12
|
+
# 2. Acquire the global lock
|
13
|
+
# 3. If a process is needed, fork a new process for AppTask
|
14
|
+
# 4. Release the global lock
|
15
|
+
# 5. Rinse and Repeat
|
16
|
+
#
|
17
|
+
# The supervisor also starts the Heartbeat service and logging service as background threads.
|
18
|
+
#
|
19
|
+
# The supervisor can be managed from the command line:
|
20
|
+
#
|
21
|
+
# $ bin/supervisor [run|start|stop|restart]
|
22
|
+
#
|
23
|
+
# If started without any settings, it will default to run.
|
24
|
+
class SupervisorServer
|
25
|
+
|
26
|
+
include HotPotato::Core
|
27
|
+
|
28
|
+
MAX_APP_TASKS = 32
|
29
|
+
HEARTBEAT_INTERVAL = 20
|
30
|
+
PID_FILE = "#{APP_PATH}/tmp/supervisor.pid"
|
31
|
+
LOG_FILE = "#{APP_PATH}/logs/supervisor.log"
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@options = load_options
|
35
|
+
@options.mode = parse_options
|
36
|
+
trap("INT") { shutdown }
|
37
|
+
self.send(@options.mode)
|
38
|
+
end
|
39
|
+
|
40
|
+
def run
|
41
|
+
$0 = "Hot Potato Supervisor"
|
42
|
+
log.info "Starting Hot Potato Supervisor #{HotPotato::VERSION}"
|
43
|
+
begin
|
44
|
+
start_heartbeat_service
|
45
|
+
start_log_service
|
46
|
+
routes = HotPotato::Route.routes
|
47
|
+
while @options.running do
|
48
|
+
if acquire_lock :supervisor
|
49
|
+
log.debug "Lock acquired"
|
50
|
+
routes.app_tasks.each do |app_task|
|
51
|
+
if app_task.running_instances < app_task.instances && app_task.allow_group(@options.group)
|
52
|
+
if has_capacity
|
53
|
+
log.info "Starting AppTask [#{classify(app_task.classname)}]"
|
54
|
+
pid = fork do
|
55
|
+
Process.setsid
|
56
|
+
exec "#{APP_PATH}/bin/app_task #{app_task.classname.to_s}"
|
57
|
+
end
|
58
|
+
Process.detach pid
|
59
|
+
sleep 2
|
60
|
+
else
|
61
|
+
log.warn "Cannot start AppTask [#{app_task.classname}] - Server at Capacity (Increase max_app_tasks)"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
release_lock :supervisor
|
66
|
+
end
|
67
|
+
sleep (5 + rand(5))
|
68
|
+
end
|
69
|
+
rescue Exception
|
70
|
+
log.error $!
|
71
|
+
log.error $@
|
72
|
+
exit 1
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def start
|
77
|
+
# Check if we are running
|
78
|
+
if File.exists?(PID_FILE)
|
79
|
+
pid = 0;
|
80
|
+
File.open(PID_FILE, 'r') do |f|
|
81
|
+
pid = f.read.to_i
|
82
|
+
end
|
83
|
+
# Check if we are REALLY running
|
84
|
+
if Process.alive?(pid)
|
85
|
+
log.fatal "Supervisor is already running on this machine. Only one instance can run per machine."
|
86
|
+
exit 1
|
87
|
+
else
|
88
|
+
log.info "Supervisor is not running despite the presence of the pid file. I will overwrite the pid file and start the supervisor."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
Process.daemon
|
92
|
+
File.open(PID_FILE, 'w') do |f|
|
93
|
+
f.write "#{Process.pid}\n"
|
94
|
+
end
|
95
|
+
STDIN.reopen '/dev/null'
|
96
|
+
STDOUT.reopen LOG_FILE, 'a'
|
97
|
+
STDERR.reopen STDOUT
|
98
|
+
STDOUT.sync = true
|
99
|
+
STDERR.sync = true
|
100
|
+
run
|
101
|
+
end
|
102
|
+
|
103
|
+
# Stops the Supervisor. Requires the existance of a PID file.
|
104
|
+
# Calls the shutdown hook to stop AppTasks.
|
105
|
+
def stop
|
106
|
+
pid = 0
|
107
|
+
shutdown
|
108
|
+
if File.exists?(PID_FILE)
|
109
|
+
File.open(PID_FILE, 'r') do |f|
|
110
|
+
pid = f.read.to_i
|
111
|
+
end
|
112
|
+
Process.kill("INT", pid) if Process.alive?(pid)
|
113
|
+
File.delete(PID_FILE)
|
114
|
+
else
|
115
|
+
log.fatal "Supervisor PID file does not exist."
|
116
|
+
exit 1
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Restarts the Supervisor
|
121
|
+
def restart
|
122
|
+
stop
|
123
|
+
sleep 2
|
124
|
+
start
|
125
|
+
end
|
126
|
+
|
127
|
+
def parse_options
|
128
|
+
mode = :run
|
129
|
+
op = OptionParser.new do |opts|
|
130
|
+
opts.banner = "Usage: #{$0} [run|start|stop|restart]"
|
131
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
132
|
+
puts op
|
133
|
+
exit
|
134
|
+
end
|
135
|
+
end
|
136
|
+
begin
|
137
|
+
op.parse!
|
138
|
+
mode = (ARGV.shift || "run").to_sym
|
139
|
+
if ![:start, :stop, :restart, :run].include?(mode)
|
140
|
+
puts op
|
141
|
+
exit 1
|
142
|
+
end
|
143
|
+
rescue
|
144
|
+
puts op
|
145
|
+
exit 1
|
146
|
+
end
|
147
|
+
return mode
|
148
|
+
end
|
149
|
+
|
150
|
+
# Kills any running AppTasks on this machine and removes entries from the
|
151
|
+
# process table cache. Removes entry for the supervisor in the process
|
152
|
+
# table cache.
|
153
|
+
def shutdown
|
154
|
+
@options.running = false
|
155
|
+
stat.keys("hotpotato.apptask.#{@options.hostname}.*").each do |app_task_key|
|
156
|
+
app_task = JSON.parse(stat.get(app_task_key))
|
157
|
+
log.info "Killing PID #{app_task.pid} [#{app_task.classname}]"
|
158
|
+
Process.kill("INT", app_task.pid) if Process.alive?(app_task.pid)
|
159
|
+
stat.del app_task_key
|
160
|
+
end
|
161
|
+
stat.keys("hotpotato.supervisor.#{@options.hostname}.*").each do |supervisor_key|
|
162
|
+
stat.del supervisor_key
|
163
|
+
end
|
164
|
+
log.info "Stopping Supervisor..."
|
165
|
+
end
|
166
|
+
|
167
|
+
# Determines if this host reached the limit of the number of AppTasks it
|
168
|
+
# can run
|
169
|
+
def has_capacity
|
170
|
+
return stat.keys("hotpotato.apptask.#{@options.hostname}.*").count < @options.max_app_tasks
|
171
|
+
end
|
172
|
+
|
173
|
+
# OK, this is not really a log service, but it is responsible for subscribing to the log messages
|
174
|
+
# from the AppTasks on this server.
|
175
|
+
def start_log_service
|
176
|
+
Thread.new do
|
177
|
+
log.info "Thread created for Supervisor [Log]"
|
178
|
+
queue_subscribe("hotpotato.log.#{@options.hostname}") do |m|
|
179
|
+
log_entry = JSON.parse(m)
|
180
|
+
if log.respond_to?(log_entry["severity"].to_sym)
|
181
|
+
log.send(log_entry["severity"].to_sym, "#{log_entry['classname']} [#{log_entry['pid']}] - #{log_entry['message']}")
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Starts a background thread to update the process list in redis.
|
188
|
+
def start_heartbeat_service
|
189
|
+
si = SupervisorInfo.new
|
190
|
+
stat.set si.key, si.to_json
|
191
|
+
stat.expire si.key, 120
|
192
|
+
|
193
|
+
Thread.new do
|
194
|
+
log.info "Thread created for Supervisor [Heartbeat]"
|
195
|
+
loop do
|
196
|
+
si.touch
|
197
|
+
stat.set si.key, si.to_json, 120
|
198
|
+
sleep HEARTBEAT_INTERVAL
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Loads the options, mainly from the config.yml file, into an OpenStruct object.
|
204
|
+
def load_options
|
205
|
+
options = OpenStruct.new
|
206
|
+
options.max_app_tasks = MAX_APP_TASKS
|
207
|
+
options.group = ""
|
208
|
+
options.mode = :run
|
209
|
+
options.hostname = Socket.gethostname
|
210
|
+
options.running = true
|
211
|
+
|
212
|
+
config["servers"].each do |server|
|
213
|
+
if server["hostname"] == options.hostname
|
214
|
+
options.max_app_tasks = server["max_app_tasks"] || MAX_APP_TASKS
|
215
|
+
options.group = server["group"] || ""
|
216
|
+
break
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
return options
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|