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 ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ log/*.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in daemonic.gemspec
4
+ gemspec
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
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = "test/*_test.rb"
7
+ end
8
+
9
+ task :default => :test
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
@@ -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,14 @@
1
+ require 'forwardable'
2
+
3
+ module Daemonic
4
+ module Logging
5
+ extend Forwardable
6
+
7
+ def_delegators :logger, :info, :debug, :warn, :fatal
8
+
9
+ def logger
10
+ config.logger
11
+ end
12
+
13
+ end
14
+ 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,3 @@
1
+ module Daemonic
2
+ VERSION = "0.0.1"
3
+ 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,6 @@
1
+ --workers 5
2
+ --pid tmp/test.pid
3
+ --name daemon_test
4
+ --command "ruby test/test_daemon.rb"
5
+ --loglevel debug
6
+ --log log/test.log
@@ -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
@@ -0,0 +1,10 @@
1
+ exiting = false
2
+ trap("TERM") { exiting = true }
3
+ trap("INT") { exiting = true }
4
+ trap("HUP") { puts "HUP HOLLAND HUP" }
5
+
6
+ $PROGRAM_NAME = "test_daemon.rb daemon_test worker##{ENV["DAEMON_WORKER_NUMBER"]}"
7
+
8
+ until exiting
9
+ sleep 0.1
10
+ end
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