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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Eliminate N+1 queries on the queues index — replaced per-queue `COUNT` loop with a single `GROUP BY queue_name` aggregation
- `CacheSizeStats#buckets` now runs a single `SUM(CASE WHEN ...)` aggregation instead of one `COUNT` query per size bucket; `#total` is derived from the already-computed bucket counts (no extra query)
- `JobsController` filter options (queue and priority dropdowns) now resolved with one `pluck` call instead of two separate queries

### Added

- Covering indexes added to dummy app schema — `solid_queue_jobs (finished_at, created_at)` for the slow-job scan; `(queue_name, created_at)` on `solid_queue_scheduled_executions` and `solid_queue_blocked_executions` (both previously lacked a queue-name index)
- Install generator — `rails generate solid_stack_web:install` creates `config/initializers/solid_stack_web.rb` with every config option documented inline and injects the mount line into `config/routes.rb`
- `SolidStackWeb.mount_path` — returns the path at which the engine is mounted in the host app, derived automatically from routes; use `link_to "Dashboard", SolidStackWeb.mount_path` to link to the dashboard without hardcoding the path
- Accessibility pass — skip-to-content link; ARIA labels on all navigation elements; `scope="col"` on every table header; visually-hidden "Actions" label on empty action-column headers; `aria-sort` on active sort columns in stats and cache entries; `aria_label: "Pagination"` on all pagination navs; `.sqw-sr-only` and `.sqw-skip-link` CSS utilities added to base stylesheet
Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ mount SolidStackWeb::Engine, at: "/solid_stack"

The dashboard will be available at `/solid_stack` (or whatever path you choose).

### Install generator

Run the install generator to create a documented initializer and wire up the mount point in one step:

```bash
rails generate solid_stack_web:install
```

This creates `config/initializers/solid_stack_web.rb` with every configuration option commented inline, and injects `mount SolidStackWeb::Engine, at: "/solid_stack"` into `config/routes.rb`.

---

## Metrics endpoint
Expand Down Expand Up @@ -88,16 +98,6 @@ The `authenticate` block is evaluated in the context of each request's controlle
link_to "Queue Dashboard", SolidStackWeb.mount_path
```

### Install generator

Run the install generator to create a documented initializer and wire up the mount point in one step:

```bash
rails generate solid_stack_web:install
```

This creates `config/initializers/solid_stack_web.rb` with every configuration option commented inline, and injects `mount SolidStackWeb::Engine, at: "/solid_stack"` into `config/routes.rb`.

---

## Solid Queue
Expand Down
1 change: 0 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ The path to v1.0.0 is staged: first achieve feature parity with `solid_queue_das
> _Make it easy to adopt and easy to contribute to._

### Remaining
- **Query optimisation** — eliminate N+1 queries across all list views; add covering indexes to the dummy app schema
- **Error pages** — engine-scoped 404/500 views so errors stay within the dashboard chrome
- **Changelog-driven upgrade notes** — `UPGRADING.md` for any breaking configuration changes

Expand Down
7 changes: 5 additions & 2 deletions app/controllers/solid_stack_web/jobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ class JobsController < ApplicationController
before_action :require_discardable, only: [:destroy]

def index
@queue_options = Job::EXECUTION_MODELS[@status].joins(:job).distinct.pluck("solid_queue_jobs.queue_name").sort
@priority_options = Job::EXECUTION_MODELS[@status].joins(:job).distinct.pluck("solid_queue_jobs.priority").sort
pairs = Job::EXECUTION_MODELS[@status].joins(:job)
.distinct
.pluck("solid_queue_jobs.queue_name", "solid_queue_jobs.priority")
@queue_options = pairs.map(&:first).uniq.sort
@priority_options = pairs.map(&:last).uniq.sort

respond_to do |format|
format.html { @pagy, @executions = pagy(filtered_scope) }
Expand Down
13 changes: 4 additions & 9 deletions app/controllers/solid_stack_web/queues_controller.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
module SolidStackWeb
class QueuesController < ApplicationController
def index
queue_names = ::SolidQueue::ReadyExecution.distinct.pluck(:queue_name)
paused = ::SolidQueue::Pause.pluck(:queue_name).to_set
counts = ::SolidQueue::ReadyExecution.group(:queue_name).count
paused = ::SolidQueue::Pause.pluck(:queue_name).to_set

@queues = queue_names.sort.map do |name|
{
name: name,
size: ::SolidQueue::ReadyExecution.where(queue_name: name).count,
paused: paused.include?(name)
}
end
@queues = counts.map { |name, size| { name:, size:, paused: paused.include?(name) } }
.sort_by { |q| q[:name] }

@sparklines = @queues.each_with_object({}) do |queue, h|
h[queue[:name]] = QueueDepthSparkline.new(queue[:name])
Expand Down
16 changes: 10 additions & 6 deletions app/models/solid_stack_web/cache_size_stats.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@ def top_entries
end

def buckets
@buckets ||= BUCKETS.map do |b|
scope = ::SolidCache::Entry.all
scope = scope.where("byte_size >= ?", b[:min]) if b[:min] > 0
scope = scope.where("byte_size < ?", b[:max]) if b[:max]
{ label: b[:label], count: scope.count }
@buckets ||= begin
row = ::SolidCache::Entry.pluck(
Arel.sql("COALESCE(SUM(CASE WHEN byte_size < 1024 THEN 1 ELSE 0 END), 0)"),
Arel.sql("COALESCE(SUM(CASE WHEN byte_size >= 1024 AND byte_size < 10240 THEN 1 ELSE 0 END), 0)"),
Arel.sql("COALESCE(SUM(CASE WHEN byte_size >= 10240 AND byte_size < 102400 THEN 1 ELSE 0 END), 0)"),
Arel.sql("COALESCE(SUM(CASE WHEN byte_size >= 102400 AND byte_size < 1048576 THEN 1 ELSE 0 END), 0)"),
Arel.sql("COALESCE(SUM(CASE WHEN byte_size >= 1048576 THEN 1 ELSE 0 END), 0)")
).first || Array.new(5, 0)
BUCKETS.zip(row).map { |b, count| { label: b[:label], count: count.to_i } }
end
end

def total
@total ||= ::SolidCache::Entry.count
@total ||= buckets.sum { |b| b[:count] }
end
end
end
3 changes: 3 additions & 0 deletions spec/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release"
t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance"
t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
t.index ["queue_name", "created_at"], name: "index_solid_queue_blocked_executions_on_queue_name"
end

create_table "solid_queue_claimed_executions", force: :cascade do |t|
Expand Down Expand Up @@ -41,6 +42,7 @@
t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id"
t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name"
t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at"
t.index ["finished_at", "created_at"], name: "index_solid_queue_jobs_on_finished_at_and_created_at"
t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering"
t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting"
end
Expand Down Expand Up @@ -107,6 +109,7 @@
t.datetime "scheduled_at", null: false
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
t.index ["queue_name", "created_at"], name: "index_solid_queue_scheduled_executions_on_queue_name"
t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all"
end

Expand Down