daemonizer 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +48 -0
- data/Rakefile +20 -2
- data/VERSION +1 -1
- data/bin/daemonizer +8 -0
- data/daemonizer.gemspec +57 -0
- data/lib/daemonizer.rb +21 -0
- data/lib/daemonizer/autoload.rb +16 -0
- data/lib/daemonizer/cli.rb +115 -0
- data/lib/daemonizer/config.rb +65 -0
- data/lib/daemonizer/daemonize.rb +67 -0
- data/lib/daemonizer/dsl.rb +58 -0
- data/lib/daemonizer/engine.rb +61 -0
- data/lib/daemonizer/errors.rb +6 -0
- data/lib/daemonizer/process_manager.rb +94 -0
- data/lib/daemonizer/worker.rb +96 -0
- data/lib/daemonizer/worker_pool.rb +52 -0
- metadata +38 -10
data/README
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
engine :fork
|
2
|
+
workers 2
|
3
|
+
|
4
|
+
pool :daemonizer do
|
5
|
+
workers 4
|
6
|
+
poll_period 5
|
7
|
+
log_file "log/daemonizer.log" #relative to Demfile
|
8
|
+
|
9
|
+
before_init do |logger, block|
|
10
|
+
block.call
|
11
|
+
end
|
12
|
+
|
13
|
+
after_init do |logger, worker_id, workers_count|
|
14
|
+
logger.info "Started #{worker_id} from #{workers_count}"
|
15
|
+
|
16
|
+
exit = false
|
17
|
+
|
18
|
+
stop = proc {
|
19
|
+
exit = true
|
20
|
+
}
|
21
|
+
|
22
|
+
trap('TERM', stop)
|
23
|
+
trap('INT', stop)
|
24
|
+
trap('EXIT', stop)
|
25
|
+
|
26
|
+
loop do
|
27
|
+
break if exit
|
28
|
+
logger.info "Ping #{worker_id}"
|
29
|
+
sleep 10
|
30
|
+
end
|
31
|
+
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
pool :new_daemonizer do
|
37
|
+
|
38
|
+
before_init do |logger, block|
|
39
|
+
logger.info "New: Preparing master process"
|
40
|
+
block.call
|
41
|
+
end
|
42
|
+
|
43
|
+
after_init do |logger, worker_id, workers_count|
|
44
|
+
logger.info "Started #{worker_id}/#{workers_count}"
|
45
|
+
sleep 1
|
46
|
+
logger.info "Stopped #{worker_id}/#{workers_count}"
|
47
|
+
end
|
48
|
+
end
|
data/Rakefile
CHANGED
@@ -1,14 +1,32 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rubygems/specification'
|
3
|
+
|
1
4
|
begin
|
2
5
|
require 'jeweler'
|
3
6
|
Jeweler::Tasks.new do |gemspec|
|
4
7
|
gemspec.name = "daemonizer"
|
5
8
|
gemspec.summary = "Daemonizer allows you to easily create custom daemons on ruby. Supporting preforked and threaded models."
|
6
|
-
gemspec.description = "Inspired by bundler. Mostly
|
9
|
+
gemspec.description = "Inspired by bundler and rack. Mostly built on top of Alexey Kovyrin's loops code. http://github.com/kovyrin/loops"
|
7
10
|
gemspec.email = "glebpom@gmail.com"
|
8
11
|
gemspec.homepage = "http://github.com/glebpom/daemonizer"
|
9
12
|
gemspec.authors = ["Gleb Pomykalov"]
|
13
|
+
gemspec.add_dependency('log4r', '>= 1.1.8')
|
10
14
|
end
|
11
15
|
Jeweler::GemcutterTasks.new
|
12
16
|
rescue LoadError
|
13
17
|
puts "Jeweler not available. Install it with: gem install jeweler"
|
14
|
-
end
|
18
|
+
end
|
19
|
+
|
20
|
+
begin
|
21
|
+
require 'yard'
|
22
|
+
YARD::Rake::YardocTask.new(:yard) do |t|
|
23
|
+
t.options = ['--title', 'Daemonizer Documentation']
|
24
|
+
if ENV['PRIVATE']
|
25
|
+
t.options.concat ['--protected', '--private']
|
26
|
+
else
|
27
|
+
t.options.concat ['--protected', '--no-private']
|
28
|
+
end
|
29
|
+
end
|
30
|
+
rescue LoadError
|
31
|
+
puts 'Yard not available. Install it with: sudo gem install yard'
|
32
|
+
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/bin/daemonizer
ADDED
data/daemonizer.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{daemonizer}
|
8
|
+
s.version = "0.0.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Gleb Pomykalov"]
|
12
|
+
s.date = %q{2010-07-06}
|
13
|
+
s.default_executable = %q{daemonizer}
|
14
|
+
s.description = %q{Inspired by bundler and rack. Mostly built on top of Alexey Kovyrin's loops code. http://github.com/kovyrin/loops}
|
15
|
+
s.email = %q{glebpom@gmail.com}
|
16
|
+
s.executables = ["daemonizer"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"README"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
"README",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"bin/daemonizer",
|
25
|
+
"daemonizer.gemspec",
|
26
|
+
"lib/daemonizer.rb",
|
27
|
+
"lib/daemonizer/autoload.rb",
|
28
|
+
"lib/daemonizer/cli.rb",
|
29
|
+
"lib/daemonizer/config.rb",
|
30
|
+
"lib/daemonizer/daemonize.rb",
|
31
|
+
"lib/daemonizer/dsl.rb",
|
32
|
+
"lib/daemonizer/engine.rb",
|
33
|
+
"lib/daemonizer/errors.rb",
|
34
|
+
"lib/daemonizer/process_manager.rb",
|
35
|
+
"lib/daemonizer/worker.rb",
|
36
|
+
"lib/daemonizer/worker_pool.rb"
|
37
|
+
]
|
38
|
+
s.homepage = %q{http://github.com/glebpom/daemonizer}
|
39
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
40
|
+
s.require_paths = ["lib"]
|
41
|
+
s.rubygems_version = %q{1.3.7}
|
42
|
+
s.summary = %q{Daemonizer allows you to easily create custom daemons on ruby. Supporting preforked and threaded models.}
|
43
|
+
|
44
|
+
if s.respond_to? :specification_version then
|
45
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
46
|
+
s.specification_version = 3
|
47
|
+
|
48
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
49
|
+
s.add_runtime_dependency(%q<log4r>, [">= 1.1.8"])
|
50
|
+
else
|
51
|
+
s.add_dependency(%q<log4r>, [">= 1.1.8"])
|
52
|
+
end
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<log4r>, [">= 1.1.8"])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
data/lib/daemonizer.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'yaml'
|
3
|
+
require 'erb'
|
4
|
+
require 'pathname'
|
5
|
+
require 'log4r'
|
6
|
+
|
7
|
+
include Log4r
|
8
|
+
|
9
|
+
module Daemonizer
|
10
|
+
|
11
|
+
def self.root=(value)
|
12
|
+
@@root = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.root
|
16
|
+
@@root
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
require File.dirname(__FILE__) + '/../lib/daemonizer/autoload'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Daemonizer
|
2
|
+
# @private
|
3
|
+
def self.__p(*path) File.join(File.dirname(File.expand_path(__FILE__)), *path) end
|
4
|
+
|
5
|
+
autoload :Config, __p('config')
|
6
|
+
autoload :Dsl, __p('dsl')
|
7
|
+
autoload :Errors, __p('errors')
|
8
|
+
autoload :CLI, __p('cli')
|
9
|
+
autoload :Daemonize, __p('daemonize')
|
10
|
+
autoload :Engine, __p('engine')
|
11
|
+
autoload :Worker, __p('worker')
|
12
|
+
autoload :WorkerPool, __p('worker_pool')
|
13
|
+
autoload :ProcessManager, __p('process_manager')
|
14
|
+
|
15
|
+
include Errors
|
16
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'rubygems/config_file'
|
3
|
+
|
4
|
+
module Daemonizer
|
5
|
+
class CLI < Thor
|
6
|
+
check_unknown_options!
|
7
|
+
|
8
|
+
def initialize(*)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "start", "Start pool"
|
13
|
+
method_option :demfile, :type => :string, :aliases => "-D", :banner => "Path to Demfile"
|
14
|
+
def start(pool_name = nil)
|
15
|
+
control_pools_loop(pool_name, "successfully started", options[:demfile]) do |pool|
|
16
|
+
# Pid file check
|
17
|
+
if Daemonize.check_pid(pool.pid_file)
|
18
|
+
print_pool pool.name, "Can't start, another process exists!"
|
19
|
+
exit(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
print_pool pool.name, "Starting pool"
|
23
|
+
|
24
|
+
app_name = "#{pool.name} monitor\0"
|
25
|
+
|
26
|
+
Daemonize.daemonize(app_name)
|
27
|
+
|
28
|
+
Dir.chdir(Daemonizer.root) # Make sure we're in the working directory
|
29
|
+
|
30
|
+
# Pid file creation
|
31
|
+
Daemonize.create_pid(pool.pid_file)
|
32
|
+
|
33
|
+
# Workers processing
|
34
|
+
engine = Engine.new(pool)
|
35
|
+
engine.start!
|
36
|
+
|
37
|
+
# Workers exited, cleaning up
|
38
|
+
File.delete(pool.pid_file) rescue nil
|
39
|
+
end
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "stop", "Stop pool"
|
44
|
+
method_option :demfile, :type => :string, :aliases => "-D", :banner => "Path to Demfile"
|
45
|
+
def stop(pool_name = nil)
|
46
|
+
control_pools_loop(pool_name, "successfully stoped", options[:demfile]) do |pool|
|
47
|
+
STDOUT.sync = true
|
48
|
+
unless Daemonize.check_pid(pool.pid_file)
|
49
|
+
print_pool pool.name, "No pid file or a stale pid file!"
|
50
|
+
exit 1
|
51
|
+
end
|
52
|
+
|
53
|
+
pid = Daemonize.read_pid(pool.pid_file)
|
54
|
+
print_pool pool.name, "Killing the process: #{pid}: "
|
55
|
+
|
56
|
+
loop do
|
57
|
+
Process.kill('SIGTERM', pid)
|
58
|
+
sleep(1)
|
59
|
+
break unless Daemonize.check_pid(pool.pid_file)
|
60
|
+
end
|
61
|
+
|
62
|
+
print_pool pool.name, " Done!"
|
63
|
+
exit(0)
|
64
|
+
end
|
65
|
+
return true
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "restart", "Restart pool"
|
69
|
+
def restart(pool_name = nil)
|
70
|
+
invoke :stop, pool_name
|
71
|
+
invoke :start, pool_name
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "status", "Print pool status"
|
75
|
+
def status(pool_name = nil)
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def control_pools_loop(pool_name, message = nil, demfile = nil, &block)
|
81
|
+
find_pools(pool_name, demfile).each do |pool|
|
82
|
+
Process.fork do
|
83
|
+
yield(pool)
|
84
|
+
end
|
85
|
+
Process.wait
|
86
|
+
if $?.exitstatus == 0 and message
|
87
|
+
print_pool pool.name, message
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def find_pools(pool_name, demfile)
|
93
|
+
demfile_name = demfile || "Demfile"
|
94
|
+
|
95
|
+
Daemonizer.root = File.dirname(File.expand_path(demfile_name))
|
96
|
+
|
97
|
+
pools = Dsl.evaluate(demfile_name)
|
98
|
+
|
99
|
+
if pool_name
|
100
|
+
if pool = pools[pool_name.to_sym]
|
101
|
+
[pool]
|
102
|
+
else
|
103
|
+
print_pool pool_name, "Pool with name `#{pool_name}` is not configured"
|
104
|
+
[]
|
105
|
+
end
|
106
|
+
else
|
107
|
+
pools.values
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def print_pool(pool_name, message)
|
112
|
+
puts "#{pool_name}: #{message}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Daemonizer
|
2
|
+
class Config
|
3
|
+
class ConfigError < StandardError; end
|
4
|
+
|
5
|
+
attr_reader :pool
|
6
|
+
|
7
|
+
def initialize(pool, options)
|
8
|
+
@pool = pool
|
9
|
+
@options = options
|
10
|
+
init_defaults
|
11
|
+
init_logger
|
12
|
+
validate
|
13
|
+
end
|
14
|
+
|
15
|
+
def init_logger
|
16
|
+
@logger = Logger.new @pool.to_s
|
17
|
+
outputter = FileOutputter.new('log', :filename => self.log_file)
|
18
|
+
outputter.formatter = PatternFormatter.new :pattern => "%d - %l %g - %m"
|
19
|
+
@logger.outputters = outputter
|
20
|
+
@logger.level = INFO
|
21
|
+
GDC.set "#{Process.pid}/monitor"
|
22
|
+
end
|
23
|
+
|
24
|
+
def init_defaults
|
25
|
+
@options[:before_init] ||= Proc.new
|
26
|
+
@options[:engine] ||= :fork
|
27
|
+
@options[:workers] ||= 1
|
28
|
+
@options[:log_file] ||= "log/#{@pool}.log"
|
29
|
+
@options[:poll_period] ||= 5
|
30
|
+
@options[:pid_file] ||= "pid/#{@pool}.pid"
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate
|
34
|
+
Daemonizer.report_fatal_error "Workers count should be more then zero", @logger if @options[:workers] < 1
|
35
|
+
Daemonizer.report_fatal_error "Engine #{@options[:engine]} is not known", @logger unless [:fork, :thread].include?(@options[:engine])
|
36
|
+
if @options[:before_init]
|
37
|
+
Daemonizer.report_fatal_error "before_init should have block", @logger unless @options[:before_init].is_a?(Proc)
|
38
|
+
end
|
39
|
+
Daemonizer.report_fatal_error "after_init should be set", @logger if @options[:after_init].nil?
|
40
|
+
Daemonizer.report_fatal_error "after_init should have block", @logger unless @options[:after_init].is_a?(Proc)
|
41
|
+
Daemonizer.report_fatal_error "Poll period should be more then zero", @logger if @options[:poll_period] < 1
|
42
|
+
end
|
43
|
+
|
44
|
+
[:before_init, :engine, :workers, :after_init, :poll_period, :root].each do |method|
|
45
|
+
define_method method do
|
46
|
+
@options[method.to_sym]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
[:log_file, :pid_file].each do |method|
|
51
|
+
define_method method do
|
52
|
+
File.join(Daemonizer.root, @options[method.to_sym])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def name
|
57
|
+
@pool
|
58
|
+
end
|
59
|
+
|
60
|
+
def logger
|
61
|
+
@logger
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Daemonizer
|
2
|
+
module Daemonize
|
3
|
+
def self.read_pid(pid_file)
|
4
|
+
File.open(pid_file) do |f|
|
5
|
+
f.gets.to_i
|
6
|
+
end
|
7
|
+
rescue Errno::ENOENT
|
8
|
+
0
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.check_pid(pid_file)
|
12
|
+
pid = read_pid(pid_file)
|
13
|
+
return false if pid.zero?
|
14
|
+
if defined?(::JRuby)
|
15
|
+
system "kill -0 #{pid} &> /dev/null"
|
16
|
+
return $? == 0
|
17
|
+
else
|
18
|
+
Process.kill(0, pid)
|
19
|
+
end
|
20
|
+
true
|
21
|
+
rescue Errno::ESRCH, Errno::ECHILD, Errno::EPERM
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.create_pid(pid_file)
|
26
|
+
if File.exist?(pid_file)
|
27
|
+
puts "Pid file #{pid_file} exists! Checking the process..."
|
28
|
+
if check_pid(pid_file)
|
29
|
+
puts "Can't create new pid file because another process is runnig!"
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
puts "Stale pid file! Removing..."
|
33
|
+
File.delete(pid_file)
|
34
|
+
end
|
35
|
+
|
36
|
+
File.open(pid_file, 'w') do |f|
|
37
|
+
f.puts(Process.pid)
|
38
|
+
end
|
39
|
+
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.daemonize(app_name)
|
44
|
+
if defined?(::JRuby)
|
45
|
+
puts "WARNING: daemonize method is not implemented for JRuby (yet), please consider using nohup."
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
fork && exit # Fork and exit from the parent
|
50
|
+
|
51
|
+
# Detach from the controlling terminal
|
52
|
+
unless sess_id = Process.setsid
|
53
|
+
raise Daemons.RuntimeException.new('cannot detach from controlling terminal')
|
54
|
+
end
|
55
|
+
|
56
|
+
# Prevent the possibility of acquiring a controlling terminal
|
57
|
+
trap 'SIGHUP', 'IGNORE'
|
58
|
+
exit if pid = fork
|
59
|
+
|
60
|
+
$0 = app_name if app_name
|
61
|
+
|
62
|
+
File.umask(0000) # Insure sensible umask
|
63
|
+
|
64
|
+
return sess_id
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Daemonizer
|
2
|
+
class DslError < StandardError; end
|
3
|
+
|
4
|
+
class Dsl
|
5
|
+
def self.evaluate(gemfile)
|
6
|
+
builder = new
|
7
|
+
builder.instance_eval(File.read(gemfile.to_s), gemfile.to_s, 1)
|
8
|
+
builder.instance_variable_get("@configs")
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@source = nil
|
13
|
+
@options = {}
|
14
|
+
@pool = :default
|
15
|
+
@configs = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def poll_period(seconds)
|
19
|
+
@options[:poll_period] = seconds.to_i
|
20
|
+
end
|
21
|
+
|
22
|
+
def log_file(log)
|
23
|
+
@options[:log_file] = log
|
24
|
+
end
|
25
|
+
|
26
|
+
def workers(num)
|
27
|
+
@options[:workers] = num.to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def engine(name)
|
31
|
+
@options[:engine] = name.to_sym
|
32
|
+
end
|
33
|
+
|
34
|
+
def before_init(&blk)
|
35
|
+
@options[:before_init] = blk
|
36
|
+
end
|
37
|
+
|
38
|
+
def after_init(&blk)
|
39
|
+
@options[:after_init] = blk
|
40
|
+
end
|
41
|
+
|
42
|
+
def pid_file(pid)
|
43
|
+
@options[:pid_file] = pid
|
44
|
+
end
|
45
|
+
|
46
|
+
def pool(name, &blk)
|
47
|
+
@pool = name.to_sym
|
48
|
+
options = @options.dup
|
49
|
+
yield
|
50
|
+
@configs[@pool] = Config.new(@pool, @options)
|
51
|
+
rescue Config::ConfigError => e
|
52
|
+
puts "* Error in pool \"#{@pool}\": #{e.to_s}. Skipping..."
|
53
|
+
ensure
|
54
|
+
@options = options
|
55
|
+
@pool = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Daemonizer
|
2
|
+
class Engine
|
3
|
+
attr_reader :config
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def logger
|
10
|
+
@config.logger
|
11
|
+
end
|
12
|
+
|
13
|
+
def start!
|
14
|
+
@pm = ProcessManager.new(@config)
|
15
|
+
|
16
|
+
init_block = Proc.new do
|
17
|
+
@pm.start_workers do |process_id|
|
18
|
+
@config.after_init.call(logger, process_id, @config.workers)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
@config.before_init.call(@config.logger, init_block)
|
23
|
+
# Start monitoring loop
|
24
|
+
|
25
|
+
setup_signals
|
26
|
+
@pm.monitor_workers
|
27
|
+
end
|
28
|
+
|
29
|
+
=begin
|
30
|
+
def debug_loop!(loop_name)
|
31
|
+
@pm = Daemonizer::ProcessManager.new(global_config, Daemonizer.logger)
|
32
|
+
loop_config = loops_config[loop_name] || {}
|
33
|
+
|
34
|
+
# Adjust loop config values before starting it in debug mode
|
35
|
+
loop_config['workers_number'] = 1
|
36
|
+
loop_config['debug_loop'] = true
|
37
|
+
|
38
|
+
# Load loop class
|
39
|
+
unless klass = load_loop_class(loop_name, loop_config)
|
40
|
+
puts "Can't load loop class!"
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
|
44
|
+
# Start the loop
|
45
|
+
start_loop(loop_name, klass, loop_config)
|
46
|
+
end
|
47
|
+
=end
|
48
|
+
private
|
49
|
+
def setup_signals
|
50
|
+
stop = proc {
|
51
|
+
@config.logger.info "Received a signal... stopping..."
|
52
|
+
@pm.start_shutdown!
|
53
|
+
}
|
54
|
+
|
55
|
+
trap('TERM', stop)
|
56
|
+
trap('INT', stop)
|
57
|
+
trap('EXIT', stop)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Daemonizer
|
2
|
+
class ProcessManager
|
3
|
+
def initialize(config)
|
4
|
+
@worker_pools = {}
|
5
|
+
@shutdown = false
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def logger
|
10
|
+
@config.logger
|
11
|
+
end
|
12
|
+
|
13
|
+
def start_workers(&blk)
|
14
|
+
raise ArgumentError, "Need a worker block!" unless block_given?
|
15
|
+
|
16
|
+
@worker_pools[@config.pool] = WorkerPool.new(@config.pool, self, @config.engine, &blk)
|
17
|
+
@worker_pools[@config.pool].start_workers(@config.workers)
|
18
|
+
end
|
19
|
+
|
20
|
+
def monitor_workers
|
21
|
+
setup_signals
|
22
|
+
|
23
|
+
logger.debug 'Starting workers monitoring code...'
|
24
|
+
loop do
|
25
|
+
logger.debug "Checking workers' health..."
|
26
|
+
@worker_pools.each do |name, pool|
|
27
|
+
break if shutdown?
|
28
|
+
pool.check_workers
|
29
|
+
end
|
30
|
+
|
31
|
+
break if shutdown?
|
32
|
+
logger.debug "Sleeping for #{@config.poll_period} seconds..."
|
33
|
+
sleep(@config.poll_period)
|
34
|
+
end
|
35
|
+
ensure
|
36
|
+
logger.debug "Workers monitoring loop is finished, starting shutdown..."
|
37
|
+
# Send out stop signals
|
38
|
+
stop_workers(false)
|
39
|
+
|
40
|
+
# Wait for all the workers to die
|
41
|
+
unless wait_for_workers(10)
|
42
|
+
logger.warn "Some workers are still alive after 10 seconds of waiting. Killing them..."
|
43
|
+
stop_workers(true)
|
44
|
+
wait_for_workers(5)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def setup_signals
|
49
|
+
# Zombie reapers
|
50
|
+
trap('CHLD') {}
|
51
|
+
trap('EXIT') {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def wait_for_workers(seconds)
|
55
|
+
seconds.times do
|
56
|
+
logger.debug "Shutting down... waiting for workers to die (we have #{seconds} seconds)..."
|
57
|
+
running_total = 0
|
58
|
+
|
59
|
+
@worker_pools.each do |name, pool|
|
60
|
+
running_total += pool.wait_workers
|
61
|
+
end
|
62
|
+
|
63
|
+
if running_total.zero?
|
64
|
+
logger.debug "All workers are dead. Exiting..."
|
65
|
+
return true
|
66
|
+
end
|
67
|
+
|
68
|
+
logger.debug "#{running_total} workers are still running! Sleeping for a second..."
|
69
|
+
sleep(1)
|
70
|
+
end
|
71
|
+
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
|
75
|
+
def stop_workers(force = false)
|
76
|
+
# Set shutdown flag
|
77
|
+
logger.debug "Stopping workers#{force ? ' (forced)' : ''}..."
|
78
|
+
|
79
|
+
# Termination loop
|
80
|
+
@worker_pools.each do |name, pool|
|
81
|
+
pool.stop_workers(force)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def shutdown?
|
86
|
+
@shutdown
|
87
|
+
end
|
88
|
+
|
89
|
+
def start_shutdown!
|
90
|
+
logger.debug "Starting shutdown (shutdown flag set)..."
|
91
|
+
@shutdown = true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Daemonizer
|
2
|
+
class Worker
|
3
|
+
attr_reader :name
|
4
|
+
attr_reader :pid
|
5
|
+
|
6
|
+
def initialize(name, pm, engine, worker_id, &blk)
|
7
|
+
raise ArgumentError, "Need a worker block!" unless block_given?
|
8
|
+
|
9
|
+
@name = name
|
10
|
+
@pm = pm
|
11
|
+
@engine = engine.to_s
|
12
|
+
@worker_block = blk
|
13
|
+
@worker_id = worker_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger
|
17
|
+
@pm.logger
|
18
|
+
end
|
19
|
+
|
20
|
+
def shutdown?
|
21
|
+
@pm.shutdown?
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
return if shutdown?
|
26
|
+
if @engine == 'fork'
|
27
|
+
@pid = Kernel.fork do
|
28
|
+
@pid = Process.pid
|
29
|
+
GDC.set "#{@pid}/#{@worker_id}"
|
30
|
+
normal_exit = false
|
31
|
+
begin
|
32
|
+
$0 = "#{@name} worker: instance #{@worker_id}\0"
|
33
|
+
@worker_block.call(@worker_id)
|
34
|
+
normal_exit = true
|
35
|
+
exit(0)
|
36
|
+
rescue Exception => e
|
37
|
+
message = SystemExit === e ? "exit(#{e.status})" : e.to_s
|
38
|
+
if SystemExit === e and e.success?
|
39
|
+
if normal_exit
|
40
|
+
logger.info("Worker finished: normal return")
|
41
|
+
else
|
42
|
+
logger.error("Worker exited: #{message} at #{e.backtrace.first}")
|
43
|
+
end
|
44
|
+
else
|
45
|
+
logger.error("Worker exited with error: #{message}\n #{e.backtrace.join("\n ")}")
|
46
|
+
end
|
47
|
+
logger.debug("Terminating #{@name} worker: #{@pid}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
elsif @engine == 'thread'
|
51
|
+
@thread = Thread.start do
|
52
|
+
@worker_block.call
|
53
|
+
end
|
54
|
+
else
|
55
|
+
raise ArgumentError, "Invalid engine name: #{@engine}"
|
56
|
+
end
|
57
|
+
rescue Exception => e
|
58
|
+
logger.error("Exception from worker: #{e} at #{e.backtrace.first}")
|
59
|
+
end
|
60
|
+
|
61
|
+
def running?(verbose = false)
|
62
|
+
if @engine == 'fork'
|
63
|
+
return false unless @pid
|
64
|
+
begin
|
65
|
+
Process.waitpid(@pid, Process::WNOHANG)
|
66
|
+
res = Process.kill(0, @pid)
|
67
|
+
logger.debug("KILL(#{@pid}) = #{res}") if verbose
|
68
|
+
return true
|
69
|
+
rescue Errno::ESRCH, Errno::ECHILD, Errno::EPERM => e
|
70
|
+
logger.error("Exception from kill: #{e} at #{e.backtrace.first}") if verbose
|
71
|
+
return false
|
72
|
+
end
|
73
|
+
elsif @engine == 'thread'
|
74
|
+
@thread && @thread.alive?
|
75
|
+
else
|
76
|
+
raise ArgumentError, "Invalid engine name: #{@engine}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def stop(force = false)
|
81
|
+
if @engine == 'fork'
|
82
|
+
begin
|
83
|
+
sig = force ? 'SIGKILL' : 'SIGTERM'
|
84
|
+
logger.debug("Sending #{sig} to ##{@pid}")
|
85
|
+
Process.kill(sig, @pid)
|
86
|
+
rescue Errno::ESRCH, Errno::ECHILD, Errno::EPERM=> e
|
87
|
+
logger.error("Exception from kill: #{e} at #{e.backtrace.first}")
|
88
|
+
end
|
89
|
+
elsif @engine == 'thread'
|
90
|
+
force && !defined?(::JRuby) ? @thread.kill! : @thread.kill
|
91
|
+
else
|
92
|
+
raise ArgumentError, "Invalid engine name: #{@engine}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Daemonizer
|
2
|
+
class WorkerPool
|
3
|
+
attr_reader :name, :logger
|
4
|
+
|
5
|
+
def initialize(name, pm, engine, &blk)
|
6
|
+
@name = name
|
7
|
+
@pm = pm
|
8
|
+
@worker_block = blk
|
9
|
+
@engine = engine
|
10
|
+
@workers = []
|
11
|
+
@logger = @pm.logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def shutdown?
|
15
|
+
@pm.shutdown?
|
16
|
+
end
|
17
|
+
|
18
|
+
def start_workers(number)
|
19
|
+
logger.debug "Creating #{number} workers for #{name} pool..."
|
20
|
+
number.times do |i|
|
21
|
+
@workers << Worker.new(name, @pm, @engine, i+1, &@worker_block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def check_workers
|
26
|
+
logger.debug "Checking loop #{name} workers..."
|
27
|
+
@workers.each do |worker|
|
28
|
+
next if worker.running? || worker.shutdown?
|
29
|
+
logger.warn "Worker #{worker.name} is not running. Restart!"
|
30
|
+
worker.run
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def wait_workers
|
35
|
+
running = 0
|
36
|
+
@workers.each do |worker|
|
37
|
+
next unless worker.running?
|
38
|
+
running += 1
|
39
|
+
logger.debug "Worker #{name} is still running (#{worker.pid})"
|
40
|
+
end
|
41
|
+
return running
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop_workers(force)
|
45
|
+
logger.debug "Stopping #{name} pool workers..."
|
46
|
+
@workers.each do |worker|
|
47
|
+
next unless worker.running?
|
48
|
+
worker.stop(force)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: daemonizer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Gleb Pomykalov
|
@@ -15,14 +15,29 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-06
|
19
|
-
default_executable:
|
20
|
-
dependencies:
|
21
|
-
|
22
|
-
|
18
|
+
date: 2010-07-06 00:00:00 +04:00
|
19
|
+
default_executable: daemonizer
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: log4r
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 1
|
33
|
+
- 8
|
34
|
+
version: 1.1.8
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
description: Inspired by bundler and rack. Mostly built on top of Alexey Kovyrin's loops code. http://github.com/kovyrin/loops
|
23
38
|
email: glebpom@gmail.com
|
24
|
-
executables:
|
25
|
-
|
39
|
+
executables:
|
40
|
+
- daemonizer
|
26
41
|
extensions: []
|
27
42
|
|
28
43
|
extra_rdoc_files:
|
@@ -31,6 +46,19 @@ files:
|
|
31
46
|
- README
|
32
47
|
- Rakefile
|
33
48
|
- VERSION
|
49
|
+
- bin/daemonizer
|
50
|
+
- daemonizer.gemspec
|
51
|
+
- lib/daemonizer.rb
|
52
|
+
- lib/daemonizer/autoload.rb
|
53
|
+
- lib/daemonizer/cli.rb
|
54
|
+
- lib/daemonizer/config.rb
|
55
|
+
- lib/daemonizer/daemonize.rb
|
56
|
+
- lib/daemonizer/dsl.rb
|
57
|
+
- lib/daemonizer/engine.rb
|
58
|
+
- lib/daemonizer/errors.rb
|
59
|
+
- lib/daemonizer/process_manager.rb
|
60
|
+
- lib/daemonizer/worker.rb
|
61
|
+
- lib/daemonizer/worker_pool.rb
|
34
62
|
has_rdoc: true
|
35
63
|
homepage: http://github.com/glebpom/daemonizer
|
36
64
|
licenses: []
|