daemonic 0.0.2 → 0.1.0
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.
- checksums.yaml +4 -4
- data/.gitignore +5 -1
- data/.travis.yml +8 -0
- data/{LICENSE.txt → MIT-LICENSE.txt} +1 -1
- data/README.md +108 -78
- data/Rakefile +11 -4
- data/daemonic.gemspec +7 -4
- data/examples/init-d.sh +37 -0
- data/examples/rss +41 -0
- data/features/support/env.rb +1 -0
- data/features/worker.feature +43 -0
- data/lib/daemonic.rb +40 -8
- data/lib/daemonic/cli.rb +147 -40
- data/lib/daemonic/daemon.rb +126 -0
- data/lib/daemonic/pool.rb +43 -82
- data/lib/daemonic/producer.rb +49 -0
- data/lib/daemonic/version.rb +1 -1
- metadata +68 -31
- data/bin/daemonic +0 -6
- data/lib/daemonic/configuration.rb +0 -110
- data/lib/daemonic/logging.rb +0 -14
- data/lib/daemonic/master.rb +0 -119
- data/lib/daemonic/pidfile.rb +0 -64
- data/lib/daemonic/worker.rb +0 -93
- data/test/config +0 -6
- data/test/crappy_daemon.rb +0 -1
- data/test/integration_test.rb +0 -155
- data/test/test_daemon.rb +0 -10
data/lib/daemonic.rb
CHANGED
@@ -1,17 +1,49 @@
|
|
1
|
+
require "thread"
|
2
|
+
require "optparse"
|
3
|
+
require "logger"
|
4
|
+
require "fileutils"
|
5
|
+
require "timeout"
|
6
|
+
|
1
7
|
require "daemonic/version"
|
2
|
-
require "daemonic/
|
3
|
-
require "daemonic/pool"
|
4
|
-
require "daemonic/configuration"
|
8
|
+
require "daemonic/daemon"
|
5
9
|
require "daemonic/cli"
|
10
|
+
require "daemonic/producer"
|
11
|
+
require "daemonic/pool"
|
12
|
+
|
13
|
+
Thread.abort_on_exception = true
|
6
14
|
|
7
15
|
module Daemonic
|
8
16
|
|
9
|
-
def self.
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
17
|
+
def self.run(worker, default_options = {})
|
18
|
+
command, options = CLI.new(ARGV, default_options).run
|
19
|
+
case command
|
20
|
+
when :start then start(worker, options)
|
21
|
+
when :stop then stop(options)
|
22
|
+
when :status then status(options)
|
23
|
+
when :restart then restart(worker, options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.start(worker, options)
|
28
|
+
daemon = Daemon.new(options)
|
29
|
+
daemon.start do
|
30
|
+
Producer.new(worker, options).run
|
31
|
+
end
|
14
32
|
end
|
15
33
|
|
34
|
+
def self.stop(options)
|
35
|
+
Daemon.new(options).stop
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.status(options)
|
39
|
+
Daemon.new(options).status
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.restart(worker, options)
|
43
|
+
daemon = Daemon.new(options.merge(daemonize: true))
|
44
|
+
daemon.restart do
|
45
|
+
Producer.new(worker, options).run
|
46
|
+
end
|
47
|
+
end
|
16
48
|
|
17
49
|
end
|
data/lib/daemonic/cli.rb
CHANGED
@@ -1,67 +1,174 @@
|
|
1
|
-
require 'optparse'
|
2
|
-
|
3
1
|
module Daemonic
|
4
|
-
|
2
|
+
class CLI
|
5
3
|
|
6
|
-
|
7
|
-
options = {}
|
4
|
+
COMMANDS = %w(start stop status restart help)
|
8
5
|
|
9
|
-
|
6
|
+
attr_reader :argv, :default_options
|
10
7
|
|
11
|
-
|
8
|
+
def initialize(argv, default_options = {})
|
9
|
+
@argv = argv
|
10
|
+
@default_options = default_options
|
11
|
+
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
def run
|
14
|
+
command = argv[0]
|
15
|
+
case command
|
16
|
+
when nil, "-h", "--help", "help"
|
17
|
+
help
|
18
|
+
exit
|
19
|
+
when "-v", "--version"
|
20
|
+
puts "Daemonic version #{Daemonic::VERSION}"
|
21
|
+
exit
|
22
|
+
when "start"
|
23
|
+
start
|
24
|
+
when "stop"
|
25
|
+
stop
|
26
|
+
when "status"
|
27
|
+
status
|
28
|
+
when "restart"
|
29
|
+
restart
|
30
|
+
else
|
31
|
+
puts "Unknown command #{command.inspect}."
|
32
|
+
help
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
end
|
16
36
|
|
17
|
-
|
18
|
-
|
19
|
-
end
|
37
|
+
def help
|
38
|
+
info <<-USAGE
|
20
39
|
|
21
|
-
|
22
|
-
options[:workers] = workers
|
23
|
-
end
|
40
|
+
Usage: #{program} COMMAND OPTIONS
|
24
41
|
|
25
|
-
|
26
|
-
|
27
|
-
|
42
|
+
Available commands:
|
43
|
+
* start Start the daemon
|
44
|
+
* stop Stops the daemon
|
45
|
+
* restart Stops and starts a daemonized process
|
46
|
+
* status Shows the status
|
28
47
|
|
29
|
-
|
30
|
-
|
31
|
-
|
48
|
+
To get more information about each command, run the command with --help.
|
49
|
+
|
50
|
+
Example: #{program} start --help
|
51
|
+
|
52
|
+
USAGE
|
53
|
+
end
|
54
|
+
|
55
|
+
def start
|
56
|
+
options = parse "start", log: STDOUT, concurrency: 2, daemonize: false, startup_timeout: 1
|
57
|
+
[ :start, options ]
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop
|
61
|
+
options = parse "stop", stop_timeout: 5
|
62
|
+
[ :stop, options ]
|
63
|
+
end
|
64
|
+
|
65
|
+
def status
|
66
|
+
options = parse "status"
|
67
|
+
[ :status, options ]
|
68
|
+
end
|
69
|
+
|
70
|
+
def restart
|
71
|
+
options = parse "restart", log: STDOUT, concurrency: 2, stop_timeout: 5, startup_timeout: 1
|
72
|
+
[ :restart, options ]
|
73
|
+
end
|
32
74
|
|
33
|
-
|
34
|
-
|
75
|
+
private
|
76
|
+
|
77
|
+
def program
|
78
|
+
$PROGRAM_NAME
|
79
|
+
end
|
80
|
+
|
81
|
+
def info(text)
|
82
|
+
puts text.gsub(/^ */, '')
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse(command, options = {})
|
86
|
+
|
87
|
+
optparser = OptionParser.new { |parser|
|
88
|
+
|
89
|
+
parser.banner = "Usage: #{program} #{command} OPTIONS"
|
90
|
+
|
91
|
+
parser.separator ""
|
92
|
+
parser.separator "Process options:"
|
93
|
+
|
94
|
+
parser.on "-P", "--pid LOCATION", "Where the pid file is stored (required for daemonized processes)" do |pid|
|
95
|
+
options[:pid] = pid
|
35
96
|
end
|
36
97
|
|
37
|
-
|
38
|
-
|
98
|
+
if options.has_key?(:daemonize)
|
99
|
+
parser.on "-d", "--[no-]daemonize", "Should the process be daemonized" do |daemonize|
|
100
|
+
options[:daemonize] = daemonize
|
101
|
+
end
|
39
102
|
end
|
40
103
|
|
41
|
-
|
42
|
-
options[:
|
104
|
+
if options.has_key?(:concurrency)
|
105
|
+
parser.on "-c", "--concurrency NUMBER", Integer, "How many consumer threads to spawn (default: #{options[:concurrency]})" do |concurrency|
|
106
|
+
if concurrency < 1
|
107
|
+
puts "Concurrency cannot be smaller than 1."
|
108
|
+
exit 1
|
109
|
+
end
|
110
|
+
options[:concurrency] = concurrency
|
111
|
+
end
|
43
112
|
end
|
44
113
|
|
45
|
-
|
46
|
-
|
114
|
+
|
115
|
+
if options.has_key?(:startup_timeout)
|
116
|
+
parser.on "--startup-timeout TIMEOUT", Integer, "How many seconds to wait for the process to start (default: #{options[:startup_timeout]})" do |timeout|
|
117
|
+
if timeout < 1
|
118
|
+
puts "Timeout cannot be smaller than 1."
|
119
|
+
exit 1
|
120
|
+
end
|
121
|
+
options[:startup_timeout] = timeout
|
122
|
+
end
|
47
123
|
end
|
48
124
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
125
|
+
if options.has_key?(:stop_timeout)
|
126
|
+
parser.on "--stop-timeout TIMEOUT", Integer, "How many seconds to wait for the process to stop (default: #{options[:stop_timeout]})" do |timeout|
|
127
|
+
if timeout < 1
|
128
|
+
puts "Timeout cannot be smaller than 1."
|
129
|
+
exit 1
|
130
|
+
end
|
131
|
+
options[:stop_timeout] = timeout
|
132
|
+
end
|
53
133
|
end
|
54
134
|
|
55
|
-
|
56
|
-
|
57
|
-
|
135
|
+
if options.has_key?(:log)
|
136
|
+
|
137
|
+
parser.separator ""
|
138
|
+
parser.separator "Logging options:"
|
139
|
+
|
140
|
+
parser.on "--log FILE", "Where to write the log to" do |log|
|
141
|
+
options[:log] = log
|
142
|
+
end
|
143
|
+
|
144
|
+
parser.on "--verbose", "Sets the log level to debug" do
|
145
|
+
options[:log_level] = Logger::DEBUG
|
146
|
+
end
|
147
|
+
|
148
|
+
parser.on "--log-level LEVEL", %w(debug info warn fatal), "Set the log level (default: info)" do |level|
|
149
|
+
options[:log_level] = Logger.const_get(level.upcase)
|
150
|
+
end
|
151
|
+
|
58
152
|
end
|
59
153
|
|
60
|
-
|
154
|
+
parser.separator ""
|
155
|
+
parser.separator "Common options:"
|
156
|
+
|
157
|
+
parser.on_tail("-h", "--help", "Show this message") do
|
158
|
+
puts parser
|
159
|
+
exit
|
160
|
+
end
|
61
161
|
|
62
|
-
|
162
|
+
}
|
63
163
|
|
64
|
-
|
164
|
+
begin
|
165
|
+
optparser.parse!(argv)
|
166
|
+
default_options.merge(options)
|
167
|
+
rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => error
|
168
|
+
puts error
|
169
|
+
puts optparser
|
170
|
+
exit 1
|
171
|
+
end
|
65
172
|
end
|
66
173
|
|
67
174
|
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Daemonic
|
2
|
+
class Daemon
|
3
|
+
|
4
|
+
attr_reader :options
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def start(&block)
|
11
|
+
fail ArgumentError, "No block given" if block.nil?
|
12
|
+
if options[:daemonize]
|
13
|
+
ensure_pid_specified
|
14
|
+
fork do
|
15
|
+
at_exit { cleanup_pid_file }
|
16
|
+
Process.daemon(true)
|
17
|
+
write_pid_file
|
18
|
+
block.call
|
19
|
+
end
|
20
|
+
sleep 0.1
|
21
|
+
wait_until(options.fetch(:startup_timeout) { 1 }) { !running? }
|
22
|
+
if running?
|
23
|
+
puts "The daemon started successfully"
|
24
|
+
else
|
25
|
+
puts "The daemon did not start properly"
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
else
|
29
|
+
at_exit { cleanup_pid_file }
|
30
|
+
write_pid_file
|
31
|
+
block.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def status
|
36
|
+
ensure_pid_specified
|
37
|
+
if running?
|
38
|
+
puts "Running with pid: #{pid.inspect}"
|
39
|
+
exit 0
|
40
|
+
else
|
41
|
+
puts "Not running. Pid: #{pid.inspect}"
|
42
|
+
exit 2
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def stop
|
47
|
+
ensure_pid_specified
|
48
|
+
puts "Stopping"
|
49
|
+
if running?
|
50
|
+
Process.kill("TERM", pid)
|
51
|
+
wait_until(options.fetch(:stop_timeout) { 5 }) { !running? }
|
52
|
+
if running?
|
53
|
+
puts "Couldn't shut down. Pid: #{pid}"
|
54
|
+
exit 1
|
55
|
+
else
|
56
|
+
puts "Worker shut down."
|
57
|
+
end
|
58
|
+
else
|
59
|
+
puts "Not running. Pid: #{pid.inspect}"
|
60
|
+
exit 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def restart(&block)
|
65
|
+
ensure_pid_specified
|
66
|
+
if running?
|
67
|
+
stop
|
68
|
+
start(&block)
|
69
|
+
else
|
70
|
+
puts "Not running. Starting a new worker."
|
71
|
+
cleanup_pid_file
|
72
|
+
start(&block)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def running?
|
79
|
+
return false unless pid
|
80
|
+
Process.getpgid(pid)
|
81
|
+
true
|
82
|
+
rescue Errno::ESRCH
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
def pid
|
87
|
+
File.exist?(pid_file) && Integer(File.read(pid_file).strip)
|
88
|
+
end
|
89
|
+
|
90
|
+
def cleanup_pid_file
|
91
|
+
File.unlink(pid_file) if pid_file
|
92
|
+
end
|
93
|
+
|
94
|
+
def write_pid_file
|
95
|
+
if pid_file
|
96
|
+
FileUtils.mkdir_p(File.dirname(pid_file))
|
97
|
+
File.open(pid_file, "w") { |f| f.puts Process.pid }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def pid_file
|
102
|
+
options[:pid]
|
103
|
+
end
|
104
|
+
|
105
|
+
def wait_until(timeout, &condition)
|
106
|
+
sleep 0.1
|
107
|
+
Timeout.timeout(timeout) do
|
108
|
+
until condition.call
|
109
|
+
print "."
|
110
|
+
sleep 0.1
|
111
|
+
end
|
112
|
+
end
|
113
|
+
print "\n"
|
114
|
+
rescue Timeout::Error
|
115
|
+
print "\n"
|
116
|
+
end
|
117
|
+
|
118
|
+
def ensure_pid_specified
|
119
|
+
unless pid_file
|
120
|
+
puts "No location of PID specified."
|
121
|
+
exit 1
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
data/lib/daemonic/pool.rb
CHANGED
@@ -1,104 +1,65 @@
|
|
1
|
-
|
2
|
-
require 'daemonic/worker'
|
3
|
-
|
1
|
+
# Stolen from RubyTapas by Avdi Grimm, episode 145.
|
4
2
|
module Daemonic
|
5
3
|
class Pool
|
6
|
-
include Logging
|
7
|
-
|
8
|
-
attr_reader :config
|
9
4
|
|
10
|
-
|
11
|
-
|
12
|
-
def initialize(config)
|
13
|
-
@config = config
|
14
|
-
@workers = []
|
15
|
-
reload_desired_workers
|
16
|
-
end
|
5
|
+
class StopSignal
|
17
6
|
|
18
|
-
|
19
|
-
|
20
|
-
increase if count < desired_workers
|
21
|
-
count == desired_workers
|
7
|
+
def inspect
|
8
|
+
"[STOP SIGNAL]"
|
22
9
|
end
|
23
|
-
|
24
|
-
end
|
10
|
+
alias_method :to_s, :inspect
|
25
11
|
|
26
|
-
def restart
|
27
|
-
workers.each do |worker|
|
28
|
-
worker.restart
|
29
|
-
yield worker if block_given?
|
30
|
-
end
|
31
12
|
end
|
32
13
|
|
33
|
-
|
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
|
14
|
+
STOP_SIGNAL = StopSignal.new
|
53
15
|
|
54
|
-
def
|
55
|
-
@
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
16
|
+
def initialize(thread_count, worker, logger)
|
17
|
+
@worker = worker
|
18
|
+
@jobs = SizedQueue.new(thread_count)
|
19
|
+
@logger = logger
|
20
|
+
@threads = thread_count.times.map {|worker_num|
|
21
|
+
Thread.new do
|
22
|
+
dispatch(worker_num)
|
23
|
+
end
|
24
|
+
}
|
61
25
|
end
|
62
26
|
|
63
|
-
def
|
64
|
-
@
|
65
|
-
|
27
|
+
def enqueue(job)
|
28
|
+
@logger.debug { "Enqueueing #{job.inspect}" }
|
29
|
+
@jobs.push(job)
|
66
30
|
end
|
31
|
+
alias_method :<<, :enqueue
|
67
32
|
|
68
|
-
def
|
69
|
-
|
33
|
+
def stop
|
34
|
+
@threads.size.times do
|
35
|
+
enqueue(STOP_SIGNAL)
|
36
|
+
end
|
37
|
+
@threads.each(&:join)
|
70
38
|
end
|
71
39
|
|
72
40
|
private
|
73
41
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
deadline = Time.now + timeout
|
91
|
-
until Time.now >= deadline
|
92
|
-
result = yield
|
93
|
-
if result
|
94
|
-
return
|
95
|
-
else
|
96
|
-
sleep 0.1
|
42
|
+
def dispatch(worker_num)
|
43
|
+
@logger.debug { "T#{worker_num}: Starting" }
|
44
|
+
loop do
|
45
|
+
job = @jobs.pop
|
46
|
+
if STOP_SIGNAL.equal?(job)
|
47
|
+
@logger.debug { "T#{worker_num}: Received stop signal, terminating." }
|
48
|
+
break
|
49
|
+
end
|
50
|
+
begin
|
51
|
+
@logger.debug { "T#{worker_num}: Consuming #{job.inspect}" }
|
52
|
+
@worker.consume(job)
|
53
|
+
Thread.pass
|
54
|
+
rescue Object => error
|
55
|
+
@logger.warn { "T#{worker_num}: Error while processing #{job}: #{error.class}: #{error}" }
|
56
|
+
@logger.info { error.backtrace.join("\n") }
|
57
|
+
Thread.pass
|
97
58
|
end
|
98
59
|
end
|
99
|
-
|
100
|
-
stop
|
101
|
-
exit 1
|
60
|
+
@logger.debug { "T#{worker_num}: Stopped" }
|
102
61
|
end
|
62
|
+
|
63
|
+
|
103
64
|
end
|
104
65
|
end
|