Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Multi-database support — a `connects_to` config option wraps all engine requests in `ActiveRecord::Base.connected_to(...)` when set; accepts any keyword arguments supported by Rails (`database:`, `role:`, `shard:`); defaults to `nil` so existing single-database setups are unaffected
- Webhook alert config — `alert_webhook_url` and `alert_failure_threshold` settings POST a JSON payload (`event`, `failure_count`, `threshold`, `fired_at`) to any URL when the failed job count meets or exceeds the threshold; fires asynchronously in a background thread so dashboard requests are never blocked; a configurable `alert_webhook_cooldown` (default 3600 s) prevents repeated alerts while the count stays elevated; HTTP errors are logged and swallowed
- Bulk retry with delay — "+5s", "+10s", "+30s", and "+1m" stagger buttons on the Failed Jobs page retry all matched jobs with a configurable interval between each; the first job runs immediately, subsequent jobs are scheduled at incremental offsets; uses per-execution `retry` so `scheduled_at` is respected by SolidQueue's dispatcher; buttons only appear when more than one job is present
- Scheduled job management — "Run Now" promotes a scheduled job to run immediately by back-dating its `scheduled_at`; "+1h", "+24h", and "+7d" buttons push `scheduled_at` forward by the chosen offset; both actions update the execution and the underlying job record; Turbo Stream responses remove the row on "Run Now" and update the `scheduled_at` cell in place on postpone
Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ SolidQueueWeb.configure do |config|
config.alert_webhook_url = "https://hooks.example.com/solid-queue" # POST target (default: nil = disabled)
config.alert_failure_threshold = 10 # fire when failed count >= this (default: nil = disabled)
config.alert_webhook_cooldown = 1800 # seconds between repeated alerts (default: 3600)
config.connects_to = { database: { writing: :queue, reading: :queue } } # multi-db (default: nil)
end

SolidQueueWeb.authenticate do
Expand All @@ -114,6 +115,47 @@ end

No authentication is enforced by default. When the `authenticate` block returns falsy, HTTP Basic auth is used as a fallback.

## Webhook alerts

Set `alert_webhook_url` and `alert_failure_threshold` to receive a POST request whenever the failed job count meets or exceeds the threshold. This is useful for paging an on-call team or triggering a Slack notification via an incoming webhook.

```ruby
SolidQueueWeb.configure do |config|
config.alert_webhook_url = "https://hooks.slack.com/services/..."
config.alert_failure_threshold = 10 # fire when >= 10 jobs have failed
config.alert_webhook_cooldown = 1800 # don't re-fire for 30 minutes (default: 3600)
end
```

The request body is JSON:

```json
{
"event": "failure_threshold_exceeded",
"failure_count": 14,
"threshold": 10,
"fired_at": "2026-05-21T12:34:56Z"
}
```

The webhook fires asynchronously in a background thread so dashboard page loads are never delayed. HTTP errors are logged to `Rails.logger` and swallowed. The cooldown window prevents repeated alerts while the count stays elevated — the clock resets on each app restart.

## Multi-database setup

If Solid Queue runs on a separate database, set `connects_to` to match your app's database configuration. The engine wraps every request in `ActiveRecord::Base.connected_to(...)` with the options you provide.

```ruby
SolidQueueWeb.configure do |config|
# Solid Queue on a named database:
config.connects_to = { database: { writing: :queue, reading: :queue } }

# Or just pin to the writing role to bypass automatic read/write splitting:
config.connects_to = { role: :writing }
end
```

When `connects_to` is `nil` (the default), no connection switching occurs and single-database apps are unaffected.

## Roadmap

Planned features, roughly ordered by priority:
Expand All @@ -122,7 +164,6 @@ Planned features, roughly ordered by priority:
- Admin audit log — record who retried or discarded which jobs and when (requires host-app user identity)

**Infrastructure**
- Multi-database support — when Solid Queue runs on a separate database from the host app
- Read replica support — route dashboard queries to a replica to avoid impacting the primary

Pull requests for any of these are welcome. See [Contributing](#contributing) below.
Expand Down
10 changes: 10 additions & 0 deletions app/controllers/solid_queue_web/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ class ApplicationController < ActionController::Base
STAGGER_INTERVALS = { "5s" => 5.seconds, "10s" => 10.seconds, "30s" => 30.seconds, "1m" => 1.minute }.freeze

before_action :authenticate!
around_action :with_database_connection

private

def with_database_connection
config = SolidQueueWeb.connects_to
if config
ActiveRecord::Base.connected_to(**config) { yield }
else
yield
end
end

def authenticate!
return unless (auth = SolidQueueWeb.authenticate)

Expand Down
7 changes: 6 additions & 1 deletion lib/solid_queue_web.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
module SolidQueueWeb
class << self
attr_writer :page_size, :dashboard_refresh_interval, :default_refresh_interval, :search_results_limit,
:slow_job_threshold, :alert_webhook_url, :alert_failure_threshold, :alert_webhook_cooldown
:slow_job_threshold, :alert_webhook_url, :alert_failure_threshold, :alert_webhook_cooldown,
:connects_to

def page_size
@page_size || 25
Expand Down Expand Up @@ -39,6 +40,10 @@ def alert_webhook_cooldown
@alert_webhook_cooldown || 3600
end

def connects_to
@connects_to
end

def configure
yield self
end
Expand Down
16 changes: 16 additions & 0 deletions spec/requests/solid_queue_web/dashboard_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@
end
end

describe "multi-database connects_to" do
after { SolidQueueWeb.connects_to = nil }

it "passes through normally when connects_to is not configured" do
get "/jobs"
expect(response).to have_http_status(:ok)
end

it "calls connected_to with the configured options when connects_to is set" do
SolidQueueWeb.connects_to = { role: :writing }
expect(ActiveRecord::Base).to receive(:connected_to).with(role: :writing).and_call_original
get "/jobs"
expect(response).to have_http_status(:ok)
end
end

describe "alert webhook" do
after do
SolidQueueWeb.alert_webhook_url = nil
Expand Down
12 changes: 11 additions & 1 deletion spec/solid_queue_web_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
RSpec.describe SolidQueueWeb do
describe ".configure" do
after do
SolidQueueWeb.page_size = nil
SolidQueueWeb.page_size = nil
SolidQueueWeb.dashboard_refresh_interval = nil
SolidQueueWeb.default_refresh_interval = nil
SolidQueueWeb.search_results_limit = nil
SolidQueueWeb.connects_to = nil
end

it "yields self and applies settings" do
Expand All @@ -29,5 +30,14 @@
expect(SolidQueueWeb.default_refresh_interval).to eq(10_000)
expect(SolidQueueWeb.search_results_limit).to eq(25)
end

it "returns nil for connects_to by default" do
expect(SolidQueueWeb.connects_to).to be_nil
end

it "accepts a connects_to hash" do
SolidQueueWeb.connects_to = { database: { writing: :queue, reading: :queue } }
expect(SolidQueueWeb.connects_to).to eq(database: { writing: :queue, reading: :queue })
end
end
end