Skip to content

Background jobs ​

Trek apps run Active Job on GoodJob, installed by bin/rails g trek:install (or on its own with bin/rails g trek:install:good_job). GoodJob stores its queue in the application's own PostgreSQL database, so there is no second database to provision — which is why the installer removes the Solid Queue setup rails new generates.

What the installer sets up ​

WhereWhat
Gemfilegood_job replaces solid_queue (Solid Cache stays: its table lives in the primary database)
config/application.rbconfig.active_job.queue_adapter = :good_job and config.action_mailer.delivery_job = "MailDeliveryJob"
config/environments/production.rbconfig.good_job.execution_mode = :async and config.good_job.shutdown_timeout = 25
config/environments/test.rbconfig.active_job.queue_adapter = :test, so specs can check what is enqueued (have_enqueued_job)
config/initializers/good_job.rbqueues, cron, error reporting, and the dashboard guard
config/database.ymla connection pool sized for the web threads and the job threads
app/jobs/mail_delivery_job.rbthe delivery job every deliver_later email goes through
config/routes.rbmount GoodJob::Engine => "good_job" inside namespace :admin

Async in Puma, or a dedicated worker ​

In production the jobs run in the web container's own threads (execution_mode = :async): nothing else to deploy, nothing else to pay for. shutdown_timeout = 25 lets a restart drain the jobs in flight instead of dropping them.

GOOD_JOB_MAX_THREADS sizes the job pool. It also feeds the database pool, since config/database.yml reserves connections for the web threads and the job threads separately:

yml
max_connections: <%= web = ENV.fetch("RAILS_MAX_THREADS", 5).to_i; 1 + web + ENV.fetch("GOOD_JOB_MAX_THREADS", web).to_i %>

When the jobs outgrow the web container — long-running work, or a queue that must not compete with requests — move them to their own process: set config.good_job.execution_mode = :external and uncomment the worker line in the Procfile (worker: bundle exec good_job start). Note that in :external mode only the worker runs a scheduler, so cron then runs there and nowhere else.

Queues ​

One scheduler serves every queue by default (config.good_job.queues = "*"). Give a queue its own lane when it must not wait behind the others:

ruby
config.good_job.queues = "mailers:2;*:3"

The thread counts have to stay within GOOD_JOB_MAX_THREADS — they are database connections too.

Scheduled jobs (cron) ​

Recurring jobs are declared in the initializer, and only run in production (a development machine should not fire them). The installer schedules the purge of the admin's expired sessions; trek:audit_log and trek:public_accounts add their own purges at the top of the hash:

ruby
config.good_job.enable_cron = Rails.env.production?
config.good_job.cron = {
  purge_audit_logs: {
    cron: "0 4 * * *",
    class: "Trek::AuditLog::PurgeJob",
    description: "Delete the audit log rows past the retention window"
  },
  purge_expired_sessions: {
    cron: "0 3 * * *", # every day at 3am
    class: "Trek::PurgeExpiredSessionsJob",
    description: "Delete the admin's sessions past their idle timeout or lifetime"
  }
}

The dashboard ​

GoodJob's dashboard is mounted at /admin/good_job. It sits outside Admin::BaseController, so it is guarded in the initializer by the same signed session cookie the admin uses: anyone who is not an administrator gets a 404 rather than a redirect, which keeps the route invisible. The installed request spec covers the three cases (administrator, privileged non-administrator, visitor).

Mail deliveries are retried ​

GoodJob discards a failed job rather than retrying it, so a transient error from the mail service would lose the email. Every deliver_later goes through MailDeliveryJob instead, which retries five times with a growing delay:

ruby
class MailDeliveryJob < ActionMailer::MailDeliveryJob
  retry_on StandardError, wait: :polynomially_longer, attempts: 5
end

An error raised in a scheduler thread, outside a job's own execution, has no retry handler to catch it; config.good_job.on_thread_error reports it through Rails.error like the rest of the app.

Released under the MIT License.