daemonizer 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|