pazuzu 0.1.2
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 +8 -0
- data/Gemfile +2 -0
- data/Pazuzu.gemspec +29 -0
- data/README.md +171 -0
- data/Rakefile +21 -0
- data/bin/pazuzu +6 -0
- data/bin/pazuzud +6 -0
- data/lib/pazuzu.rb +37 -0
- data/lib/pazuzu/application.rb +114 -0
- data/lib/pazuzu/cgroup.rb +73 -0
- data/lib/pazuzu/command_line/controller.rb +152 -0
- data/lib/pazuzu/control/protocol.rb +304 -0
- data/lib/pazuzu/control/socket_client.rb +30 -0
- data/lib/pazuzu/control/socket_server.rb +75 -0
- data/lib/pazuzu/instance.rb +218 -0
- data/lib/pazuzu/procfiles.rb +16 -0
- data/lib/pazuzu/supervisor.rb +201 -0
- data/lib/pazuzu/supervisor_runner.rb +95 -0
- data/lib/pazuzu/utility/annotated_logger.rb +46 -0
- data/lib/pazuzu/utility/output_tailer.rb +60 -0
- data/lib/pazuzu/utility/process_spawning.rb +40 -0
- data/lib/pazuzu/utility/rate_limiter.rb +68 -0
- data/lib/pazuzu/utility/runnable.rb +120 -0
- data/lib/pazuzu/utility/runnable_pool.rb +62 -0
- data/lib/pazuzu/version.rb +3 -0
- data/lib/pazuzu/worker.rb +173 -0
- metadata +193 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
module Pazuzu
|
2
|
+
module CommandLine
|
3
|
+
|
4
|
+
class Controller
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
end
|
8
|
+
|
9
|
+
def run!(arguments)
|
10
|
+
config_path, socket_path, format = nil, nil, 'text'
|
11
|
+
|
12
|
+
OptionParser.new do |opts|
|
13
|
+
opts.banner = "#{File.basename($0)} [OPTIONS] COMMAND [...]"
|
14
|
+
opts.separator ""
|
15
|
+
opts.on('--socket PATH', 'Connect to socket PATH') do |value|
|
16
|
+
socket_path = File.expand_path(value)
|
17
|
+
end
|
18
|
+
opts.on("-c FILE", "--config FILE", 'Read configuration from FILE') do |value|
|
19
|
+
config_path = File.expand_path(value)
|
20
|
+
end
|
21
|
+
opts.on('-f FORMAT', '--format FORMAT') do |value|
|
22
|
+
format = value
|
23
|
+
end
|
24
|
+
opts.on("-h", "--help", "Show this help.") do
|
25
|
+
puts opts
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
opts.parse!(arguments)
|
29
|
+
end
|
30
|
+
|
31
|
+
if arguments.empty?
|
32
|
+
abort "No command specified. Run with 'help' for list of commands."
|
33
|
+
end
|
34
|
+
|
35
|
+
unless %w(text json).include?(format)
|
36
|
+
abort "Invalid format '#{format}'."
|
37
|
+
end
|
38
|
+
|
39
|
+
if config_path
|
40
|
+
configuration = YAML.load(File.open(config_path))
|
41
|
+
socket_path = configuration['socket_path']
|
42
|
+
end
|
43
|
+
socket_path ||= Supervisor::DEFAULT_SOCKET_PATH
|
44
|
+
|
45
|
+
command, command_arguments = arguments[0], arguments[1..-1]
|
46
|
+
|
47
|
+
begin
|
48
|
+
client = Control::SocketClient.new(socket_path)
|
49
|
+
rescue Errno::ECONNREFUSED
|
50
|
+
abort "Connection refused on #{socket_path}. Server might not be running."
|
51
|
+
else
|
52
|
+
response = client.execute(command, *command_arguments)
|
53
|
+
if response
|
54
|
+
case format
|
55
|
+
when 'json'
|
56
|
+
puts JSON.dump(response, :pretty => true)
|
57
|
+
when 'text'
|
58
|
+
if response['success']
|
59
|
+
puts(response['success'])
|
60
|
+
elsif response['error']
|
61
|
+
abort(response['error'])
|
62
|
+
elsif response['results']
|
63
|
+
case command
|
64
|
+
when 'log'
|
65
|
+
print_log(response['results'])
|
66
|
+
when 'help'
|
67
|
+
print_help(response['results'])
|
68
|
+
when 'list'
|
69
|
+
print_applications(response['results'])
|
70
|
+
else
|
71
|
+
abort("Don't know how to print results from '#{command}'.")
|
72
|
+
end
|
73
|
+
else
|
74
|
+
abort("Unrecognized response from server: #{JSON.dump response}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
else
|
78
|
+
exit(1)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def print_log(entries)
|
84
|
+
entries.each do |entry|
|
85
|
+
puts [
|
86
|
+
entry['time'],
|
87
|
+
entry['source'],
|
88
|
+
entry['message']
|
89
|
+
].join("\t")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def print_help(items)
|
94
|
+
puts "Command Description"
|
95
|
+
puts "------------------------- ------------------------------"
|
96
|
+
items.each do |item|
|
97
|
+
puts [
|
98
|
+
"%-25s" % item['syntax'],
|
99
|
+
"%-30s" % item['description']
|
100
|
+
].join(" ")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def print_applications(applications)
|
105
|
+
puts "Process State Command"
|
106
|
+
puts "---------------------- ------------------------------ ----------------------------"
|
107
|
+
applications.each do |application|
|
108
|
+
(application['workers'] || []).each do |worker|
|
109
|
+
(worker['instances'] || []).each do |instance|
|
110
|
+
state = instance['state']
|
111
|
+
if state == 'running'
|
112
|
+
state = format_uptime(instance['uptime'])
|
113
|
+
end
|
114
|
+
recovery_count = instance['recovery_count']
|
115
|
+
if recovery_count and recovery_count > 0
|
116
|
+
state << " (#{recovery_count} recoveries)"
|
117
|
+
end
|
118
|
+
puts [
|
119
|
+
"%-22s" % instance['name'],
|
120
|
+
"%-30s" % state,
|
121
|
+
instance['command']
|
122
|
+
].join(" ")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def format_uptime(uptime)
|
129
|
+
s = 'running'
|
130
|
+
if uptime
|
131
|
+
s << ' for '
|
132
|
+
days, hours, minutes, seconds =
|
133
|
+
(uptime / 1.day).floor,
|
134
|
+
(uptime / 1.hour).floor,
|
135
|
+
(uptime / 1.minute).floor,
|
136
|
+
(uptime / 1.second).floor
|
137
|
+
if days > 0
|
138
|
+
s << "#{days}d, #{hours}h"
|
139
|
+
elsif hours > 0 or minutes > 0
|
140
|
+
s << "#{hours}h, " if hours > 0
|
141
|
+
s << "#{minutes}m"
|
142
|
+
else
|
143
|
+
s << "#{seconds}s"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
s
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,304 @@
|
|
1
|
+
module Pazuzu
|
2
|
+
module Control
|
3
|
+
|
4
|
+
class Protocol
|
5
|
+
|
6
|
+
COMMAND_MAPPINGS = [
|
7
|
+
{
|
8
|
+
:pattern => /^help$/,
|
9
|
+
:command => :help,
|
10
|
+
:usage => 'help',
|
11
|
+
:description => 'Show commands.'
|
12
|
+
},
|
13
|
+
{
|
14
|
+
:pattern => /^q(?:uit)?$/,
|
15
|
+
:command => :quit,
|
16
|
+
:usage => 'q, quit',
|
17
|
+
:description => 'Quits session.'
|
18
|
+
},
|
19
|
+
{
|
20
|
+
:pattern => /^list(?:\s+(?<spec>[^\s]+))?$/,
|
21
|
+
:command => :list,
|
22
|
+
:usage => 'list [SPEC]',
|
23
|
+
:description => 'Lists applications whose name, worker or instance matches SPEC (eg., "myapp", "myapp.web*").'
|
24
|
+
},
|
25
|
+
{
|
26
|
+
:pattern => /^log(?:\s+(?<spec>[^\s]+)(?:\s+(?<limit>.*))?)?$/,
|
27
|
+
:command => :log,
|
28
|
+
:usage => 'log [SPEC] [LIMIT]',
|
29
|
+
:description => 'Lists log entries for application, workers or instances matching SPEC, at most LIMIT lines.'
|
30
|
+
},
|
31
|
+
{
|
32
|
+
:pattern => /^start(?:\s+(?<spec>[^\s]+))?$/,
|
33
|
+
:command => :start,
|
34
|
+
:usage => 'start SPEC',
|
35
|
+
:description => 'Starts an application, worker or instance by SPEC.'
|
36
|
+
},
|
37
|
+
{
|
38
|
+
:pattern => /^restart(?:\s+(?<spec>[^\s]+))?$/,
|
39
|
+
:command => :restart,
|
40
|
+
:usage => 'restart SPEC',
|
41
|
+
:description => 'Restarts an application, worker or instance by SPEC. Stopped matches will be started.'
|
42
|
+
},
|
43
|
+
{
|
44
|
+
:pattern => /^stop(?:\s+(?<spec>[^\s]+))?$/,
|
45
|
+
:command => :stop,
|
46
|
+
:usage => 'stop SPEC',
|
47
|
+
:description => 'Stops an application, worker or instance by SPEC.'
|
48
|
+
},
|
49
|
+
{
|
50
|
+
:pattern => /^scale\s+(?<spec>[^\s]+)(?:\s+(?<value>(?:\+|-)?[^\s]+))?$/,
|
51
|
+
:command => :scale,
|
52
|
+
:usage => 'scale WORKER-SPEC VALUE',
|
53
|
+
:description => 'Set the number of instances for a worker (or workers). Value may be an absolute number, or a relative (+1, -1), or the string "default".'
|
54
|
+
}
|
55
|
+
].freeze
|
56
|
+
|
57
|
+
def initialize(io, supervisor)
|
58
|
+
@io = io
|
59
|
+
@supervisor = supervisor
|
60
|
+
@active = true
|
61
|
+
@logger = Utility::AnnotatedLogger.new(
|
62
|
+
@supervisor.logger, 'Control socket')
|
63
|
+
end
|
64
|
+
|
65
|
+
def run!
|
66
|
+
while @active
|
67
|
+
line = @io.gets
|
68
|
+
break unless line
|
69
|
+
line.strip!
|
70
|
+
unless line.blank?
|
71
|
+
handle_command(line)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_command(line)
|
77
|
+
@logger.info "Received: #{line}"
|
78
|
+
matched = false
|
79
|
+
COMMAND_MAPPINGS.each do |command|
|
80
|
+
match = command[:pattern].match(line)
|
81
|
+
if match
|
82
|
+
self.send("handle_#{command[:command]}_command", match)
|
83
|
+
matched = true
|
84
|
+
break
|
85
|
+
end
|
86
|
+
end
|
87
|
+
unless matched
|
88
|
+
error! "Don't understand '#{line}'"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def handle_help_command(options)
|
93
|
+
results! COMMAND_MAPPINGS.map { |command|
|
94
|
+
{
|
95
|
+
:syntax => command[:usage],
|
96
|
+
:description => command[:description]
|
97
|
+
}
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def handle_list_command(options)
|
102
|
+
spec = options[:spec]
|
103
|
+
results = Set.new
|
104
|
+
match_objects(spec) do |object|
|
105
|
+
case object
|
106
|
+
when Application
|
107
|
+
results.add(object)
|
108
|
+
when Worker
|
109
|
+
results.add(object.application)
|
110
|
+
when Instance
|
111
|
+
results.add(object.worker.application)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
results! results.map { |object| serialize_object(object) }
|
115
|
+
end
|
116
|
+
|
117
|
+
def handle_log_command(options)
|
118
|
+
spec = options[:spec]
|
119
|
+
limit = options[:limit].try(:to_i)
|
120
|
+
matches = collapse_hierarchy(match_objects(spec))
|
121
|
+
entries = matches.map { |object| object.log_entries }
|
122
|
+
entries.flatten!(1)
|
123
|
+
entries.sort_by! { |(source, time, message)| time }
|
124
|
+
entries.slice!(0, entries.length - limit) if limit
|
125
|
+
results! entries.map { |(source, time, message)|
|
126
|
+
{:source => source, :time => time, :message => message}
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def handle_start_command(options)
|
131
|
+
spec = options[:spec]
|
132
|
+
matches = collapse_hierarchy(match_objects(spec))
|
133
|
+
matches |= match_objects(spec).select { |object| object.run_state != :running }
|
134
|
+
matches.each do |object|
|
135
|
+
object.start!
|
136
|
+
end
|
137
|
+
if matches.any?
|
138
|
+
success! "Started #{matches.map(&:qname).join(', ')}."
|
139
|
+
else
|
140
|
+
error! "No matches."
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def handle_restart_command(options)
|
145
|
+
spec = options[:spec]
|
146
|
+
matches = collapse_hierarchy(match_objects(spec))
|
147
|
+
matches |= match_objects(spec).select { |object| object.run_state != :running }
|
148
|
+
matches.each do |object|
|
149
|
+
object.stop!
|
150
|
+
end
|
151
|
+
if matches.any?
|
152
|
+
begin
|
153
|
+
timeout(30) do
|
154
|
+
loop do
|
155
|
+
break if matches.all? { |object| object.run_state == :stopped }
|
156
|
+
sleep(1)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
matches.each do |object|
|
160
|
+
object.start!
|
161
|
+
end
|
162
|
+
timeout(30) do
|
163
|
+
loop do
|
164
|
+
break if matches.all? { |object| object.run_state == :running }
|
165
|
+
sleep(1)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
rescue Timeout::Error
|
169
|
+
error! "Timed out."
|
170
|
+
else
|
171
|
+
success! "Restarted #{matches.map(&:qname).join(', ')}."
|
172
|
+
end
|
173
|
+
else
|
174
|
+
error! "No matches."
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def handle_stop_command(options)
|
179
|
+
spec = options[:spec]
|
180
|
+
matches = collapse_hierarchy(match_objects(spec))
|
181
|
+
matches.each do |object|
|
182
|
+
object.stop!
|
183
|
+
end
|
184
|
+
if matches.any?
|
185
|
+
success! "Stopped #{matches.map(&:qname).join(', ')}."
|
186
|
+
else
|
187
|
+
error! "No matches."
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def handle_scale_command(options)
|
192
|
+
spec, value = options[:spec], options[:value]
|
193
|
+
if value
|
194
|
+
matches = []
|
195
|
+
match_objects(spec) do |object|
|
196
|
+
if object.is_a?(Worker)
|
197
|
+
case value
|
198
|
+
when /^(\+|-)/
|
199
|
+
object.add_instance_count!(value.to_i)
|
200
|
+
when /^default$/i
|
201
|
+
object.revert_dynamic_instance_count!
|
202
|
+
else
|
203
|
+
object.set_instance_count!(value.to_i)
|
204
|
+
end
|
205
|
+
matches << object
|
206
|
+
end
|
207
|
+
end
|
208
|
+
if matches.any?
|
209
|
+
success! "Adjusted #{matches.map(&:qname).join(', ')}."
|
210
|
+
else
|
211
|
+
error! "No matches."
|
212
|
+
end
|
213
|
+
else
|
214
|
+
error! "No value specified."
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def handle_quit_command(options)
|
219
|
+
success! 'It was a pleasure.'
|
220
|
+
@active = false
|
221
|
+
end
|
222
|
+
|
223
|
+
def respond!(data)
|
224
|
+
output = JSON.dump(data, :pretty => true)
|
225
|
+
@logger.debug "Sending: #{output}"
|
226
|
+
@io.puts('%010x' % output.length)
|
227
|
+
@io.puts(output)
|
228
|
+
end
|
229
|
+
|
230
|
+
def results!(results)
|
231
|
+
respond!(:results => results)
|
232
|
+
end
|
233
|
+
|
234
|
+
def error!(message)
|
235
|
+
respond!(:error => message)
|
236
|
+
end
|
237
|
+
|
238
|
+
def success!(message)
|
239
|
+
respond!(:success => message)
|
240
|
+
end
|
241
|
+
|
242
|
+
def serialize_object(object)
|
243
|
+
data = {}
|
244
|
+
data[:name] = object.qname
|
245
|
+
data[:command] = object.command_line if object.respond_to?(:command_line)
|
246
|
+
data[:state] = object.run_state
|
247
|
+
data[:started_at] = object.started_at.iso8601 if object.started_at
|
248
|
+
data[:stopped_at] = object.stopped_at.iso8601 if object.stopped_at
|
249
|
+
data[:uptime] = Time.now - object.started_at if object.started_at
|
250
|
+
case object
|
251
|
+
when Application
|
252
|
+
data[:workers] = object.workers.map { |worker| serialize_object(worker) }
|
253
|
+
when Worker
|
254
|
+
data[:instances] = object.instances.map { |instance| serialize_object(instance) }
|
255
|
+
when Instance
|
256
|
+
data[:recovery_count] = object.recovery_count
|
257
|
+
end
|
258
|
+
data
|
259
|
+
end
|
260
|
+
|
261
|
+
def collapse_hierarchy(objects)
|
262
|
+
return objects.dup.delete_if { |object|
|
263
|
+
case object
|
264
|
+
when Worker
|
265
|
+
objects.include?(object.application)
|
266
|
+
when Instance
|
267
|
+
objects.include?(object.worker) || objects.include?(object.worker.application)
|
268
|
+
else
|
269
|
+
false
|
270
|
+
end
|
271
|
+
}
|
272
|
+
end
|
273
|
+
|
274
|
+
def match_objects(spec, &block)
|
275
|
+
results = Set.new
|
276
|
+
@supervisor.applications.each do |application|
|
277
|
+
application.workers.each do |worker|
|
278
|
+
worker.instances.each do |instance|
|
279
|
+
if match_name?(instance.qname, spec)
|
280
|
+
results << instance
|
281
|
+
yield instance if block
|
282
|
+
end
|
283
|
+
end
|
284
|
+
if match_name?(worker.qname, spec)
|
285
|
+
results << worker
|
286
|
+
yield worker if block
|
287
|
+
end
|
288
|
+
end
|
289
|
+
if match_name?(application.qname, spec)
|
290
|
+
results << application
|
291
|
+
yield application if block
|
292
|
+
end
|
293
|
+
end
|
294
|
+
results.to_a
|
295
|
+
end
|
296
|
+
|
297
|
+
def match_name?(name, spec)
|
298
|
+
spec.blank? || name == spec || File.fnmatch(spec, name)
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
|
303
|
+
end
|
304
|
+
end
|