solid_queue_lite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +10 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +142 -0
  5. data/Rakefile +3 -0
  6. data/app/assets/stylesheets/soliq_queue_lite/application.css +15 -0
  7. data/app/controllers/concerns/solid_queue_lite/approximate_countable.rb +10 -0
  8. data/app/controllers/solid_queue_lite/application_controller.rb +4 -0
  9. data/app/controllers/solid_queue_lite/dashboards_controller.rb +61 -0
  10. data/app/controllers/solid_queue_lite/jobs_controller.rb +129 -0
  11. data/app/controllers/solid_queue_lite/processes_controller.rb +39 -0
  12. data/app/controllers/solid_queue_lite/queues_controller.rb +31 -0
  13. data/app/helpers/solid_queue_lite/application_helper.rb +27 -0
  14. data/app/jobs/solid_queue_lite/application_job.rb +4 -0
  15. data/app/jobs/solid_queue_lite/telemetry_sampler_job.rb +11 -0
  16. data/app/models/solid_queue_lite/application_record.rb +5 -0
  17. data/app/models/solid_queue_lite/stat.rb +7 -0
  18. data/app/views/layouts/solid_queue_lite/application.html.erb +383 -0
  19. data/app/views/solid_queue_lite/dashboards/show.html.erb +573 -0
  20. data/config/routes.rb +30 -0
  21. data/db/migrate/20260406000000_create_solid_queue_lite_stats.rb +16 -0
  22. data/lib/solid_queue_lite/approximate_counter.rb +87 -0
  23. data/lib/solid_queue_lite/engine.rb +20 -0
  24. data/lib/solid_queue_lite/install.rb +107 -0
  25. data/lib/solid_queue_lite/jobs.rb +236 -0
  26. data/lib/solid_queue_lite/processes.rb +156 -0
  27. data/lib/solid_queue_lite/telemetry.rb +201 -0
  28. data/lib/solid_queue_lite/version.rb +3 -0
  29. data/lib/solid_queue_lite.rb +46 -0
  30. data/lib/tasks/solid_queue_lite_tasks.rake +14 -0
  31. metadata +116 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8794ace8f2ebf4cfcaf14e887c2a166f660ee6a91e6cb381052efa29d6bb913f
4
+ data.tar.gz: 7d0b0b3fbe10136b7ef29f75686c05943a8db655d5025795f54c7ea92273a09e
5
+ SHA512:
6
+ metadata.gz: e0ce386dc14caf232ef48431d826029d9bd17cfd6dda08840e28c7779015230589f9c503a8773287ed0b59feaafa26c8489cb9569ad340234c45122e4456da59
7
+ data.tar.gz: 26962200de4d9e6d126dbac23eb84d6ea6c92cda64e0c4993daea15aedf498b5e7c32836467fdde96caf6137a80150511ba1d92a6f9e6bdb246dfd69cfae0b54
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial public release of Solid Queue Lite.
6
+ - Added a mockup-aligned dashboard experience for pulse, jobs, processes, and recurring tasks.
7
+ - Added queue discovery from Solid Queue worker configuration and process metadata.
8
+ - Added exact queue and telemetry counts for fresh installs.
9
+ - Added recurring task monitoring with last-run and next-run visibility.
10
+ - Added queue control actions, job drill-downs, retry/discard operations, and telemetry sampling support.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright nanda suhendra
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # Solid Queue Lite
2
+
3
+ A minimal, zero-build web interface for [Solid Queue](https://github.com/rails/solid_queue).
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/solid_queue_lite.svg)](https://rubygems.org/gems/solid_queue_lite)
6
+
7
+ The official `mission_control-jobs` engine is great, but it brings along Turbo, Stimulus, and expects a standard Rails asset pipeline. If you run an API-only app, use a modern JS framework, or just want to avoid frontend dependencies in your infrastructure tooling, Solid Queue Lite provides the same operational visibility without the build-step baggage.
8
+
9
+ ## Installation
10
+
11
+ Add the engine to your host application's Gemfile:
12
+
13
+ ```ruby
14
+ gem "solid_queue_lite"
15
+ ```
16
+
17
+ Install dependencies, then run the engine migration:
18
+
19
+ ```bash
20
+ bundle install
21
+ bin/rails solid_queue_lite:install
22
+ ```
23
+
24
+ The installer copies the engine migration into the host app and creates `config/initializers/solid_queue_lite.rb` if it does not already exist.
25
+
26
+ Run the migration separately, or let the installer do it for you:
27
+
28
+ ```bash
29
+ bin/rails db:migrate
30
+ # or
31
+ bin/rails solid_queue_lite:install MIGRATE=1
32
+ ```
33
+
34
+ Mount the engine behind your application's own authentication boundary:
35
+
36
+ ```ruby
37
+ # config/routes.rb
38
+ authenticate :user, ->(user) { user.admin? } do
39
+ mount SolidQueueLite::Engine => "/ops/jobs"
40
+ end
41
+ ```
42
+
43
+ The engine root renders the dashboard at `/ops/jobs`, and the jobs index is available at `/ops/jobs/jobs`.
44
+
45
+ ## Requirements
46
+
47
+ - Ruby 3.1+
48
+ - Rails 7.1+
49
+ - Solid Queue 1.x
50
+
51
+ ## Host Configuration
52
+
53
+ Use the configuration block to scope all dashboard reads in multi-tenant deployments:
54
+
55
+ ```ruby
56
+ # config/initializers/solid_queue_lite.rb
57
+ SolidQueueLite.configure do |config|
58
+ config.tenant_scope = lambda do |relation|
59
+ relation.where(queue_name: Current.account.solid_queue_prefix)
60
+ end
61
+ end
62
+ ```
63
+
64
+ The lambda receives the Active Record relation before it is queried.
65
+
66
+ ## Telemetry Sampling
67
+
68
+ The engine ships with `SolidQueueLite::TelemetrySamplerJob`, which writes aggregate queue snapshots into `solid_queue_lite_stats` and prunes samples older than 7 days.
69
+
70
+ If you use Solid Queue recurring tasks, schedule the sampler in your host application's recurring configuration:
71
+
72
+ ```yml
73
+ # config/recurring.yml
74
+ production:
75
+ solid_queue_lite_telemetry:
76
+ class: SolidQueueLite::TelemetrySamplerJob
77
+ schedule: every 5 minutes
78
+ queue: default
79
+ ```
80
+
81
+ Without a scheduler process, the dashboard still renders, but the historical charts stay empty until samples are written.
82
+
83
+ ## Console And Tasks
84
+
85
+ The reusable logic now lives under `lib/solid_queue_lite`, so you can call the same APIs from a Rails console:
86
+
87
+ ```ruby
88
+ SolidQueueLite::Telemetry.sample!
89
+ SolidQueueLite::Telemetry.backfill!
90
+ SolidQueueLite::Telemetry.dashboard_data(range_key: "24h")
91
+
92
+ SolidQueueLite::Jobs.list(state_key: "failed", page: 1, per_page: 50)
93
+ SolidQueueLite::Jobs.find(123)
94
+
95
+ SolidQueueLite::Processes.index_data
96
+ SolidQueueLite::Processes.pause_queue!("background")
97
+ ```
98
+
99
+ To force an immediate current telemetry snapshot after upgrading, run:
100
+
101
+ ```bash
102
+ bin/rake solid_queue_lite:telemetry:backfill
103
+ ```
104
+
105
+ The historical `scheduled_count` and `success_count` series cannot be reconstructed exactly for old rows because Solid Queue does not retain that event history. The backfill task writes or refreshes a current snapshot immediately so upgraded installs do not need to wait for the next scheduled sample.
106
+
107
+ ### Core Design Decisions
108
+
109
+ - **Zero Asset Pipeline:** The UI is built with raw HTML, Pico.css, and Alpine.js loaded via CDN. It adds nothing to your app's frontend build.
110
+ - **Database Safe:** Standard job dashboards often kill primary databases with naive `COUNT(*)` queries on massive tables. This engine strictly uses database metadata (e.g., `pg_class` in Postgres) for approximate counting to prevent sequential scans and lock contention.
111
+ - **Built-in Telemetry:** Solid Queue doesn't store historical data. This engine includes a lightweight, self-pruning background job that snapshots queue sizes, latency, and error rates, giving you 24-hour trends without needing an external APM.
112
+ - **Multi-tenant Support:** Exposes a simple configuration block to scope the dashboard's database queries, allowing you to isolate job visibility per tenant.
113
+
114
+ ### Features
115
+
116
+ - Monitor active Supervisors, Workers, and Dispatchers.
117
+ - Filter and inspect Ready, In-Progress, Scheduled, and Failed jobs.
118
+ - View job arguments (JSON), full stack traces, and execute individual or bulk retries/discards.
119
+ - Configurable auto-refresh that automatically pauses when you interact with the UI to prevent state loss.
120
+ - Monitor recurring task schedule, last run time, next run time, and latest status from a dedicated dashboard tab.
121
+
122
+ ## Releasing
123
+
124
+ Build the gem locally before publishing:
125
+
126
+ ```bash
127
+ gem build solid_queue_lite.gemspec
128
+ ```
129
+
130
+ Publish to RubyGems:
131
+
132
+ ```bash
133
+ gem push solid_queue_lite-0.1.0.gem
134
+ ```
135
+
136
+ Typical release flow:
137
+
138
+ 1. Update `lib/solid_queue_lite/version.rb`.
139
+ 2. Update `CHANGELOG.md`.
140
+ 3. Commit and tag the release.
141
+ 4. Build with `gem build solid_queue_lite.gemspec`.
142
+ 5. Push with `gem push <built-gem-file>`.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,10 @@
1
+ module SolidQueueLite
2
+ module ApproximateCountable
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+ def approximate_count(relation)
7
+ SolidQueueLite::ApproximateCounter.count(relation)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ module SolidQueueLite
2
+ class ApplicationController < ::ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,61 @@
1
+ module SolidQueueLite
2
+ class DashboardsController < ApplicationController
3
+ DASHBOARD_TABS = %w[pulse jobs processes recurring].freeze
4
+ DASHBOARD_PER_PAGE = 25
5
+
6
+ def show
7
+ telemetry_data = SolidQueueLite::Telemetry.dashboard_data(range_key: params.fetch(:range, "24h"))
8
+ process_data = SolidQueueLite::Processes.index_data
9
+
10
+ @active_tab = requested_tab
11
+ @selected_range = telemetry_data[:selected_range]
12
+ @stats = telemetry_data[:stats]
13
+ @latest_stat = telemetry_data[:latest_stat]
14
+ @current_ready_count = telemetry_data[:current_ready_count]
15
+ @current_scheduled_count = telemetry_data[:current_scheduled_count]
16
+ @current_failed_count = telemetry_data[:current_failed_count]
17
+ @worker_count = telemetry_data[:worker_count]
18
+ @dispatcher_count = telemetry_data[:dispatcher_count]
19
+ @stale_process_count = telemetry_data[:stale_process_count]
20
+ @recurring_tasks = telemetry_data[:recurring_tasks]
21
+ @chart_payload = telemetry_data[:chart_payload]
22
+
23
+ @processes = process_data[:processes]
24
+ @heartbeat = process_data[:heartbeat]
25
+ @queues = process_data[:queues]
26
+
27
+ @selected_queue_name = params[:queue_name].presence
28
+ @selected_queue_metrics = @queues.find { |queue| queue[:name] == @selected_queue_name }
29
+ @jobs_selected_state = params.fetch(:state, "failed")
30
+ @jobs_per_page = SolidQueueLite::Jobs.normalize_per_page(params.fetch(:per_page, DASHBOARD_PER_PAGE))
31
+ @jobs_page = SolidQueueLite::Jobs.normalize_page(params.fetch(:page, 1))
32
+
33
+ if @selected_queue_name.present?
34
+ jobs_data = SolidQueueLite::Jobs.list(
35
+ state_key: @jobs_selected_state,
36
+ page: @jobs_page,
37
+ per_page: @jobs_per_page,
38
+ queue_name: @selected_queue_name,
39
+ include_details: true
40
+ )
41
+
42
+ @jobs = jobs_data[:jobs]
43
+ @jobs_pagination = jobs_data[:pagination]
44
+ else
45
+ @jobs = []
46
+ @jobs_pagination = {
47
+ page: @jobs_page,
48
+ per_page: @jobs_per_page,
49
+ total_pages: 1,
50
+ approximate_total_count: 0
51
+ }
52
+ end
53
+ end
54
+
55
+ private
56
+ def requested_tab
57
+ requested_value = params[:tab].presence || "pulse"
58
+ DASHBOARD_TABS.include?(requested_value) ? requested_value : "pulse"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,129 @@
1
+ module SolidQueueLite
2
+ class JobsController < ApplicationController
3
+ rescue_from ::ActiveRecord::RecordNotFound, with: :render_not_found
4
+
5
+ def index
6
+ state_key = requested_state_key
7
+ data = SolidQueueLite::Jobs.list(
8
+ state_key: state_key,
9
+ page: requested_page,
10
+ per_page: requested_per_page,
11
+ queue_name: params[:queue_name],
12
+ include_details: true
13
+ )
14
+
15
+ @jobs = data[:jobs]
16
+ @selected_state = data[:selected_state]
17
+ @state_options = data[:state_options]
18
+ @selected_queue_name = data[:selected_queue_name]
19
+ @pagination = data[:pagination]
20
+
21
+ respond_to do |format|
22
+ format.json do
23
+ render json: {
24
+ jobs: @jobs,
25
+ pagination: @pagination,
26
+ filters: {
27
+ state: state_key
28
+ }
29
+ }
30
+ end
31
+ end
32
+ end
33
+
34
+ def show
35
+ @job = SolidQueueLite::Jobs.find(params[:id])
36
+
37
+ respond_to do |format|
38
+ format.json { render json: SolidQueueLite::Jobs.serialize(@job) }
39
+ end
40
+ end
41
+
42
+ def bulk_retry
43
+ retried_count = SolidQueueLite::Jobs.bulk_retry!(job_ids: params[:job_ids], state_key: params.fetch(:state, "failed"))
44
+
45
+ respond_to do |format|
46
+ format.json { render json: { retried: retried_count } }
47
+ end
48
+ end
49
+
50
+ def bulk_discard
51
+ discarded_count = SolidQueueLite::Jobs.bulk_discard!(job_ids: params[:job_ids], state_key: params.fetch(:state, requested_state_key))
52
+
53
+ respond_to do |format|
54
+ format.json { render json: { discarded: discarded_count } }
55
+ end
56
+ rescue ::SolidQueue::Execution::UndiscardableError => error
57
+ render_unprocessable_entity(error)
58
+ end
59
+
60
+ def retry
61
+ job = SolidQueueLite::Jobs.retry!(params[:id])
62
+
63
+ respond_to do |format|
64
+ format.json do
65
+ render json: {
66
+ id: job.id,
67
+ retried: true,
68
+ state: "ready"
69
+ }
70
+ end
71
+ end
72
+ end
73
+
74
+ def discard
75
+ job, previous_state = SolidQueueLite::Jobs.discard!(params[:id])
76
+
77
+ respond_to do |format|
78
+ format.json do
79
+ render json: {
80
+ id: job.id,
81
+ discarded: true,
82
+ previous_state: previous_state
83
+ }
84
+ end
85
+ end
86
+ rescue ::SolidQueue::Execution::UndiscardableError => error
87
+ render_unprocessable_entity(error)
88
+ end
89
+
90
+ private
91
+ def requested_page
92
+ SolidQueueLite::Jobs.normalize_page(params.fetch(:page, 1))
93
+ end
94
+
95
+ def requested_per_page
96
+ SolidQueueLite::Jobs.normalize_per_page(params.fetch(:per_page, SolidQueueLite::Jobs::DEFAULT_PER_PAGE))
97
+ end
98
+
99
+ def requested_state_key
100
+ params.fetch(:state, "ready")
101
+ end
102
+
103
+ def requested_state(state_key = requested_state_key)
104
+ SolidQueueLite::Jobs.resolve_state(state_key)
105
+ end
106
+
107
+ def jobs_redirect_params(default_state: "failed")
108
+ SolidQueueLite::Jobs.jobs_redirect_params(params, default_state: default_state)
109
+ end
110
+
111
+ def redirect_target(default_state: "failed")
112
+ return params[:return_to] if params[:return_to].to_s.start_with?("/")
113
+
114
+ jobs_path(jobs_redirect_params(default_state: default_state))
115
+ end
116
+
117
+ def render_not_found
118
+ respond_to do |format|
119
+ format.json { render json: { error: "Not found" }, status: :not_found }
120
+ end
121
+ end
122
+
123
+ def render_unprocessable_entity(error)
124
+ respond_to do |format|
125
+ format.json { render json: { error: error.message }, status: :unprocessable_entity }
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,39 @@
1
+ module SolidQueueLite
2
+ class ProcessesController < ApplicationController
3
+ def index
4
+ data = SolidQueueLite::Processes.index_data
5
+ @processes = data[:processes]
6
+ @heartbeat = data[:heartbeat]
7
+ @queues = data[:queues]
8
+
9
+ respond_to do |format|
10
+ format.json do
11
+ render json: {
12
+ processes: @processes,
13
+ heartbeat: @heartbeat
14
+ }
15
+ end
16
+ end
17
+ end
18
+
19
+ def prune
20
+ prunable_before = SolidQueueLite::Processes.prune!
21
+
22
+ respond_to do |format|
23
+ format.json do
24
+ render json: {
25
+ pruned: true,
26
+ approximate_pruned_count: prunable_before
27
+ }
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+ def redirect_target
34
+ return params[:return_to] if params[:return_to].to_s.start_with?("/")
35
+
36
+ processes_path
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ module SolidQueueLite
2
+ class QueuesController < ApplicationController
3
+ def index
4
+ redirect_to processes_path
5
+ end
6
+
7
+ def pause
8
+ SolidQueueLite::Processes.pause_queue!(params.fetch(:queue_name))
9
+ redirect_to redirect_target, notice: "Paused queue #{params[:queue_name]}"
10
+ end
11
+
12
+ def resume
13
+ SolidQueueLite::Processes.resume_queue!(params.fetch(:queue_name))
14
+ redirect_to redirect_target, notice: "Resumed queue #{params[:queue_name]}"
15
+ end
16
+
17
+ def clear
18
+ SolidQueueLite::Processes.clear_queue!(params.fetch(:queue_name))
19
+ redirect_to redirect_target, notice: "Cleared ready jobs from queue #{params[:queue_name]}"
20
+ rescue ::SolidQueue::Execution::UndiscardableError => error
21
+ redirect_to redirect_target, alert: error.message
22
+ end
23
+
24
+ private
25
+ def redirect_target
26
+ return params[:return_to] if params[:return_to].to_s.start_with?("/")
27
+
28
+ processes_path
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ module SolidQueueLite
2
+ module ApplicationHelper
3
+ def dashboard_route_params(overrides = {})
4
+ {
5
+ tab: params[:tab].presence || "pulse",
6
+ range: params[:range].presence || "24h",
7
+ queue_name: params[:queue_name].presence,
8
+ state: params[:state].presence || "failed",
9
+ page: params[:page].presence,
10
+ per_page: params[:per_page].presence
11
+ }.merge(overrides).compact
12
+ end
13
+
14
+ def dashboard_return_to(overrides = {})
15
+ root_path(dashboard_route_params(overrides))
16
+ end
17
+
18
+ def dashboard_relative_time(timestamp)
19
+ return "n/a" unless timestamp
20
+ return "Just now" if timestamp >= 1.minute.ago
21
+
22
+ I18n.with_locale(:en) do
23
+ "#{time_ago_in_words(timestamp)} ago"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ module SolidQueueLite
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,11 @@
1
+ require "json"
2
+
3
+ module SolidQueueLite
4
+ class TelemetrySamplerJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(timestamp: Time.current)
8
+ SolidQueueLite::Telemetry.sample!(timestamp: timestamp)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module SolidQueueLite
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module SolidQueueLite
2
+ class Stat < ApplicationRecord
3
+ self.table_name = "solid_queue_lite_stats"
4
+
5
+ validates :timestamp, :queue_name, presence: true
6
+ end
7
+ end