feat: extend cli with write_pidfile and remove_pidfile
This commit is contained in:
1 parent
c3ff02a741
commit
7478aeadf2
1 file changed
+192
@@ -206,6 +206,59 @@ module Prism
|
||||
worker = Jobs::Worker.new(
|
||||
queue,
|
||||
queue_name: options[:queue_name],
|
||||
error_handler: lambda do |error, job|
|
||||
warn "[prism jobs] job #{job[:id]} (#{job[:job_class]}) failed: #{error.message}"
|
||||
end
|
||||
)
|
||||
|
||||
running = true
|
||||
trap('INT') { running = false }
|
||||
trap('TERM') { running = false }
|
||||
|
||||
cycles = 0
|
||||
loop do
|
||||
processed = worker.work_off
|
||||
out.puts "[prism jobs] processed #{processed} job(s)" if options[:verbose] && processed.positive?
|
||||
cycles += 1
|
||||
break if options[:once]
|
||||
break if options[:max_cycles] && cycles >= options[:max_cycles]
|
||||
break unless running
|
||||
sleep(options[:interval])
|
||||
break unless running
|
||||
end
|
||||
cycles
|
||||
ensure
|
||||
remove_pidfile(pidfile) if pidfile
|
||||
end
|
||||
|
||||
# Refuses to start a second daemon against the same pidfile while a
|
||||
# process recorded in it is still alive (mirrors the classic
|
||||
# pidfile-lock pattern used by unicorn/sidekiq); a pidfile left behind
|
||||
# by a process that crashed without cleaning up (stale pid, nothing
|
||||
# answers Process.kill(0, ...)) is silently reclaimed instead of
|
||||
# blocking every future start forever.
|
||||
def write_pidfile(pidfile)
|
||||
if File.exist?(pidfile)
|
||||
existing_pid = File.read(pidfile).strip.to_i
|
||||
if existing_pid.positive? && process_alive?(existing_pid)
|
||||
raise UsageError, "jobs worker already running (pid #{existing_pid}, pidfile #{pidfile})"
|
||||
end
|
||||
end
|
||||
FileUtils.mkdir_p(File.dirname(pidfile))
|
||||
File.write(pidfile, Process.pid.to_s)
|
||||
end
|
||||
|
||||
def remove_pidfile(pidfile)
|
||||
File.delete(pidfile) if File.exist?(pidfile) && File.read(pidfile).strip.to_i == Process.pid
|
||||
end
|
||||
|
||||
def process_alive?(pid)
|
||||
Process.kill(0, pid)
|
||||
true
|
||||
rescue Errno::ESRCH
|
||||
false
|
||||
rescue Errno::EPERM
|
||||
true
|
||||
end
|
||||
|
||||
def new_app(name)
|
||||
@@ -253,6 +306,145 @@ module Prism
|
||||
end
|
||||
end
|
||||
|
||||
def jobs(rest)
|
||||
subcommand, *args = rest
|
||||
case subcommand
|
||||
when 'work' then jobs_work(args)
|
||||
else raise UsageError, "unknown jobs subcommand #{subcommand.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def parse_jobs_work_options(args)
|
||||
options = { file: 'db/jobs.pstore', interval: 1.0, queue_name: nil, once: false,
|
||||
max_cycles: nil, verbose: false, require: [], pidfile: nil }
|
||||
args.each do |arg|
|
||||
case arg
|
||||
when '--once' then options[:once] = true
|
||||
when '--verbose' then options[:verbose] = true
|
||||
when /\A--file=(.+)\z/ then options[:file] = Regexp.last_match(1)
|
||||
when /\A--interval=(.+)\z/ then options[:interval] = Regexp.last_match(1).to_f
|
||||
when /\A--queue=(.+)\z/ then options[:queue_name] = Regexp.last_match(1)
|
||||
when /\A--max-cycles=(\d+)\z/ then options[:max_cycles] = Regexp.last_match(1).to_i
|
||||
when /\A--require=(.+)\z/ then options[:require] << Regexp.last_match(1)
|
||||
when /\A--pidfile=(.+)\z/ then options[:pidfile] = Regexp.last_match(1)
|
||||
else raise UsageError, "unknown option #{arg.inspect}"
|
||||
end
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def parse_routes_options(args)
|
||||
options = { require: [], app: nil }
|
||||
args.each do |arg|
|
||||
case arg
|
||||
when /\A--require=(.+)\z/ then options[:require] << Regexp.last_match(1)
|
||||
when /\A--app=(.+)\z/ then options[:app] = Regexp.last_match(1)
|
||||
else raise UsageError, "unknown option #{arg.inspect}"
|
||||
end
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
# `--app` picks by name when a project loads more than one
|
||||
# Prism::Application subclass (e.g. a host app plus a mounted engine).
|
||||
# Auto-detection deliberately looks only at subclasses newly defined by
|
||||
# THIS call's --require, not at Application.subclasses as a whole: a
|
||||
# Ruby class is never garbage-collected just because its constant gets
|
||||
# undefined, so in a long-lived process (the test suite, an irb/console
|
||||
# session reloading code) every Application subclass ever defined stays
|
||||
# in that list forever - scanning it whole would make auto-detection
|
||||
# increasingly wrong the longer the process lives, not just after a
|
||||
# fresh `ruby bin/prism routes --require=app.rb` (where it happens to
|
||||
# equal "the one class app.rb just defined" anyway).
|
||||
def resolve_app_class(explicit_name, newly_loaded)
|
||||
return Object.const_get(explicit_name) if explicit_name
|
||||
raise UsageError, 'no Prism::Application subclass loaded (pass --require=FILE or --app=ClassName)' if newly_loaded.empty?
|
||||
if newly_loaded.size > 1
|
||||
names = newly_loaded.map(&:name).join(', ')
|
||||
raise UsageError, "multiple Prism::Application subclasses loaded (#{names}) - pick one with --app=ClassName"
|
||||
end
|
||||
newly_loaded.first
|
||||
end
|
||||
|
||||
def print_routes(router, out)
|
||||
rows = router.routes.map do |route|
|
||||
[route.method, route.pattern, route_target(route), route.name ? "#{route.name}_path" : '']
|
||||
end
|
||||
header = %w[VERB PATH TO NAME]
|
||||
widths = header.each_index.map { |i| ([header[i].length] + rows.map { |row| row[i].length }).max }
|
||||
|
||||
out.puts format_row(header, widths)
|
||||
rows.each { |row| out.puts format_row(row, widths) }
|
||||
|
||||
router.mounts.each do |prefix, mounted_app|
|
||||
out.puts "MOUNT #{prefix} -> #{mounted_app.class}"
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def route_target(route)
|
||||
route.controller_route? ? "#{route.controller_name}##{route.action_name}" : '(block)'
|
||||
end
|
||||
|
||||
def format_row(row, widths)
|
||||
row.each_index.map { |i| row[i].ljust(widths[i]) }.join(' ').rstrip
|
||||
end
|
||||
|
||||
def parse_console_options(args)
|
||||
options = { require: [], prompt: 'prism> ' }
|
||||
args.each do |arg|
|
||||
case arg
|
||||
when /\A--require=(.+)\z/ then options[:require] << Regexp.last_match(1)
|
||||
when /\A--prompt=(.+)\z/ then options[:prompt] = Regexp.last_match(1)
|
||||
else raise UsageError, "unknown option #{arg.inspect}"
|
||||
end
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def parse_seed_options(args)
|
||||
options = { require: [], file: 'db/seeds.rb' }
|
||||
args.each do |arg|
|
||||
case arg
|
||||
when /\A--require=(.+)\z/ then options[:require] << Regexp.last_match(1)
|
||||
when /\A--file=(.+)\z/ then options[:file] = Regexp.last_match(1)
|
||||
else raise UsageError, "unknown option #{arg.inspect}"
|
||||
end
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def parse_db_options(args)
|
||||
options = { database: 'db/development.pstore', migrations: 'db/migrate', step: 1 }
|
||||
args.each do |arg|
|
||||
case arg
|
||||
when /\A--database=(.+)\z/ then options[:database] = Regexp.last_match(1)
|
||||
when /\A--migrations=(.+)\z/ then options[:migrations] = Regexp.last_match(1)
|
||||
when /\A--step=(\d+)\z/ then options[:step] = Regexp.last_match(1).to_i
|
||||
else raise UsageError, "unknown option #{arg.inspect}"
|
||||
end
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def build_migrator(options)
|
||||
store = Orm::Store.new(File.expand_path(options[:database], @root))
|
||||
Orm::Migrator.new(store, File.expand_path(options[:migrations], @root))
|
||||
end
|
||||
|
||||
def parse_assets_precompile_options(args)
|
||||
options = {}
|
||||
args.each do |arg|
|
||||
case arg
|
||||
when /\A--source=(.+)\z/ then options[:source] = Regexp.last_match(1)
|
||||
when /\A--output=(.+)\z/ then options[:output] = Regexp.last_match(1)
|
||||
else raise UsageError, "unknown option #{arg.inspect}"
|
||||
end
|
||||
end
|
||||
raise UsageError, 'usage: prism assets:precompile --source=DIR --output=DIR' unless options[:source] && options[:output]
|
||||
options
|
||||
end
|
||||
|
||||
def next_migration_version
|
||||
version = Time.now.utc.strftime('%Y%m%d%H%M%S')
|
||||
version = (version.to_i + 1).to_s while File.exist?(File.join(@root, 'db', 'migrate')) &&
|
||||
|
||||
Reference in new issue
Block a user