Skip to content

Commit

Permalink
Add daemonize option to CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
bensheldon committed Jan 22, 2021
1 parent 2a9132b commit e9be407
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,16 @@ Options:
[--max-threads=COUNT] # Maximum number of threads to use for working jobs. (env var: GOOD_JOB_MAX_THREADS, default: 5)
[--queues=QUEUE_LIST] # Queues to work from. (env var: GOOD_JOB_QUEUES, default: *)
[--poll-interval=SECONDS] # Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 1)
[--daemonize] # Run as a background daemon (default: false)
[--pidfile=PIDFILE] # Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)
Executes queued jobs.
All options can be configured with environment variables.
See option descriptions for the matching environment variable name.
== Configuring queues
Separate multiple queues with commas; exclude queues with a leading minus;
separate isolated execution pools with semicolons and threads with colons.
```
Expand Down
16 changes: 15 additions & 1 deletion lib/good_job/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class CLI < Thor
# Requiring this loads the application's configuration and classes.
RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")

# @!visibility private
def self.exit_on_failure?
true
end

# @!macro thor.desc
# @!method $1
# @return [void]
Expand All @@ -27,7 +32,8 @@ class CLI < Thor
See option descriptions for the matching environment variable name.
== Configuring queues
\x5Separate multiple queues with commas; exclude queues with a leading minus;
Separate multiple queues with commas; exclude queues with a leading minus;
separate isolated execution pools with semicolons and threads with colons.
DESCRIPTION
Expand All @@ -43,10 +49,18 @@ class CLI < Thor
type: :numeric,
banner: 'SECONDS',
desc: "Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 5)"
method_option :daemonize,
type: :boolean,
desc: "Run as a background daemon (default: false)"
method_option :pidfile,
type: :string,
desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)"
def start
set_up_application!
configuration = GoodJob::Configuration.new(options)

Daemon.new(pidfile: configuration.pidfile).daemonize if configuration.daemonize?

notifier = GoodJob::Notifier.new
poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
scheduler = GoodJob::Scheduler.from_configuration(configuration)
Expand Down
16 changes: 15 additions & 1 deletion lib/good_job/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def poll_interval

# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
# @return [Boolean]
# @return [Integer]
def cleanup_preserved_jobs_before_seconds_ago
(
options[:before_seconds_ago] ||
Expand All @@ -118,6 +118,20 @@ def cleanup_preserved_jobs_before_seconds_ago
).to_i
end

# Tests whether to daemonize the process.
# @return [Boolean]
def daemonize?
options[:daemonize] || false
end

# Path of the pidfile to create when running as a daemon.
# @return [Pathname,String]
def pidfile
options[:pidfile] ||
env['GOOD_JOB_PIDFILE'] ||
Rails.application.root.join('tmp', 'pids', 'good_job.pid')
end

private

def rails_config
Expand Down
59 changes: 59 additions & 0 deletions lib/good_job/daemon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module GoodJob
#
# Manages daemonization of the current process.
#
class Daemon
# The path of the generated pidfile.
# @return [Pathname,String]
attr_reader :pidfile

# @param pidfile [Pathname,String] Pidfile path
def initialize(pidfile:)
@pidfile = pidfile
end

# Daemonizes the current process and writes out a pidfile.
def daemonize
check_pid
Process.daemon
write_pid
end

private

def write_pid
File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(Process.pid.to_s) }
at_exit { File.delete(pidfile) if File.exist?(pidfile) }
rescue Errno::EEXIST
check_pid
retry
end

def delete_pid
File.delete(pidfile) if File.exist?(pidfile)
end

def check_pid
case pid_status(pidfile)
when :running, :not_owned
abort "A server is already running. Check #{pidfile}"
when :dead
File.delete(pidfile)
end
end

def pid_status(pidfile)
return :exited unless File.exist?(pidfile)

pid = ::File.read(pidfile).to_i
return :dead if pid.zero?

Process.kill(0, pid) # check process status
:running
rescue Errno::ESRCH
:dead
rescue Errno::EPERM
:not_owned
end
end
end
39 changes: 39 additions & 0 deletions spec/lib/good_job/daemon_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require 'rails_helper'

RSpec.describe GoodJob::Daemon, skip_if_java: true do
let(:pidfile) { Rails.application.root.join('tmp/pidfile.pid') }
let(:daemon) { described_class.new(pidfile: pidfile) }

before do
FileUtils.mkdir_p Rails.application.root.join('tmp')
allow(Process).to receive(:daemon)
allow(daemon).to receive(:at_exit)
end

after do
File.delete(pidfile)
end

describe '#daemonize' do
it 'calls Process.daemon' do
daemon.daemonize
expect(Process).to have_received :daemon
end

it 'writes a pidfile' do
expect do
daemon.daemonize
end.to change { Pathname.new(pidfile).exist? }.from(false).to(true)
end

context 'when a pidfile already exists' do
before do
File.open(pidfile, "w") { |f| f.write(Process.pid) }
end

it 'aborts with a message' do
expect { daemon.daemonize }.to output("A server is already running. Check #{pidfile}\n").to_stderr.and raise_error SystemExit
end
end
end
end

0 comments on commit e9be407

Please sign in to comment.