Skip to content

Commit

Permalink
Add Delayed::Plugins::Pidfile
Browse files Browse the repository at this point in the history
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, a Errno::EEXIST exception 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 collectiveidea#875
  • Loading branch information
jdufresne committed Feb 3, 2024
1 parent b66bb64 commit cf71918
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, a `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:
Expand Down
25 changes: 25 additions & 0 deletions lib/delayed/plugins/pidfile.rb
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions spec/delayed/plugins/pidfile_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit cf71918

Please sign in to comment.