daemonic 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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/master"
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.start(options)
10
- config = Configuration.new(options, Dir.pwd)
11
- config.reload
12
- Process.daemon if config.daemonize?
13
- Master.new(config).start
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
- module CLI
2
+ class CLI
5
3
 
6
- def self.parse(args)
7
- options = {}
4
+ COMMANDS = %w(start stop status restart help)
8
5
 
9
- parser = OptionParser.new do |opts|
6
+ attr_reader :argv, :default_options
10
7
 
11
- opts.banner = "Usage: #{$0} options"
8
+ def initialize(argv, default_options = {})
9
+ @argv = argv
10
+ @default_options = default_options
11
+ end
12
12
 
13
- opts.on("--command COMMAND", "The command to start") do |command|
14
- options[:command] = command
15
- end
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
- opts.on("--[no-]daemonize", "Start process in background") do |daemonize|
18
- options[:daemonize] = daemonize
19
- end
37
+ def help
38
+ info <<-USAGE
20
39
 
21
- opts.on("--workers NUM", Integer, "Amount of workers (default: 1)") do |workers|
22
- options[:workers] = workers
23
- end
40
+ Usage: #{program} COMMAND OPTIONS
24
41
 
25
- opts.on("--pid FILENAME", "Location of pid files") do |pidfile|
26
- options[:pidfile] = pidfile
27
- end
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
- opts.on("--working-dir DIRECTORY", "Specify the working directory") do |dir|
30
- options[:working_dir] = dir
31
- end
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
- opts.on("--name NAME", "Name of the server") do |name|
34
- options[:program_name] = name
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
- opts.on("--log FILENAME", "Send daemon_of_the_fall output to a file") do |logfile|
38
- options[:logfile] = logfile
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
- opts.on("--loglevel LEVEL", [:debug, :info, :warn, :fatal], "Set the log level (default: info)") do |level|
42
- options[:loglevel] = level
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
- opts.on("--config FILENAME", "Read settings from a file") do |config_file|
46
- options[:config_file] = config_file
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
- opts.on_tail("--version", "Shows the version") do
50
- require "daemonic/version"
51
- puts "#{$0}: version #{Daemonic::VERSION}"
52
- exit 0
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
- opts.on_tail("--help", "You're watching it") do
56
- puts opts
57
- exit 0
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
- end
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
- parser.parse!(args.dup)
162
+ }
63
163
 
64
- options
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
- require 'daemonic/logging'
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
- attr_reader :workers, :desired_workers
11
-
12
- def initialize(config)
13
- @config = config
14
- @workers = []
15
- reload_desired_workers
16
- end
5
+ class StopSignal
17
6
 
18
- def start
19
- wait_for global_timeout do
20
- increase if count < desired_workers
21
- count == desired_workers
7
+ def inspect
8
+ "[STOP SIGNAL]"
22
9
  end
23
- decrease while count > desired_workers
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
- 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
14
+ STOP_SIGNAL = StopSignal.new
53
15
 
54
- def increase!
55
- @desired_workers += 1
56
- increase
57
- end
58
-
59
- def decrease
60
- workers.pop.stop
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 decrease!
64
- @desired_workers -= 1
65
- decrease
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 monitor
69
- workers.each(&:monitor)
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 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
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
- fatal "Unable to get to boot the right amount of workers. Running: #{count}, desired: #{desired_workers}."
100
- stop
101
- exit 1
60
+ @logger.debug { "T#{worker_num}: Stopped" }
102
61
  end
62
+
63
+
103
64
  end
104
65
  end