---
url: /background-jobs.md
---
# Background jobs

Trek apps run Active Job on [GoodJob](https://github.com/bensheldon/good_job), 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:

```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`](/reference/generators/audit-log#purging-old-rows) and [`trek:public_accounts`](/reference/generators/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.
