daemonic 0.0.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 +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +100 -0
- data/Rakefile +9 -0
- data/bin/daemonic +14 -0
- data/daemonic.gemspec +23 -0
- data/lib/daemonic/cli.rb +68 -0
- data/lib/daemonic/configuration.rb +114 -0
- data/lib/daemonic/logging.rb +14 -0
- data/lib/daemonic/master.rb +119 -0
- data/lib/daemonic/pidfile.rb +64 -0
- data/lib/daemonic/pool.rb +104 -0
- data/lib/daemonic/version.rb +3 -0
- data/lib/daemonic/worker.rb +93 -0
- data/lib/daemonic.rb +17 -0
- data/test/config +6 -0
- data/test/crappy_daemon.rb +1 -0
- data/test/integration_test.rb +138 -0
- data/test/test_daemon.rb +10 -0
- metadata +103 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 TODO: Write your name
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# Daemonic
|
2
|
+
|
3
|
+
Daemonic is a tool that let's you turn your long running processes into daemons.
|
4
|
+
|
5
|
+
At a glance:
|
6
|
+
|
7
|
+
* Designed to be external to and independent of your app.
|
8
|
+
* Possibility to spawn multiple instances of your app.
|
9
|
+
* Smart restart behavior, comparable to Unicorn.
|
10
|
+
* Automatically restarts crashed instances of your app.
|
11
|
+
|
12
|
+
### What can Daemonic do for me?
|
13
|
+
|
14
|
+
Say you have written your own daemon. It processes messages of a queue for
|
15
|
+
example. You can start it by running `ruby work.rb`. Now the only thing you
|
16
|
+
need to do is turn it into a proper daemon. This means daemonizing your
|
17
|
+
process, managing restarts, adding concurrency, managing a pid file, etc.
|
18
|
+
|
19
|
+
This is a lot of work, quite error prone and frankly, very tedious. That's
|
20
|
+
where Daemonic comes in. All you need to do, to start your message processor is:
|
21
|
+
|
22
|
+
```
|
23
|
+
$ daemonic --command "ruby work.rb" --pid work.pid --workers 5 --daemonize
|
24
|
+
```
|
25
|
+
|
26
|
+
And voila: you now have 5 instances of your worker, each in it's own process.
|
27
|
+
This works just like Unicorn, but with any kind of process, not just Rack apps.
|
28
|
+
You can restart all workers by sending the `USR2` signal, just like in Unicorn
|
29
|
+
too.
|
30
|
+
|
31
|
+
```
|
32
|
+
$ kill -USR2 `cat work.pid`
|
33
|
+
```
|
34
|
+
|
35
|
+
This will restart each process individually, meaning you never lose capacity.
|
36
|
+
|
37
|
+
There are some caveats though. For one, daemonic checks if the process is
|
38
|
+
running, but it is very naive. It doesn't really know if the app is ready, just
|
39
|
+
if it is running or not.
|
40
|
+
|
41
|
+
Secondly, if your app needs a shared resource, spawning multiple workers will
|
42
|
+
not work. This is the case with web servers that need to bind to a port.
|
43
|
+
|
44
|
+
Also, your own worker needs to respond to the `TERM` and `HUP` signals.
|
45
|
+
Daemonic expects the app to shut down after sending the `TERM` signal. It's up
|
46
|
+
to you to finish up what you are doing.
|
47
|
+
|
48
|
+
## Installation
|
49
|
+
|
50
|
+
You don't need to add daemonic to your Gemfile. You can simply install it and
|
51
|
+
use it straight away.
|
52
|
+
|
53
|
+
```
|
54
|
+
gem install daemonic
|
55
|
+
```
|
56
|
+
|
57
|
+
## Usage
|
58
|
+
|
59
|
+
Daemonic accepts command line options and can also read from a configuration
|
60
|
+
file. The configuration file contains the same options as you would pass to the
|
61
|
+
command line. Here is an example:
|
62
|
+
|
63
|
+
```
|
64
|
+
--command "ruby work.rb"
|
65
|
+
--workers 5
|
66
|
+
--pid tmp/worker.pid
|
67
|
+
--name my-worker
|
68
|
+
--log log/development.log
|
69
|
+
--daemonize
|
70
|
+
```
|
71
|
+
|
72
|
+
Then you can run Daemonic with the `config` option:
|
73
|
+
|
74
|
+
```
|
75
|
+
$ daemonic --config my-worker.conf
|
76
|
+
```
|
77
|
+
|
78
|
+
There are a bunch more options. See `daemonic --help` for the full list.
|
79
|
+
|
80
|
+
### Signals
|
81
|
+
|
82
|
+
These are the signals daemonic responds to:
|
83
|
+
|
84
|
+
* `TERM`/`INT` shuts down all workers and then the master
|
85
|
+
* `HUP` will be forwarded to each worker and the master will reload the config file
|
86
|
+
* `USR2` will restart each worker, but not the master
|
87
|
+
* `TTIN` increase the number of workers by one
|
88
|
+
* `TTOU` decrease the number of workers by one
|
89
|
+
|
90
|
+
## Contributing
|
91
|
+
|
92
|
+
1. Fork it
|
93
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
94
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
95
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
96
|
+
5. Create new Pull Request
|
97
|
+
|
98
|
+
## TODO
|
99
|
+
|
100
|
+
* Ability to create init script like behavior.
|
data/Rakefile
ADDED
data/bin/daemonic
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
|
3
|
+
|
4
|
+
require 'daemonic'
|
5
|
+
|
6
|
+
config = Daemonic.configuration(ARGV, Dir.pwd)
|
7
|
+
|
8
|
+
config.reload
|
9
|
+
|
10
|
+
if config.daemonize?
|
11
|
+
Process.daemon
|
12
|
+
end
|
13
|
+
|
14
|
+
Daemonic.spawn(config)
|
data/daemonic.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'daemonic/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "daemonic"
|
8
|
+
spec.version = Daemonic::VERSION
|
9
|
+
spec.authors = ["iain"]
|
10
|
+
spec.email = ["iain@iain.nl"]
|
11
|
+
spec.description = %q{Manages daemonizing your workers with basic monitoring and restart behavior.}
|
12
|
+
spec.summary = %q{Manages daemonizing your workers with basic monitoring and restart behavior.}
|
13
|
+
spec.homepage = "https://github.com/yourkarma/daemonic"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
data/lib/daemonic/cli.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Daemonic
|
4
|
+
module CLI
|
5
|
+
|
6
|
+
def self.parse(args)
|
7
|
+
options = {}
|
8
|
+
|
9
|
+
parser = OptionParser.new do |opts|
|
10
|
+
|
11
|
+
opts.banner = "Usage: #{$0} options"
|
12
|
+
|
13
|
+
opts.on("--command COMMAND", "The command to start") do |command|
|
14
|
+
options[:command] = command
|
15
|
+
end
|
16
|
+
|
17
|
+
opts.on("--[no-]daemonize", "Start process in background") do |daemonize|
|
18
|
+
options[:daemonize] = daemonize
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("--workers NUM", Integer, "Amount of workers (default: 1)") do |workers|
|
22
|
+
options[:workers] = workers
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on("--pid FILENAME", "Location of pid files") do |pidfile|
|
26
|
+
options[:pidfile] = pidfile
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on("--working-dir DIRECTORY", "Specify the working directory") do |dir|
|
30
|
+
options[:working_dir] = dir
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("--name NAME", "Name of the server") do |name|
|
34
|
+
options[:program_name] = name
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("--log FILENAME", "Send daemon_of_the_fall output to a file") do |logfile|
|
38
|
+
options[:logfile] = logfile
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("--loglevel LEVEL", [:debug, :info, :warn, :fatal], "Set the log level (default: info)") do |level|
|
42
|
+
options[:loglevel] = level
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("--config FILENAME", "Read settings from a file") do |config_file|
|
46
|
+
options[:config_file] = config_file
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on_tail("--version", "Shows the version") do
|
50
|
+
require "daemonic/version"
|
51
|
+
puts "#{$0}: version #{Daemonic::VERSION}"
|
52
|
+
exit 0
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on_tail("--help", "You're watching it") do
|
56
|
+
puts opts
|
57
|
+
exit 0
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
parser.parse!(args.dup)
|
63
|
+
|
64
|
+
options
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
module Daemonic
|
5
|
+
class Configuration
|
6
|
+
|
7
|
+
Invalid = Class.new(ArgumentError)
|
8
|
+
|
9
|
+
attr_reader :eventual_config, :args
|
10
|
+
|
11
|
+
def initialize(args, pwd)
|
12
|
+
@args = args
|
13
|
+
@pwd = pwd
|
14
|
+
@eventual_config = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def command
|
18
|
+
self[:command]
|
19
|
+
end
|
20
|
+
|
21
|
+
def daemonize?
|
22
|
+
self[:daemonize]
|
23
|
+
end
|
24
|
+
|
25
|
+
def config_file
|
26
|
+
self[:config_file]
|
27
|
+
end
|
28
|
+
|
29
|
+
def workers
|
30
|
+
Integer(self[:workers] || "1")
|
31
|
+
end
|
32
|
+
|
33
|
+
def reload
|
34
|
+
@eventual_config = {}
|
35
|
+
@logger = nil
|
36
|
+
load_options defaults
|
37
|
+
load_options command_line_options
|
38
|
+
load_options config_file_options if config_file
|
39
|
+
load_options command_line_options
|
40
|
+
logger.debug { to_h.inspect }
|
41
|
+
validate
|
42
|
+
end
|
43
|
+
|
44
|
+
def working_dir
|
45
|
+
self[:working_dir]
|
46
|
+
end
|
47
|
+
|
48
|
+
def program_name
|
49
|
+
self[:program_name] || File.basename(working_dir)
|
50
|
+
end
|
51
|
+
|
52
|
+
def pidfile
|
53
|
+
self[:pidfile] || File.join(working_dir, "tmp/#{program_name}.pid")
|
54
|
+
end
|
55
|
+
|
56
|
+
def logfile
|
57
|
+
self[:logfile] || STDOUT
|
58
|
+
end
|
59
|
+
|
60
|
+
def logger
|
61
|
+
@logger ||= ::Logger.new(logfile).tap { |logger|
|
62
|
+
logger.formatter = proc { |severity, datetime, progname, msg|
|
63
|
+
"[#{severity}] [#{datetime}] [#{program_name}] [#{Process.pid}] #{msg}\n"
|
64
|
+
}
|
65
|
+
logger.level = ::Logger.const_get(loglevel)
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_h
|
70
|
+
eventual_config
|
71
|
+
end
|
72
|
+
|
73
|
+
def loglevel
|
74
|
+
(self[:loglevel] || "INFO").to_s.upcase
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def [](key)
|
80
|
+
eventual_config[key.to_s]
|
81
|
+
end
|
82
|
+
|
83
|
+
def []=(key, value)
|
84
|
+
eventual_config[key.to_s] = value
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def load_options(options)
|
90
|
+
options.each do |key, value|
|
91
|
+
self[key] = value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def command_line_options
|
96
|
+
CLI.parse(args)
|
97
|
+
end
|
98
|
+
|
99
|
+
def config_file_options
|
100
|
+
contents = File.open(config_file, 'r:utf-8').read
|
101
|
+
args = Shellwords.split(contents)
|
102
|
+
CLI.parse(args)
|
103
|
+
end
|
104
|
+
|
105
|
+
def validate
|
106
|
+
raise Invalid, "No command specified" if command.nil?
|
107
|
+
end
|
108
|
+
|
109
|
+
def defaults
|
110
|
+
{ :workers => 1, :working_dir => @pwd }
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'daemonic/version'
|
3
|
+
require 'daemonic/logging'
|
4
|
+
require 'daemonic/pidfile'
|
5
|
+
|
6
|
+
Thread.abort_on_exception = true
|
7
|
+
|
8
|
+
module Daemonic
|
9
|
+
class Master
|
10
|
+
include Logging
|
11
|
+
|
12
|
+
attr_reader :config, :pidfile
|
13
|
+
|
14
|
+
def initialize(config)
|
15
|
+
@config = config
|
16
|
+
@shutting_down = false
|
17
|
+
@current_status = nil
|
18
|
+
config.reload
|
19
|
+
update_program_name("booting")
|
20
|
+
@pidfile = Pidfile.new(
|
21
|
+
pid: Process.pid,
|
22
|
+
config: config,
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
Dir.chdir(config.working_dir)
|
28
|
+
write_pidfile
|
29
|
+
at_exit { clean_pidfile }
|
30
|
+
trap_signals
|
31
|
+
start_workers
|
32
|
+
update_program_name(nil)
|
33
|
+
wait_until_done
|
34
|
+
update_program_name
|
35
|
+
fatal "Shutting down master"
|
36
|
+
end
|
37
|
+
|
38
|
+
def restart
|
39
|
+
@monitor = false
|
40
|
+
update_program_name("restarting")
|
41
|
+
pool.restart { update_program_name }
|
42
|
+
update_program_name(nil)
|
43
|
+
@monitor = true
|
44
|
+
end
|
45
|
+
|
46
|
+
def stop
|
47
|
+
@monitor = false
|
48
|
+
update_program_name("shutting down")
|
49
|
+
pool.stop { update_program_name }
|
50
|
+
@shutting_down = true
|
51
|
+
end
|
52
|
+
|
53
|
+
def hup
|
54
|
+
config.reload
|
55
|
+
pool.start
|
56
|
+
pool.hup
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def write_pidfile
|
62
|
+
pidfile.write
|
63
|
+
end
|
64
|
+
|
65
|
+
def clean_pidfile
|
66
|
+
pidfile.clean
|
67
|
+
end
|
68
|
+
|
69
|
+
def start_workers
|
70
|
+
pool.start { update_program_name }
|
71
|
+
@monitor = true
|
72
|
+
end
|
73
|
+
|
74
|
+
def pool
|
75
|
+
@pool ||= Pool.new(config)
|
76
|
+
end
|
77
|
+
|
78
|
+
def trap_signals
|
79
|
+
trap("USR2") { info "USR2 received!"; restart }
|
80
|
+
trap("TERM") { info "TERM received!"; stop }
|
81
|
+
trap("INT") { info "INT received!"; stop }
|
82
|
+
trap("HUP") { info "HUP received!"; hup }
|
83
|
+
trap("TTIN") { info "TTIN received!"; ttin }
|
84
|
+
trap("TTOU") { info "TTOU received!"; ttou }
|
85
|
+
end
|
86
|
+
|
87
|
+
def ttin
|
88
|
+
pool.increase!
|
89
|
+
end
|
90
|
+
|
91
|
+
def ttou
|
92
|
+
pool.decrease!
|
93
|
+
end
|
94
|
+
|
95
|
+
def wait_until_done
|
96
|
+
while keep_running?
|
97
|
+
pool.monitor if @monitor
|
98
|
+
update_program_name
|
99
|
+
sleep 0.1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def keep_running?
|
104
|
+
not @shutting_down
|
105
|
+
end
|
106
|
+
|
107
|
+
def update_program_name(additional = :unchanged)
|
108
|
+
@current_status = additional if additional != :unchanged
|
109
|
+
base = "#{config.program_name} master [version #{VERSION}; #{pool.count}/#{pool.desired_workers} workers]"
|
110
|
+
if @current_status
|
111
|
+
$PROGRAM_NAME = "#{base} (#{@current_status})"
|
112
|
+
else
|
113
|
+
$PROGRAM_NAME = base
|
114
|
+
end
|
115
|
+
$PROGRAM_NAME
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Daemonic
|
4
|
+
class Pidfile
|
5
|
+
include FileUtils
|
6
|
+
include Logging
|
7
|
+
|
8
|
+
attr_reader :pid, :config, :index
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@pid = options.fetch(:pid)
|
12
|
+
@config = options.fetch(:config)
|
13
|
+
@index = options[:index]
|
14
|
+
end
|
15
|
+
|
16
|
+
def write
|
17
|
+
mkdir_p(File.dirname(filename))
|
18
|
+
|
19
|
+
if File.exist?(filename)
|
20
|
+
existing_pid = File.open(filename, 'r').read.chomp
|
21
|
+
running = Process.getpgid(existing_pid) rescue false
|
22
|
+
if running
|
23
|
+
fatal "Error: PID file already exists at `#{filename}` and a process with PID `#{existing_pid}` is running"
|
24
|
+
exit 1
|
25
|
+
else
|
26
|
+
debug "Warning: cleaning up stale pid file at `#{filename}`"
|
27
|
+
write!
|
28
|
+
end
|
29
|
+
else
|
30
|
+
debug "Writing pidfile #{filename} with pid #{pid}"
|
31
|
+
write!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def clean
|
36
|
+
if File.exist?(filename)
|
37
|
+
existing_pid = File.open(filename, 'r').read.chomp
|
38
|
+
if existing_pid == pid.to_s
|
39
|
+
debug "Cleaning up my own pid file at `#{filename}`"
|
40
|
+
rm(filename)
|
41
|
+
else
|
42
|
+
debug "Pid file `#{filename}` did not belong to me, so not cleaning. I am #{Process.pid}, and the pid file says #{existing_pid}"
|
43
|
+
end
|
44
|
+
else
|
45
|
+
debug "Pid file disappeared from `#{filename}`"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def filename
|
52
|
+
if index
|
53
|
+
config.pidfile.to_s.sub(/\.([^\.]+)\z/, ".#{index}.\\1")
|
54
|
+
else
|
55
|
+
config.pidfile
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def write!
|
60
|
+
File.open(filename, 'w') { |f| f.write(pid) }
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'daemonic/logging'
|
2
|
+
require 'daemonic/worker'
|
3
|
+
|
4
|
+
module Daemonic
|
5
|
+
class Pool
|
6
|
+
include Logging
|
7
|
+
|
8
|
+
attr_reader :config
|
9
|
+
|
10
|
+
attr_reader :workers, :desired_workers
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
@config = config
|
14
|
+
@workers = []
|
15
|
+
reload_desired_workers
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
wait_for global_timeout do
|
20
|
+
increase if count < desired_workers
|
21
|
+
count == desired_workers
|
22
|
+
end
|
23
|
+
decrease while count > desired_workers
|
24
|
+
end
|
25
|
+
|
26
|
+
def restart
|
27
|
+
workers.each do |worker|
|
28
|
+
worker.restart
|
29
|
+
yield worker if block_given?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
workers.each do |worker|
|
35
|
+
worker.stop
|
36
|
+
yield worker if block_given?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def hup
|
41
|
+
reload_desired_workers
|
42
|
+
workers.each(&:hup)
|
43
|
+
start
|
44
|
+
end
|
45
|
+
|
46
|
+
def count
|
47
|
+
workers.count { |worker| worker.running? }
|
48
|
+
end
|
49
|
+
|
50
|
+
def increase
|
51
|
+
workers << start_worker(workers.size)
|
52
|
+
end
|
53
|
+
|
54
|
+
def increase!
|
55
|
+
@desired_workers += 1
|
56
|
+
increase
|
57
|
+
end
|
58
|
+
|
59
|
+
def decrease
|
60
|
+
workers.pop.stop
|
61
|
+
end
|
62
|
+
|
63
|
+
def decrease!
|
64
|
+
@desired_workers -= 1
|
65
|
+
decrease
|
66
|
+
end
|
67
|
+
|
68
|
+
def monitor
|
69
|
+
workers.each(&:monitor)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def start_worker(num)
|
75
|
+
Worker.new(
|
76
|
+
index: num,
|
77
|
+
config: config,
|
78
|
+
).tap(&:start)
|
79
|
+
end
|
80
|
+
|
81
|
+
def reload_desired_workers
|
82
|
+
@desired_workers = config.workers
|
83
|
+
end
|
84
|
+
|
85
|
+
def global_timeout
|
86
|
+
(desired_workers * 2) + 1
|
87
|
+
end
|
88
|
+
|
89
|
+
def wait_for(timeout=2)
|
90
|
+
deadline = Time.now + timeout
|
91
|
+
until Time.now >= deadline
|
92
|
+
result = yield
|
93
|
+
if result
|
94
|
+
return
|
95
|
+
else
|
96
|
+
sleep 0.1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
fatal "Unable to get to boot the right amount of workers. Running: #{count}, desired: #{desired_workers}."
|
100
|
+
stop
|
101
|
+
exit 1
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Daemonic
|
2
|
+
class Worker
|
3
|
+
include Logging
|
4
|
+
|
5
|
+
attr_reader :index, :config
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@index = options.fetch(:index)
|
9
|
+
@config = options.fetch(:config)
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
@pid = Process.spawn(
|
14
|
+
{"DAEMON_WORKER_NUMBER" => index.to_s},
|
15
|
+
*Array(config.command),
|
16
|
+
{:chdir => config.working_dir}
|
17
|
+
)
|
18
|
+
sleep 0.1
|
19
|
+
info "#{to_s} Starting."
|
20
|
+
pidfile.write
|
21
|
+
wait_for { running? }
|
22
|
+
info "#{to_s} Started!"
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop
|
26
|
+
Process.kill("TERM", pid)
|
27
|
+
info "#{to_s} Stopping."
|
28
|
+
Process.waitpid(pid)
|
29
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
30
|
+
warn "#{to_s} Already stopped."
|
31
|
+
ensure
|
32
|
+
pidfile.clean
|
33
|
+
@pid = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def restart
|
37
|
+
info "#{to_s} Restarting."
|
38
|
+
stop
|
39
|
+
start
|
40
|
+
end
|
41
|
+
|
42
|
+
def hup
|
43
|
+
Process.kill("HUP", pid)
|
44
|
+
end
|
45
|
+
|
46
|
+
def monitor
|
47
|
+
if not running?
|
48
|
+
warn "#{to_s} Not running."
|
49
|
+
start
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def running?
|
54
|
+
if @pid
|
55
|
+
!Process.waitpid(pid, Process::WNOHANG) rescue false
|
56
|
+
else
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def pid
|
64
|
+
@pid || raise("unknown pid")
|
65
|
+
end
|
66
|
+
|
67
|
+
def pidfile
|
68
|
+
Pidfile.new(
|
69
|
+
pid: pid,
|
70
|
+
config: config,
|
71
|
+
index: index,
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def wait_for(timeout=2)
|
76
|
+
deadline = Time.now + timeout
|
77
|
+
until Time.now >= deadline
|
78
|
+
result = yield
|
79
|
+
if result
|
80
|
+
return
|
81
|
+
else
|
82
|
+
sleep 0.1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
warn "#{to_s} It took too long to start."
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_s
|
89
|
+
"<Worker:#{index} pid:#{@pid.inspect} running:#{running?.inspect}>"
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
data/lib/daemonic.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "daemonic/version"
|
2
|
+
require "daemonic/master"
|
3
|
+
require "daemonic/pool"
|
4
|
+
require "daemonic/configuration"
|
5
|
+
require "daemonic/cli"
|
6
|
+
|
7
|
+
module Daemonic
|
8
|
+
|
9
|
+
def self.spawn(*args)
|
10
|
+
Master.new(*args).start
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.configuration(*args)
|
14
|
+
Configuration.new(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
data/test/config
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
exit 1
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
class TestIntegration < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
def test_integration
|
7
|
+
master_pid = daemonic %Q|--workers 5 --pid tmp/test.pid --name daemon-test --command "ruby test/test_daemon.rb" --log #{LOGFILE}|
|
8
|
+
perform_tests(master_pid)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_with_config_file
|
12
|
+
master_pid = daemonic %Q|--config test/config|
|
13
|
+
perform_tests(master_pid)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_with_failing_daemon
|
17
|
+
master_pid = daemonic %Q|--pid tmp/test.pid --name daemon-test --command "ruby test/crappy_daemon.rb" --log #{LOGFILE}|
|
18
|
+
sleep 0.2
|
19
|
+
wait_for(10) { not running? master_pid }
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup
|
23
|
+
FileUtils.mkdir_p(File.dirname(LOGFILE))
|
24
|
+
FileUtils.rm(LOGFILE) if File.exist?(LOGFILE)
|
25
|
+
end
|
26
|
+
|
27
|
+
def teardown
|
28
|
+
spawned_commands.each { |pid| Process.kill("TERM", pid) }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def running?(pid)
|
34
|
+
Process.getpgid(pid) rescue false
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform_tests(master_pid)
|
38
|
+
|
39
|
+
pids_of_workers = []
|
40
|
+
wait_for {
|
41
|
+
pids_of_workers = find_worker_pids
|
42
|
+
pids_of_workers.size == 5
|
43
|
+
}
|
44
|
+
|
45
|
+
sleep 1
|
46
|
+
|
47
|
+
# restart
|
48
|
+
signal "USR2", master_pid
|
49
|
+
|
50
|
+
sleep 1
|
51
|
+
|
52
|
+
wait_for {
|
53
|
+
new_pids_of_workers = find_worker_pids
|
54
|
+
all_are_different?(new_pids_of_workers, pids_of_workers)
|
55
|
+
}
|
56
|
+
assert_equal 5, find_worker_pids.size
|
57
|
+
|
58
|
+
# master pid file
|
59
|
+
file = "tmp/test.pid"
|
60
|
+
assert File.exist?(file), "Missing master pidfile at #{file}"
|
61
|
+
|
62
|
+
# worker pid files
|
63
|
+
(0...5).each do |num|
|
64
|
+
file = "tmp/test.#{num}.pid"
|
65
|
+
assert File.exist?(file), "Missing worker pidfile at #{file}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# increase workers on the fly
|
69
|
+
signal "TTIN", master_pid
|
70
|
+
assert_equal 6, find_worker_pids.size
|
71
|
+
|
72
|
+
# decrease workers on the fly
|
73
|
+
signal "TTOU", master_pid
|
74
|
+
assert_equal 5, find_worker_pids.size
|
75
|
+
|
76
|
+
# monitor crashing workers
|
77
|
+
pid_to_kill = find_worker_pids.first
|
78
|
+
signal "TERM", pid_to_kill
|
79
|
+
sleep 0.2
|
80
|
+
assert_equal 5, find_worker_pids.size
|
81
|
+
|
82
|
+
# shutdown
|
83
|
+
signal "TERM", master_pid
|
84
|
+
workers_remaining = []
|
85
|
+
wait_for {
|
86
|
+
workers_remaining = find_worker_pids
|
87
|
+
workers_remaining.size == 0
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
SCRIPT_NAME = "test_daemon.rb"
|
92
|
+
LOGFILE = "log/test.log"
|
93
|
+
|
94
|
+
def spawned_commands
|
95
|
+
@spawned_commands ||= []
|
96
|
+
end
|
97
|
+
|
98
|
+
def find_worker_pids
|
99
|
+
find_pids_for(SCRIPT_NAME)
|
100
|
+
end
|
101
|
+
|
102
|
+
def daemonic(command)
|
103
|
+
cmd = "./bin/daemonic #{command}"
|
104
|
+
puts "\n#{cmd}"
|
105
|
+
pid = spawn(cmd)
|
106
|
+
spawned_commands << pid
|
107
|
+
pid
|
108
|
+
end
|
109
|
+
|
110
|
+
def find_pids_for(filter)
|
111
|
+
processes = `ps u`.split("\n")
|
112
|
+
found = processes.select { |line| line.include?(filter) }
|
113
|
+
found.map { |line| line.split(/\s+/, 4)[1].to_i }
|
114
|
+
end
|
115
|
+
|
116
|
+
def signal(type, pid)
|
117
|
+
Process.kill(type, pid)
|
118
|
+
sleep 0.1
|
119
|
+
end
|
120
|
+
|
121
|
+
def wait_for(timeout=10)
|
122
|
+
deadline = Time.now + timeout
|
123
|
+
until Time.now >= deadline
|
124
|
+
result = yield
|
125
|
+
if result
|
126
|
+
return
|
127
|
+
else
|
128
|
+
sleep 0.1
|
129
|
+
end
|
130
|
+
end
|
131
|
+
raise "Timeout expired, running now: #{find_worker_pids.inspect}"
|
132
|
+
end
|
133
|
+
|
134
|
+
def all_are_different?(one, two)
|
135
|
+
(one & two).empty?
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
data/test/test_daemon.rb
ADDED
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: daemonic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- iain
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-06-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Manages daemonizing your workers with basic monitoring and restart behavior.
|
47
|
+
email:
|
48
|
+
- iain@iain.nl
|
49
|
+
executables:
|
50
|
+
- daemonic
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE.txt
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- bin/daemonic
|
60
|
+
- daemonic.gemspec
|
61
|
+
- lib/daemonic.rb
|
62
|
+
- lib/daemonic/cli.rb
|
63
|
+
- lib/daemonic/configuration.rb
|
64
|
+
- lib/daemonic/logging.rb
|
65
|
+
- lib/daemonic/master.rb
|
66
|
+
- lib/daemonic/pidfile.rb
|
67
|
+
- lib/daemonic/pool.rb
|
68
|
+
- lib/daemonic/version.rb
|
69
|
+
- lib/daemonic/worker.rb
|
70
|
+
- test/config
|
71
|
+
- test/crappy_daemon.rb
|
72
|
+
- test/integration_test.rb
|
73
|
+
- test/test_daemon.rb
|
74
|
+
homepage: https://github.com/yourkarma/daemonic
|
75
|
+
licenses:
|
76
|
+
- MIT
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 1.8.23
|
96
|
+
signing_key:
|
97
|
+
specification_version: 3
|
98
|
+
summary: Manages daemonizing your workers with basic monitoring and restart behavior.
|
99
|
+
test_files:
|
100
|
+
- test/config
|
101
|
+
- test/crappy_daemon.rb
|
102
|
+
- test/integration_test.rb
|
103
|
+
- test/test_daemon.rb
|