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..3529c0c 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,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: @@ -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. 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