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.
@@ -0,0 +1,8 @@
1
+ *~
2
+ *.bak
3
+ /_trash
4
+ *.sublime-workspace
5
+ *.sublime-project
6
+ /.rspec
7
+ /Gemfile.lock
8
+ /.rvmrc
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $:.push File.expand_path("../lib", __FILE__)
4
+ require "pazuzu/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "pazuzu"
8
+ s.version = Pazuzu::VERSION
9
+ s.authors = ["Alexander Staubo"]
10
+ s.email = ["alex@origo.no"]
11
+ s.homepage = "http://github.com/origo/pazuzu"
12
+ s.summary = s.description = %{Pazuzu is a supervisor daemon that manages pools of application processes as daemons.}
13
+
14
+ s.rubyforge_project = "pazuzu"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_runtime_dependency 'activesupport', '~> 3.1.0'
22
+ s.add_runtime_dependency 'statemachine', '~> 2.0.1'
23
+ s.add_runtime_dependency 'yajl-ruby', '~> 1.1'
24
+ s.add_runtime_dependency 'i18n'
25
+ s.add_runtime_dependency 'scashin133-syslog_logger', '~> 1.7.3'
26
+
27
+ s.add_development_dependency "rspec"
28
+ s.add_development_dependency "rake"
29
+ end
@@ -0,0 +1,171 @@
1
+ Pazuzu
2
+ ======
3
+
4
+ Pazuzu is a supervisor daemon that manages pools of application processes as daemons. Pazuzu uses Heroku's `Procfile` format and relies on Linux cgroups to encapsulate groups of process.
5
+
6
+ ....
7
+ ......
8
+ ..=?$.. .
9
+ ... ...N~,.... .....
10
+ .. ..... . ...M8O.... ......
11
+ ......NZ$,.. . ...DD8.... . $O$+....
12
+ .$8~Z7$. . ....:N88ZO:Z.... ..8ZOD=..
13
+ . ...8OO7Z$+.... . ..88Z:I=8$?.. ....ZO7I$M.....
14
+ ....DZ+$ZZZ8... . .....?DN878=,~..... . ...N7ODOIDO...
15
+ ....D=ZZ7Z$$?D..... ..M$D8D$D8=I~.... ...N$88IDO8O:....
16
+ ...OOZ$ZO$$?OO.... ...8D8ND8?MM,,... ....8+DZZ7$ZDOO.. .
17
+ ....OZN7$~8Z$7$$.......88O88$~?DO.......=OZZOO8MDOZODN... ..
18
+ .,D$7I7ZZ$?+$Z8,......ZDDDND8M,......M$OZNDO$NODM8+I..
19
+ ..MDO$Z$DMD+~:Z8:......DDNMM?I,....8Z8Z8Z8N?Z$DDO8D...
20
+ . ~+D7IN?Z=IOD$ONN.....N8N8~$D..IIOZ78Z$8Z$DO?ZMOOM...
21
+ ...?7DNO=?IDOO8OONDNNNNNNN8OII$I=$Z8Z78D8NZZZZDDD. ..
22
+ .....MZ$DNDDN?ZZ8DNN8DDDNNNDO7$I$+++ZOOOOO8$NMDI8...
23
+ ....:DOO8NOOZOO8DNNO88DO78O87~~77D=7OO$8OO?NZ?M...
24
+ ...O8ODN$87O8DD8NNND8Z++II:77?7~$$OZOZ$7D+8. .
25
+ .,8ONNDO~=Z8ZONNNNO=~==:8IN87I=OZZ7ID$7.,..
26
+ ...,8OND88+DZ8DNNNNNDO87+D7D8$$:7Z7Z8Z?.....
27
+ ... ...DNN88NNDONNNNNNNNMZO7$DO8ZZ$7$O7, ...
28
+ . ...MNDDDDO88DNNNNNDOOMD$DNN=77777,......
29
+ ..,D8ZDO8NDNNNNDD88D78NND8O,$O$.....
30
+ ...OONN8ODDNNNNDD8ZOI$DNDZ8Z7OO?..
31
+ ... .O78O8OZ8DNNNNNNZOI=ODNDOZI~78+...
32
+ ..8O$OZ88NNNNNNNDD8O+$ONN8DD$IZD...
33
+ .ZDD8OZZDODNDDNNDD$8I+DDNND87O$$$...
34
+ ....N7IOZ8O$O$DNNNNND7O$~,DNDOOZI+78...
35
+ ..=8D8DDNDODZNNNDDNDZOZ:?8DNNZ$~O$8.. .
36
+ ..8DN88DZDDDDNNNNDND8OOO$D8NN88$?=7?...
37
+ . ...8OM8N8D8DDNNNNNNNDZO7Z+DDNN?I7IM8O...
38
+ . ..MDDMODOND88NNN8NDMNO$$Z7ZINNMN$NNIZ,
39
+ ..8O88888DDODDNNDNNNON78Z+Z8DMN=N:O8Z?..
40
+ . .8888OD88M8DNNDONNNM+DOD,+Z$88OZIMD=I..
41
+ . M8OOO8NDO+8NNNOOZZMDNN8DO?$ZZOO8IDDOM... .
42
+ ....8878+8ZONNNN.DDOZ$..DDNDOIMI7888=8OOO+....
43
+ ..MO88O8O=8D88..8DOOO..DNN8$.Z88Z=O8Z=DIZ.. .
44
+ ..DO8ZO8Z$D=D8..ZD8Z. .NDNDM.?O8OZ$?8$8Z8....
45
+ . ..DIDDD8OO78D. .Z8DD...,DND$..8OOD87D$$IM=...
46
+ ...8DDO7OI$D$MO. .OND8,...NDD.. .M88I~N7DIDI..
47
+ ...DZDMOZ$O88.....DDODM...DDZ.....NZ77O?Z?88...
48
+ ...8D~D7D8ZO: ....DDNDDM..DDD7......=O8D$DDOD...
49
+ ..DOD8888.... ..8NNDD,.ZDD87... ...DOZN+87... .
50
+ ...... ......... ,8DND8Z.D$MOO...... ...,O.$$...
51
+ . ......... ....DNDDN+NDN8+8,... . ........
52
+ . . . ...DDDDD.MDN,7Z$. . ...
53
+ ....DDND:.MDMM.~Z....... ..
54
+ ......D8DDN..8DOZNIZ.........
55
+ ... ... .7Z8DDDDDI,.NOD=O7.... ..
56
+ .. ..........:$$$NDNN87?MNI?IM..........
57
+ . ...........~D=?D$ODNNZD8=Z7=~++++++... .
58
+ ..?????????8ZO7$Z8NNDN$O$?=++++++++....
59
+ ...???????II$ZOZ7O+7OD$=OOI$=+++++++,...
60
+ ...I+++++??I+D7O$8$?IM7:88=OO?++++++?..
61
+ ...7++++++???$ZZZZZZ$Z$Z77~~+I+++++++:
62
+ .....?+++++??I++8DO8DON87+,+7O$?++++++++...
63
+ .....?+++++++??++?++?$ODZ$8$O$$Z=+++++++...
64
+
65
+ Introduction
66
+ ------------
67
+
68
+ Pazuzu controls *applications*. An application consists of *workers*. Each worker has a name and a command line. Each worker may have multiple *instances* of itself running.
69
+
70
+ For example, a typical application may consist of:
71
+
72
+ * A Rails app, running under some web server like Thin or Unicorn.
73
+ * A WebSockets app listening on a socket.
74
+ * Some queue-processing daemons.
75
+ * Some maintenance jobs that run now and then.
76
+
77
+ Applications are configured through a simple text format:
78
+
79
+ <key>:<command line>
80
+
81
+ For example:
82
+
83
+ main: bundle exec unicorn -c config/unicorn.rb config.ru
84
+ queue_processor: bundle exec ruby app/queue_processor.rb
85
+ web_socket_server: node lib/server.js
86
+
87
+ Pazuzu will control the lifecycle of each worker. Main points:
88
+
89
+ * Workers can be started and stopped by issuing appropriate commands.
90
+ * Workers will be monitored and respawned if they crash.
91
+ * If the desired number of workers is changed, new worker instances will be spawned or terminated as needed.
92
+ * Recent output from workers is kept so that it can be viewed.
93
+ * If Pazuzu crashes, workers will continue to run, and will be reattached when Pazuzu is restarted.
94
+
95
+ Pazuzu leaves the following to external tools:
96
+
97
+ * Resource limiting. Use the Linux cgroups tools to limit CPU and memory usage.
98
+ * Scaling. Use external tools to monitor load, and invoke the `pazuzu` command to change the number of workers.
99
+
100
+ Installation
101
+ ------------
102
+
103
+ gem install pazuzu
104
+
105
+ Requirements
106
+ ------------
107
+
108
+ * Ruby 1.9.1 or later.
109
+ * Linux 2.6.24 or later with [cgroups](http://www.kernel.org/doc/Documentation/cgroups/) enabled in the kernel configuration.
110
+ * The [libcg](http://libcg.git.sourceforge.net/) binaries, which include `cgcreate` and `cgexec`.
111
+
112
+ Getting started
113
+ ---------------
114
+
115
+ Create a minimal configuration `/etc/pazuzu/pazuzu.conf`:
116
+
117
+ applications:
118
+ myapp:
119
+ procfile: /srv/myapp
120
+
121
+ Create a minimal /srv/myapp/Procfile:
122
+
123
+ myworker: sleep 1h
124
+
125
+ Start Pazuzu (non-daemonized):
126
+
127
+ sudo pazuzud
128
+
129
+ Configuration file
130
+ ------------------
131
+
132
+ `log_path`: Set the log file. Specify `syslog` to use syslog. Defaults to stderr.
133
+
134
+ `socket_path`: Where to create a socket for accepting commands. Defaults to `/var/run/pazuzud.socket`.
135
+
136
+ `include`: Include another configuration file or a number of files. May be either a single file name/glob pattern or an array of file names/glob patterns (eg., `/etc/pazuzu/conf.d/*.conf`). The included files's keys are merged into the current configuration in the order of inclusion.
137
+
138
+ `cgroups`: Has the following sub-keys:
139
+
140
+ * `hiearchy_root`: Root of hierarchy for all cgroups created. Defaults to `pazuzu`.
141
+
142
+ * `subsystems`: An array of cgroups subsystem to attach each cgroup to. Defaults to `memory`, `cpu`, `cpuacct` and `blkio` (essentially all subsystems at the time of writing).
143
+
144
+ * `fs_root`: Where the cgroups file system is mounted. Defaults to `/sys/fs/cgroup`, which may or may not be the default for your Linux distribution.
145
+
146
+ `applications`: This is where applications are listed. Each application has its own key. For example:
147
+
148
+ applications:
149
+ foo:
150
+ procfile: /srv/foo
151
+
152
+ Each application has the following sub-keys:
153
+
154
+ * `procfile` (*required*): The path to the procfile. If a directory, it's assumed that it contains a file named `Procfile`.
155
+ * `user`: User name or UID to run the process as. Defaults to the user that Pazuzu runs as.
156
+ * `group`: Group name or GID to run the process as. Defaults to the group that Pazuzu runs as.
157
+ * `workers`: This key allows per-worker overrides. Each worker has its own key, which is the name specified in the `Procfile`.
158
+
159
+ For example:
160
+
161
+ applications:
162
+ foo:
163
+ procfile: /srv/foo
164
+ workers:
165
+ bar:
166
+ num_instances: 2
167
+
168
+ Each worker has the following sub-keys:
169
+
170
+ * `command_line`: Override the worker's command line.
171
+ * `num_instances`: The number of instances to run. Defaults to 1.
@@ -0,0 +1,21 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc 'Bump version'
4
+ task :bump do
5
+ if `git status -uno -s --porcelain | wc -l`.to_i > 0
6
+ abort "You have uncommitted changed."
7
+ end
8
+ text = File.read('lib/pazuzu/version.rb')
9
+ if text =~ /VERSION = '(.*)'/
10
+ old_version = $1
11
+ version_parts = old_version.split('.')
12
+ version_parts[-1] = version_parts[-1].to_i + 1
13
+ new_version = version_parts.join('.')
14
+ text.gsub!(/VERSION = '(.*)'/, "VERSION = '#{new_version}'")
15
+ File.open('lib/pazuzu/version.rb', 'w') { |f| f << text }
16
+ (system("git add lib/pazuzu/version.rb") and
17
+ system("git commit -m 'Bump to #{new_version}.'")) or abort "Failed to commit."
18
+ else
19
+ abort "Could not find version number"
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pazuzu'
4
+
5
+ controller = Pazuzu::CommandLine::Controller.new
6
+ controller.run!(ARGV)
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pazuzu'
4
+
5
+ runner = Pazuzu::SupervisorRunner.new
6
+ runner.run!(ARGV)
@@ -0,0 +1,37 @@
1
+ require 'set'
2
+ require 'optparse'
3
+ require 'timeout'
4
+ require 'fileutils'
5
+ require 'yaml'
6
+ require 'singleton'
7
+ require 'logger'
8
+ require 'delegate'
9
+ require 'etc'
10
+ require 'thread'
11
+ require 'socket'
12
+
13
+ require 'statemachine'
14
+ require 'active_support/core_ext/hash'
15
+ require 'yajl/json_gem'
16
+
17
+ require 'pazuzu/utility/annotated_logger'
18
+ require 'pazuzu/utility/rate_limiter'
19
+ require 'pazuzu/utility/process_spawning'
20
+ require 'pazuzu/utility/runnable'
21
+ require 'pazuzu/utility/runnable_pool'
22
+ require 'pazuzu/utility/output_tailer'
23
+
24
+ require 'pazuzu/control/socket_server'
25
+ require 'pazuzu/control/socket_client'
26
+ require 'pazuzu/control/protocol'
27
+
28
+ require 'pazuzu/command_line/controller'
29
+
30
+ require 'pazuzu/supervisor'
31
+ require 'pazuzu/procfiles'
32
+ require 'pazuzu/application'
33
+ require 'pazuzu/instance'
34
+ require 'pazuzu/worker'
35
+ require 'pazuzu/supervisor_runner'
36
+ require 'pazuzu/version'
37
+ require 'pazuzu/cgroup'
@@ -0,0 +1,114 @@
1
+ module Pazuzu
2
+
3
+ class Application
4
+
5
+ include Utility::Runnable
6
+
7
+ def initialize(supervisor, name)
8
+ super()
9
+ @supervisor = supervisor
10
+ @name = name
11
+ @logger = Utility::AnnotatedLogger.new(supervisor.logger, qname)
12
+ @workers = Utility::RunnablePool.new
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def configure!(procfile_path, configuration)
17
+ procfile_path = Procfiles.normalize_procfile_path(procfile_path)
18
+
19
+ root_path = File.dirname(procfile_path)
20
+
21
+ @user = configuration['user']
22
+ @group = configuration['group']
23
+
24
+ workers_config = configuration['workers'] || {}
25
+
26
+ leftover_workers = @workers.children.dup
27
+
28
+ procfile = YAML.load(File.open(procfile_path))
29
+ procfile = {} unless procfile.is_a?(Hash)
30
+ procfile.each do |name, command_line|
31
+ name = name.to_s
32
+
33
+ worker_config = workers_config[name] || {}
34
+
35
+ worker = @workers.children.select { |w| w.name == name }.first
36
+ if worker
37
+ leftover_workers.delete(worker)
38
+ else
39
+ @logger.info "Adding worker #{name}"
40
+ worker = Worker.new(self, name)
41
+ end
42
+ worker.configure!(root_path, command_line, worker_config)
43
+ @workers.register(worker)
44
+ end
45
+
46
+ leftover_workers.each do |worker|
47
+ @logger.info "Removing worker #{worker.name}"
48
+ @workers.unregister(worker)
49
+ end
50
+ end
51
+
52
+ def qname
53
+ @name
54
+ end
55
+
56
+ # Call to set up a forked process according to the application's
57
+ # configuration.
58
+ def setup_spawned_process!
59
+ Utility::ProcessSpawning.set_user_and_group!(@user, @group)
60
+ Utility::ProcessSpawning.prepare_child_process!
61
+ end
62
+
63
+ def workers
64
+ @workers.children
65
+ end
66
+
67
+ def log_entries
68
+ entries = @workers.children.map(&:log_entries)
69
+ entries.flatten!(1)
70
+ entries.sort_by! { |(source, time, message)| time }
71
+ entries
72
+ end
73
+
74
+ attr_reader :name
75
+ attr_reader :supervisor
76
+ attr_reader :user
77
+ attr_reader :group
78
+
79
+ private
80
+
81
+ def on_starting
82
+ @logger.info "Starting"
83
+ @monitor_thread ||= Thread.start { monitor_workers }
84
+ @workers.start!
85
+ runnable_state.started!
86
+ end
87
+
88
+ def on_started
89
+ @logger.info "Started"
90
+ end
91
+
92
+ def on_stopping
93
+ @logger.info "Stopping"
94
+ @workers.stop!
95
+ end
96
+
97
+ def monitor_workers
98
+ while [:starting, :running, :stopping].include?(run_state)
99
+ if runnable_state.state == :stopping
100
+ unless @workers.children.any? { |w| w.run_state == :running }
101
+ runnable_state.stopped!
102
+ end
103
+ end
104
+ sleep(1)
105
+ end
106
+ rescue Exception => e
107
+ @logger.error(e.message)
108
+ ensure
109
+ @monitor_thread = nil
110
+ end
111
+
112
+ end
113
+
114
+ end
@@ -0,0 +1,73 @@
1
+ module Pazuzu
2
+
3
+ class Cgroup
4
+
5
+ class CgroupCreationFailed < Exception; end
6
+
7
+ class << self
8
+ # Are cgroups available?
9
+ def available?
10
+ File.exist?('/proc/cgroups')
11
+ end
12
+ end
13
+
14
+ def initialize(fs_root_path, path, subsystems)
15
+ @fs_root_path = fs_root_path
16
+ @path = path
17
+ @subsystems = subsystems
18
+ end
19
+
20
+ def mounted?
21
+ return @subsystems.any? { |subsystem|
22
+ Dir.glob(File.join(@fs_root_path, subsystem, @path, "cgroup.procs")).any? { |file_name|
23
+ File.exists?(file_name)
24
+ }
25
+ }
26
+ end
27
+
28
+ def exec(command)
29
+ unless system("cgcreate -g #{@subsystems.join(',')}:#{@path}")
30
+ raise CgroupCreationFailed, "Could not create cgroup hiearchy #{@path}"
31
+ end
32
+ command = "cgexec -g #{@subsystems.join(',')}:#{@path} #{command}"
33
+ return Process.exec(command)
34
+ end
35
+
36
+ def pids
37
+ pids = pids_including_children
38
+ pids.reject { |pid| pids.include?(parent_pid_for(pid)) }
39
+ end
40
+
41
+ attr_reader :path
42
+
43
+ private
44
+
45
+ def pids_including_children
46
+ return @subsystems.flat_map { |subsystem|
47
+ Dir.glob(File.join(@fs_root_path, subsystem, @path, "cgroup.procs")).flat_map { |file_name|
48
+ begin
49
+ File.read(file_name).split("\n").map { |s| Integer(s) }
50
+ rescue Errno::ENOENT
51
+ nil
52
+ end
53
+ }
54
+ }.compact.tap(&:uniq!)
55
+ end
56
+
57
+ def parent_pid_for(pid)
58
+ begin
59
+ File.readlines("/proc/#{pid}/status").each do |line|
60
+ return $1.to_i if line =~ /^PPid:\s*(\d+)/i
61
+ end
62
+ rescue Errno::ENOENT
63
+ end
64
+ nil
65
+ end
66
+
67
+ def subsystem_path(subsystem)
68
+ File.join(@fs_root_path, subsystem, @path)
69
+ end
70
+
71
+ end
72
+
73
+ end