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

- Multiple webhook targets — `alert_webhook_url` now accepts an array of URL strings; all configured endpoints receive the same payload when the failure threshold is exceeded; a failure posting to one URL is logged and skipped without blocking the remaining targets
- Retry failed job with modified arguments — the Arguments card on the job detail page becomes an editable textarea for failed jobs; editing the JSON and clicking "Retry with these arguments" updates the job record and retries in one step; invalid JSON redirects back with an error message without touching the failed execution

## [1.0.0] - 2026-05-21
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ SolidQueueWeb.configure do |config|
end
```

To fan out to multiple endpoints (e.g. Slack and PagerDuty simultaneously), pass an array:

```ruby
config.alert_webhook_url = [
"https://hooks.slack.com/services/...",
"https://events.pagerduty.com/..."
]
```

All configured URLs receive the same payload. A failure posting to one URL is logged and skipped without blocking the remaining targets.

The request body is JSON:

```json
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Pull requests for any of these are welcome. See [Contributing](README.md#contrib
| Feature | Notes |
|---|---|
| ~~**Retry failed job with modified arguments**~~ | ✓ Shipped — editable textarea on the job detail page; submitting updates the job record and retries in one step. |
| **Multiple webhook targets** | Change `alert_webhook_url` to accept an array. Fan out to Slack + PagerDuty simultaneously. |
| ~~**Multiple webhook targets**~~ | ✓ Shipped — `alert_webhook_url` accepts a string or an array; all URLs receive the same payload; one failure doesn't block the rest. |
| **Queue depth alert** | Fire a webhook when a queue's ready count exceeds a per-queue threshold (e.g. `alert_queue_thresholds: { critical: 50 }`). |

---
Expand Down
9 changes: 7 additions & 2 deletions app/services/solid_queue_web/alert_webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def call(failure_count:)
return if failure_count < SolidQueueWeb.alert_failure_threshold
return unless should_fire?

Thread.new { post(SolidQueueWeb.alert_webhook_url, failure_count) }
urls = webhook_urls
Thread.new { urls.each { |url| post(url, failure_count) } }
end

def reset!
Expand All @@ -22,7 +23,11 @@ def reset!
private

def configured?
SolidQueueWeb.alert_webhook_url.present? && SolidQueueWeb.alert_failure_threshold.present?
webhook_urls.any? && SolidQueueWeb.alert_failure_threshold.present?
end

def webhook_urls
Array(SolidQueueWeb.alert_webhook_url).flatten.compact.select(&:present?)
end

def should_fire?
Expand Down
29 changes: 29 additions & 0 deletions spec/services/solid_queue_web/alert_webhook_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,34 @@
expect(Rails.logger).to receive(:error).with(/Alert webhook failed/)
expect { described_class.call(failure_count: 5) }.not_to raise_error
end

context "when alert_webhook_url is an array" do
let(:second_url) { "http://example.com/second-webhook" }

before { SolidQueueWeb.alert_webhook_url = [webhook_url, second_url] }

it "posts to all configured URLs" do
expect(Net::HTTP).to receive(:new).twice.and_call_original
described_class.call(failure_count: 5)
end

it "continues posting to remaining URLs when one fails" do
call_count = 0
allow_any_instance_of(Net::HTTP).to receive(:request) do
call_count += 1
raise RuntimeError, "connection refused" if call_count == 1
Net::HTTPSuccess.new("1.1", "200", "OK")
end
allow(Rails.logger).to receive(:error)
expect { described_class.call(failure_count: 5) }.not_to raise_error
expect(call_count).to eq(2)
end

it "is not configured when the array is empty" do
SolidQueueWeb.alert_webhook_url = []
expect_any_instance_of(Net::HTTP).not_to receive(:request)
described_class.call(failure_count: 10)
end
end
end
end