From 6b9858fabf6343393210a9cdaba11c05096ee550 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 21 May 2026 08:53:58 -0400 Subject: [PATCH 1/3] feat: multi-database support via connects_to config option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `connects_to` setting that, when configured, wraps every engine request in `ActiveRecord::Base.connected_to(...)`. Accepts any kwargs Rails supports — `database:`, `role:`, or `shard:`. Defaults to nil so single-database apps are unaffected. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/application_controller.rb | 10 ++++++++++ lib/solid_queue_web.rb | 7 ++++++- spec/requests/solid_queue_web/dashboard_spec.rb | 16 ++++++++++++++++ spec/solid_queue_web_spec.rb | 12 +++++++++++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/controllers/solid_queue_web/application_controller.rb b/app/controllers/solid_queue_web/application_controller.rb index 1ead939..157e6aa 100644 --- a/app/controllers/solid_queue_web/application_controller.rb +++ b/app/controllers/solid_queue_web/application_controller.rb @@ -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) diff --git a/lib/solid_queue_web.rb b/lib/solid_queue_web.rb index d9ea8d2..1ee1b77 100644 --- a/lib/solid_queue_web.rb +++ b/lib/solid_queue_web.rb @@ -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 @@ -39,6 +40,10 @@ def alert_webhook_cooldown @alert_webhook_cooldown || 3600 end + def connects_to + @connects_to + end + def configure yield self end diff --git a/spec/requests/solid_queue_web/dashboard_spec.rb b/spec/requests/solid_queue_web/dashboard_spec.rb index 35a3e1a..a284319 100644 --- a/spec/requests/solid_queue_web/dashboard_spec.rb +++ b/spec/requests/solid_queue_web/dashboard_spec.rb @@ -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 diff --git a/spec/solid_queue_web_spec.rb b/spec/solid_queue_web_spec.rb index f4cf7ff..08afe83 100644 --- a/spec/solid_queue_web_spec.rb +++ b/spec/solid_queue_web_spec.rb @@ -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 @@ -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 From f89bf743e892160e6684b4e359a686ea58f0879e Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 21 May 2026 08:54:05 -0400 Subject: [PATCH 2/3] docs: update CHANGELOG and README for multi-database support Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + README.md | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b2bbbf..4660424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index fdc37a6..2530460 100644 --- a/README.md +++ b/README.md @@ -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 @@ -114,6 +115,22 @@ end No authentication is enforced by default. When the `authenticate` block returns falsy, HTTP Basic auth is used as a fallback. +## 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: @@ -122,7 +139,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. From 283cb30f5332b15201c4149efdf4ddb83bfe29bd Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 21 May 2026 08:57:15 -0400 Subject: [PATCH 3/3] docs: add dedicated Webhook alerts section to README Co-Authored-By: Claude Sonnet 4.6 --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 2530460..3529c0c 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,31 @@ 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.