daemonic 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|