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.
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