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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +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
- Read replica support — when `connects_to` is set to `{ reading: <role>, writing: <role> }`, the engine automatically routes GET requests to the reading role and mutating requests (POST/DELETE/PATCH) to the writing role via `ActiveRecord::Base.connected_to(role:)`; passing any other hash (e.g. `{ role: :writing }`, `{ shard: :name }`) falls through to `connected_to` directly; defaults to `nil` so 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
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +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)
config.connects_to = { reading: :reading, writing: :writing } # read replica (default: nil)
end

SolidQueueWeb.authenticate do
Expand Down Expand Up @@ -140,20 +140,23 @@ The request body is JSON:

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
## Read replica support

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.
Set `connects_to` with both `reading:` and `writing:` keys to enable automatic role switching. GET requests are routed to the reading role; POST/DELETE/PATCH requests use the writing role.

```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 }
# Route dashboard reads to the replica, writes to primary:
config.connects_to = { reading: :reading, writing: :writing }
end
```

The role names must match what Solid Queue's models are configured with (set via `SolidQueue.connects_to` in your app). To pin all requests to a single role instead (e.g. to bypass automatic read/write splitting middleware), pass a plain `role:` or `shard:` hash:

```ruby
config.connects_to = { role: :writing }
```

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

## Roadmap
Expand All @@ -163,9 +166,6 @@ Planned features, roughly ordered by priority:
**Operations**
- Admin audit log — record who retried or discarded which jobs and when (requires host-app user identity)

**Infrastructure**
- 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.

## Contributing
Expand Down
13 changes: 10 additions & 3 deletions app/controllers/solid_queue_web/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ class ApplicationController < ActionController::Base

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

if replica_configured?(config)
role = request.get? ? config[:reading] : config[:writing]
ActiveRecord::Base.connected_to(role: role) { yield }
else
yield
ActiveRecord::Base.connected_to(**config) { yield }
end
end

def replica_configured?(config)
config.key?(:reading) && config.key?(:writing)
end

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

Expand Down
62 changes: 62 additions & 0 deletions spec/controllers/solid_queue_web/application_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require "rails_helper"

RSpec.describe SolidQueueWeb::ApplicationController, type: :controller do
controller(SolidQueueWeb::ApplicationController) do
skip_before_action :authenticate!

def index
render plain: "ok"
end
end

after { SolidQueueWeb.connects_to = nil }

describe "#with_database_connection" do
context "when connects_to is not configured" do
it "does not call connected_to" do
expect(ActiveRecord::Base).not_to receive(:connected_to)
get :index
expect(response).to have_http_status(:ok)
end
end

context "when connects_to has a single role" do
before { SolidQueueWeb.connects_to = { role: :writing } }

it "calls connected_to with the config" do
expect(ActiveRecord::Base).to receive(:connected_to).with(role: :writing).and_yield
get :index
end
end

context "when connects_to has both reading and writing roles" do
before { SolidQueueWeb.connects_to = { reading: :reading, writing: :writing } }

it "uses the reading role for GET requests" do
expect(ActiveRecord::Base).to receive(:connected_to).with(role: :reading).and_yield
get :index
expect(response).to have_http_status(:ok)
end

it "uses the writing role for non-GET requests" do
expect(ActiveRecord::Base).to receive(:connected_to).with(role: :writing).and_yield
post :index
expect(response).to have_http_status(:ok)
end
end
end

describe "#replica_configured?" do
it "returns false when only role is configured" do
expect(controller.send(:replica_configured?, { role: :writing })).to be false
end

it "returns false when only writing is configured" do
expect(controller.send(:replica_configured?, { writing: :writing })).to be false
end

it "returns true when both reading and writing roles are configured" do
expect(controller.send(:replica_configured?, { reading: :reading, writing: :writing })).to be true
end
end
end
9 changes: 9 additions & 0 deletions spec/requests/solid_queue_web/dashboard_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@
get "/jobs"
expect(response).to have_http_status(:ok)
end

context "when reading and writing roles are both configured" do
before { SolidQueueWeb.connects_to = { reading: :writing, writing: :writing } }

it "request succeeds with replica role switching active" do
get "/jobs"
expect(response).to have_http_status(:ok)
end
end
end

describe "alert webhook" do
Expand Down