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 @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Slow job webhook alert — set `alert_slow_job_count_threshold` (integer) to fire a webhook whenever the number of currently-running slow jobs meets or exceeds the configured count; requires `slow_job_threshold` to define what "slow" means; uses the same `alert_webhook_url` and `alert_webhook_cooldown` settings as other alert types; event name `slow_job_threshold_exceeded`
- Stale process webhook alert — set `alert_stale_process_threshold` (integer) to fire a webhook whenever the number of stale workers meets or exceeds the configured count; a process is stale when its heartbeat has not been updated within `SolidQueue.process_alive_threshold`; uses the same `alert_webhook_url` and `alert_webhook_cooldown` settings; event name `stale_process_detected`
- Job wait time column — the Running tab now shows a "Wait Time" column displaying how long each claimed job waited in the queue from enqueue to pickup; also included as `wait_time_seconds` in the CSV export for the claimed status
- Eliminate N+1 queries on the queues index — `queue.size` and `queue.paused?` previously fired one query each per queue; both are now pre-computed in the controller with single batch aggregations (`GROUP BY queue_name` for sizes, one `Pause` query for paused state), reducing query count from O(2N+constant) to O(constant) regardless of queue count

## [1.3.0] - 2026-05-27

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Pull requests for any of these are welcome. See [Contributing](README.md#contrib
| ~~**Slow job webhook alert**~~ | ✅ Shipped in v1.4 — `alert_slow_job_count_threshold` fires when slow-job count meets or exceeds the threshold. |
| ~~**Process stale webhook alert**~~ | ✅ Shipped in v1.4 — `alert_stale_process_threshold` fires when stale worker count meets or exceeds the threshold. |
| ~~**Job wait time column**~~ | ✅ Shipped in v1.4 — "Wait Time" column on the Running tab; `wait_time_seconds` in the claimed CSV export. |
| **Eliminate N+1 queries** | Queues index fires 2 extra queries per queue: `queue.size` (COUNT per queue) and `queue.paused?` (EXISTS per queue). Replace with single batch aggregations pre-computed in the controller. |
| ~~**Eliminate N+1 queries**~~ | ✅ Shipped in v1.4 — queue size and paused state now pre-computed with single batch aggregations; query count reduced from O(2N) to O(1) per page load. |

---

Expand Down
5 changes: 5 additions & 0 deletions app/controllers/solid_queue_web/queues_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ def index
@failed_24h = stats.failed_24h
@oldest_ready = stats.oldest_ready
@failure_sparklines = stats.failure_sparklines
@queue_sizes = SolidQueue::ReadyExecution
.joins(:job)
.group("solid_queue_jobs.queue_name")
.count
@paused_queue_names = SolidQueue::Pause.pluck(:queue_name).to_set
end
end
end
6 changes: 3 additions & 3 deletions app/views/solid_queue_web/queues/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<% @queues.each do |queue| %>
<tr>
<td class="sqd-mono"><%= queue.name %></td>
<td><%= queue.size %></td>
<td><%= @queue_sizes[queue.name] || 0 %></td>
<td>
<% if (oldest = @oldest_ready[queue.name]) %>
<% age = Time.current - oldest %>
Expand Down Expand Up @@ -52,14 +52,14 @@
<% end %>
</td>
<td>
<% if queue.paused? %>
<% if @paused_queue_names.include?(queue.name) %>
<span class="sqd-badge sqd-badge--paused">Paused</span>
<% else %>
<span class="sqd-badge sqd-badge--running">Running</span>
<% end %>
</td>
<td class="sqd-row-actions">
<% if queue.paused? %>
<% if @paused_queue_names.include?(queue.name) %>
<%= button_to "Resume", queue_pause_path(queue.name), method: :delete,
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
<% else %>
Expand Down
42 changes: 42 additions & 0 deletions spec/requests/solid_queue_web/queues_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,48 @@
end
end

describe "GET /jobs/queues — batch-loaded queue size and paused state" do
it "shows the ready job count for a queue" do
job = SolidQueue::Job.create!(
queue_name: "default", class_name: "TestJob",
arguments: {}, active_job_id: SecureRandom.uuid
)
job.ready_execution
get "/jobs/queues"
expect(response.body).to match(/<td>\s*\d+\s*<\/td>/)
end

it "shows 0 size for a queue with no ready jobs" do
SolidQueue::ReadyExecution.delete_all
get "/jobs/queues"
expect(response.body).to include("<td>0</td>")
end

it "shows the Paused badge for a paused queue" do
SolidQueue::Pause.create!(queue_name: "default")
get "/jobs/queues"
expect(response.body).to include("sqd-badge--paused")
expect(response.body).to include("Paused")
end

it "shows the Running badge for an unpaused queue" do
get "/jobs/queues"
expect(response.body).to include("sqd-badge--running")
expect(response.body).to include("Running")
end

it "shows Resume button for a paused queue" do
SolidQueue::Pause.create!(queue_name: "default")
get "/jobs/queues"
expect(response.body).to include("Resume")
end

it "shows Pause button for a running queue" do
get "/jobs/queues"
expect(response.body).to include("Pause")
end
end

describe "POST /jobs/queues/:name/pause" do
it "pauses the queue and redirects" do
post "/jobs/queues/default/pause"
Expand Down