From b4ab39fa2c053840e404ef48650014f1eff85400 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 3 Feb 2024 13:16:20 -0800 Subject: [PATCH] Add Delayed::Plugins::Pidfile The new plugin creates a pidfile at location `#{Rails.root}/tmp/delayed_job.pid` when a worker starts and then removes it when a worker stops. It uses `lifecycle.around(:execute)` to achieve this. The file is created in "write exclusive" mode. This means if the file already exists, `Errno::EEXIST` is raised. This ensures that a worker doesn't overwrite a pidfile in use. This plugin is useful to allow an outside observer (e.g. a healthcheck) to check if the worker started successfully. The plugin is not installed by default for backwards compatibility. Users can use it by adding to their initializer: Delayed::Worker.plugins << Delayed::Plugin::Pidfile refs #875 --- README.md | 13 ++++++++ lib/delayed/plugins/pidfile.rb | 25 ++++++++++++++ spec/delayed/plugins/pidfile_spec.rb | 49 ++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 lib/delayed/plugins/pidfile.rb create mode 100644 spec/delayed/plugins/pidfile_spec.rb diff --git a/README.md b/README.md index ff7f2660b..a082f7683 100644 --- a/README.md +++ b/README.md @@ -483,6 +483,19 @@ Cleaning up =========== You can invoke `rake jobs:clear` to delete all jobs in the queue. +Plugins +======= + +`Delayed::Plugin::Pidfile` creates a pidfile at location +`#{Rails.root}/tmp/delayed_job.pid` when starting a worker (e.g. with `rails +jobs:work`). If the file already exists, `Errno::EEXIST` error is raised. + +To use, add to `config/initializers/delayed_job_config.rb`: + +```rb +Delayed::Worker.plugins << Delayed::Plugin::Pidfile +``` + Having problems? ================ Good places to get help are: diff --git a/lib/delayed/plugins/pidfile.rb b/lib/delayed/plugins/pidfile.rb new file mode 100644 index 000000000..979771216 --- /dev/null +++ b/lib/delayed/plugins/pidfile.rb @@ -0,0 +1,25 @@ +require 'fileutils' + +module Delayed + module Plugins + class Pidfile < Delayed::Plugin + callbacks do |lifecycle| + lifecycle.around(:execute) do |worker, &block| + dir = File.dirname(pidfile) + FileUtils.mkdir_p(dir) + + File.write(pidfile, "#{Process.pid}\n", mode => 'wx') + begin + block.call(worker) + ensure + File.unlink(pidfile) + end + end + end + + def self.pidfile + "#{Rails.root}/tmp/delayed_job.pid" + end + end + end +end diff --git a/spec/delayed/plugins/pidfile_spec.rb b/spec/delayed/plugins/pidfile_spec.rb new file mode 100644 index 000000000..2030bc7d2 --- /dev/null +++ b/spec/delayed/plugins/pidfile_spec.rb @@ -0,0 +1,49 @@ +require 'helper' +require 'delayed/plugins/pidfile' +require 'fileutils' + +describe Delayed::Plugins::Pidfile do + around do |example| + original_plugins = Delayed::Worker.plugins + begin + example.run + ensure + Delayed::Worker.plugins = original_plugins + end + end + + it 'creates a pidfile and then removes it' do + Delayed::Worker.plugins << Delayed::Plugins::Pidfile + + pidfile_contents = nil + Delayed::Worker.plugins << Class.new(Delayed::Plugin) do + callbacks do |lifecycle| + lifecycle.around(:execute) do + pidfile_contents = File.read(Delayed::Plugins::Pidfile.pidfile) + end + end + end + + expect(File.exist?(Delayed::Plugins::Pidfile.pidfile)).to be(false) + + worker = Delayed::Worker.new + Delayed::Worker.lifecycle.run_callbacks(:execute, worker) {} + + expect(pidfile_contents).to eq("#{Process.pid}\n") + expect(File.exist?(Delayed::Plugins::Pidfile.pidfile)).to be(false) + end + + it 'raises an exception if pidfile already exists' do + Delayed::Worker.plugins << Delayed::Plugins::Pidfile + + FileUtils.touch(Delayed::Plugins::Pidfile.pidfile) + begin + worker = Delayed::Worker.new + expect { Delayed::Worker.lifecycle.run_callbacks(:execute, worker) {} }.to raise_error(Errno::EEXIST) + # Doesn't remove the file. + expect(File.exist?(Delayed::Plugins::Pidfile.pidfile)).to be(true) + ensure + File.unlink(Delayed::Plugins::Pidfile.pidfile) + end + end +end