hot_potato 0.12.1
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.
- 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
|