JobControl
JobControl gives long-running Active Job jobs a small, practical control surface: a heartbeat and a safe way to ask a job to stop.
Cancellation is cooperative. JobControl never kills a worker thread or interrupts a database transaction. Your job finishes the unit it is working on, notices the request at its next checkpoint, and exits cleanly. It works best when the job can divide its work into small, idempotent units.
What it supports
JobControl requires Ruby 3.2+ and Rails 8.1–8.x.
| Feature | Availability |
|---|---|
| Lifecycle, heartbeats, checkpoints, and cooperative cancellation | Any Active Job adapter |
| Cleanup after a permanent discard | Solid Queue 1.4–1.7 with the optional adapter |
| Job detail UI and cancellation button | mission_control-jobs 1.3.x with the optional integration |
| Cleanup after Sidekiq or GoodJob discard | Not included yet |
The core behavior lives at the Active Job layer, so it works with any adapter. Permanent discard is different: every queue exposes that operation differently, so it needs a queue-specific adapter.
Installation
bundle add job_control
bin/rails generate job_control:install
bin/rails db:migrateRun the generator again when upgrading JobControl. It skips migrations that are already present and copies any new ones.
Your first controlled job
Include JobControl::Job in jobs that need cooperative cancellation. Add checkpoints between units that are safe to finish independently.
class ExampleJob < ApplicationJob
include JobControl::Job
def perform
100.times do
perform_one_unit_of_work
checkpoint!
end
end
private
def perform_one_unit_of_work
end
endjob = ExampleJob.perform_later
JobControl.cancel(job.job_id)checkpoint! updates the heartbeat and raises JobControl::Cancelled if cancellation was requested. Put it after a blocking wait and between batches or loop iterations. It does not interrupt code already running.
A checkpoint performs one database reload and one timestamp update. In practice, checkpoint once per batch or every few seconds of work—not once per record—unless near-instant cancellation matters more than the extra writes.
Lifecycle
A job_controls row is created when a controlled job is enqueued and becomes running when execution begins.
| What happens | What JobControl does |
|---|---|
| The job succeeds | Publishes job_control.succeeded and deletes the row immediately. |
| The job sees a cancellation request at a checkpoint | Publishes job_control.cancelled and deletes the row immediately. |
| The job fails without a cancellation request | Publishes job_control.failed and deletes the row. A retry gets a fresh active control with the same job ID. |
| A cancellation request is followed by an error | Leaves the row as cancel_requested, so a retry stops before doing work. |
| Solid Queue permanently discards the job | The Solid Queue adapter deletes any remaining row after the queue transaction commits. |
There is no retention window and no cleanup cron. For an unsupported queue, a cancelled attempt that fails and is never retried cannot be detected through generic Active Job callbacks; add a queue-specific adapter before relying on permanent-discard cleanup.
Runtime API
control = JobControl.find(job.job_id)
control.status
control.cancellable?
control.cancel_requested?
control.heartbeat_at
control.last_checkpoint_at
control.cancel!Within a controlled job, call checkpoint! directly or pass cancellation to a service with its own loop:
SomeService.call(cancellation: cancellation)Observability
A control is stale after five minutes without a heartbeat by default:
JobControl.configure do |config|
config.stale_after = 15.minutes
endJobControl publishes Active Support notifications with control: in the payload:
job_control.startedjob_control.checkpointjob_control.cancel_requestedjob_control.cancelledjob_control.succeededjob_control.failed
Solid Queue
The optional adapter supports Solid Queue 1.4–1.7 and removes controls after direct and batched discard operations.
require "job_control/adapters/solid_queue"
Rails.application.config.to_prepare do
JobControl::Adapters::SolidQueue::Adapter.install!
endMission Control Jobs
With mission_control-jobs 1.3.x, JobControl adds a Job control section to the existing job detail page. It uses Mission Control's own styles and controller authentication.
require "job_control/integrations/mission_control_jobs"
Rails.application.config.to_prepare do
JobControl::Integrations::MissionControlJobs.install!
endMission Control is assumed to be mounted at /admin/jobs. For another location:
JobControl::Integrations::MissionControlJobs.install!(mount_path: "/operations/jobs")The endpoint, client code, controller, and translations come from JobControl. There is no application view override, custom controller, or hand-written route to maintain. Restart the web process after enabling it.
Development
bundle exec rakeOut of scope
JobControl does not provide hard process termination, transaction rollback, progress tracking, pause/resume, cursor recovery, throttling, batch cancellation, or an administrative adapter for every queue backend.
Contributing
Issues and pull requests are welcome at kha-wogi/job_control.