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
| Where | What |
|---|---|
Gemfile | good_job replaces solid_queue (Solid Cache stays: its table lives in the primary database) |
config/application.rb | config.active_job.queue_adapter = :good_job and config.action_mailer.delivery_job = "MailDeliveryJob" |
config/environments/production.rb | config.good_job.execution_mode = :async and config.good_job.shutdown_timeout = 25 |
config/environments/test.rb | config.active_job.queue_adapter = :test, so specs can check what is enqueued (have_enqueued_job) |
config/initializers/good_job.rb | queues, cron, error reporting, and the dashboard guard |
config/database.yml | a connection pool sized for the web threads and the job threads |
app/jobs/mail_delivery_job.rb | the delivery job every deliver_later email goes through |
config/routes.rb | mount 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:
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:
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:
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:
class MailDeliveryJob < ActionMailer::MailDeliveryJob
retry_on StandardError, wait: :polynomially_longer, attempts: 5
endAn 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.