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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/MIT-LICENSE +20 -0
- data/README.md +142 -0
- data/Rakefile +3 -0
- data/app/assets/stylesheets/soliq_queue_lite/application.css +15 -0
- data/app/controllers/concerns/solid_queue_lite/approximate_countable.rb +10 -0
- data/app/controllers/solid_queue_lite/application_controller.rb +4 -0
- data/app/controllers/solid_queue_lite/dashboards_controller.rb +61 -0
- data/app/controllers/solid_queue_lite/jobs_controller.rb +129 -0
- data/app/controllers/solid_queue_lite/processes_controller.rb +39 -0
- data/app/controllers/solid_queue_lite/queues_controller.rb +31 -0
- data/app/helpers/solid_queue_lite/application_helper.rb +27 -0
- data/app/jobs/solid_queue_lite/application_job.rb +4 -0
- data/app/jobs/solid_queue_lite/telemetry_sampler_job.rb +11 -0
- data/app/models/solid_queue_lite/application_record.rb +5 -0
- data/app/models/solid_queue_lite/stat.rb +7 -0
- data/app/views/layouts/solid_queue_lite/application.html.erb +383 -0
- data/app/views/solid_queue_lite/dashboards/show.html.erb +573 -0
- data/config/routes.rb +30 -0
- data/db/migrate/20260406000000_create_solid_queue_lite_stats.rb +16 -0
- data/lib/solid_queue_lite/approximate_counter.rb +87 -0
- data/lib/solid_queue_lite/engine.rb +20 -0
- data/lib/solid_queue_lite/install.rb +107 -0
- data/lib/solid_queue_lite/jobs.rb +236 -0
- data/lib/solid_queue_lite/processes.rb +156 -0
- data/lib/solid_queue_lite/telemetry.rb +201 -0
- data/lib/solid_queue_lite/version.rb +3 -0
- data/lib/solid_queue_lite.rb +46 -0
- data/lib/tasks/solid_queue_lite_tasks.rake +14 -0
- metadata +116 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
module SolidQueueLite
|
|
2
|
+
module Telemetry
|
|
3
|
+
SNAPSHOT_QUEUE_NAME = "*"
|
|
4
|
+
SAMPLE_WINDOW = 5.minutes
|
|
5
|
+
RETENTION_PERIOD = 7.days
|
|
6
|
+
RANGE_OPTIONS = {
|
|
7
|
+
"1h" => 1.hour,
|
|
8
|
+
"6h" => 6.hours,
|
|
9
|
+
"24h" => 24.hours
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def sample!(timestamp: Time.current)
|
|
15
|
+
snapshot_time = normalize_timestamp(timestamp)
|
|
16
|
+
stat = SolidQueueLite::Stat.create!(snapshot_attributes(snapshot_time))
|
|
17
|
+
prune!(cutoff: snapshot_time - RETENTION_PERIOD)
|
|
18
|
+
stat
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def backfill!(timestamp: Time.current)
|
|
22
|
+
snapshot_time = normalize_timestamp(timestamp)
|
|
23
|
+
latest = SolidQueueLite::Stat.where(queue_name: SNAPSHOT_QUEUE_NAME).order(timestamp: :desc).first
|
|
24
|
+
|
|
25
|
+
if latest && latest.timestamp >= snapshot_time - SAMPLE_WINDOW
|
|
26
|
+
latest.update!(snapshot_attributes(latest.timestamp))
|
|
27
|
+
latest
|
|
28
|
+
else
|
|
29
|
+
sample!(timestamp: snapshot_time)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dashboard_data(range_key: "24h")
|
|
34
|
+
selected_range = RANGE_OPTIONS.key?(range_key) ? range_key : "24h"
|
|
35
|
+
window_start = Time.current - RANGE_OPTIONS.fetch(selected_range)
|
|
36
|
+
stats = SolidQueueLite::Stat.where(queue_name: SNAPSHOT_QUEUE_NAME, timestamp: window_start..).order(:timestamp)
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
selected_range: selected_range,
|
|
40
|
+
stats: stats,
|
|
41
|
+
latest_stat: stats.last,
|
|
42
|
+
current_ready_count: exact_count(ready_relation),
|
|
43
|
+
current_scheduled_count: exact_count(scheduled_relation),
|
|
44
|
+
current_failed_count: exact_count(current_failed_relation),
|
|
45
|
+
worker_count: ::SolidQueue::Process.where(kind: "Worker").count,
|
|
46
|
+
dispatcher_count: ::SolidQueue::Process.where(kind: "Dispatcher").count,
|
|
47
|
+
stale_process_count: ::SolidQueue::Process.prunable.where(kind: [ "Worker", "Dispatcher" ]).count,
|
|
48
|
+
recurring_tasks: recurring_task_rows,
|
|
49
|
+
chart_payload: {
|
|
50
|
+
labels: stats.map { |stat| stat.timestamp.strftime("%H:%M") },
|
|
51
|
+
ready_counts: stats.map(&:ready_count),
|
|
52
|
+
scheduled_counts: stats.map { |stat| stat.respond_to?(:scheduled_count) ? stat.scheduled_count : 0 },
|
|
53
|
+
success_counts: stats.map { |stat| stat.respond_to?(:success_count) ? stat.success_count : 0 },
|
|
54
|
+
failed_counts: stats.map(&:failed_count),
|
|
55
|
+
avg_latencies: stats.map { |stat| stat.avg_latency&.round(2) || 0.0 }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def snapshot_attributes(timestamp)
|
|
61
|
+
window_start = timestamp - SAMPLE_WINDOW
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
timestamp: timestamp,
|
|
65
|
+
queue_name: SNAPSHOT_QUEUE_NAME,
|
|
66
|
+
ready_count: exact_count(ready_relation),
|
|
67
|
+
scheduled_count: exact_count(scheduled_relation),
|
|
68
|
+
failed_count: recent_failed_relation(window_start).count,
|
|
69
|
+
success_count: successful_job_relation(window_start).count,
|
|
70
|
+
avg_latency: average_latency(recent_claimed_relation(window_start))
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ready_relation
|
|
75
|
+
SolidQueueLite.apply_tenant_scope(::SolidQueue::ReadyExecution.all)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def scheduled_relation
|
|
79
|
+
SolidQueueLite.apply_tenant_scope(::SolidQueue::ScheduledExecution.all)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def recent_failed_relation(window_start)
|
|
83
|
+
SolidQueueLite.apply_tenant_scope(::SolidQueue::FailedExecution.where(created_at: window_start..))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def current_failed_relation
|
|
87
|
+
SolidQueueLite.apply_tenant_scope(::SolidQueue::FailedExecution.all)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def successful_job_relation(window_start)
|
|
91
|
+
SolidQueueLite.apply_tenant_scope(
|
|
92
|
+
::SolidQueue::Job.left_outer_joins(:failed_execution)
|
|
93
|
+
.where(finished_at: window_start..)
|
|
94
|
+
.where(solid_queue_failed_executions: { id: nil })
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def recent_claimed_relation(window_start)
|
|
99
|
+
SolidQueueLite.apply_tenant_scope(
|
|
100
|
+
::SolidQueue::ClaimedExecution.joins(:job).where(
|
|
101
|
+
::SolidQueue::ClaimedExecution.table_name => { created_at: window_start.. }
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def average_latency(relation)
|
|
107
|
+
adapter_name = relation.connection.adapter_name.downcase
|
|
108
|
+
|
|
109
|
+
expression = case adapter_name
|
|
110
|
+
when /postgres/
|
|
111
|
+
Arel.sql("EXTRACT(EPOCH FROM solid_queue_claimed_executions.created_at - solid_queue_jobs.created_at)")
|
|
112
|
+
when /mysql/, /trilogy/
|
|
113
|
+
Arel.sql("TIMESTAMPDIFF(MICROSECOND, solid_queue_jobs.created_at, solid_queue_claimed_executions.created_at) / 1000000.0")
|
|
114
|
+
when /sqlite/
|
|
115
|
+
Arel.sql("(julianday(solid_queue_claimed_executions.created_at) - julianday(solid_queue_jobs.created_at)) * 86400.0")
|
|
116
|
+
else
|
|
117
|
+
raise NotImplementedError, "Unsupported adapter for telemetry sampling: #{relation.connection.adapter_name}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
relation.average(expression)&.to_f
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def exact_count(relation)
|
|
124
|
+
relation.except(:select, :order).count
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def recurring_task_rows
|
|
128
|
+
recurring_tasks.map do |task|
|
|
129
|
+
latest_execution = recurring_task_executions_for(task).max_by(&:run_at)
|
|
130
|
+
latest_job = latest_execution&.job
|
|
131
|
+
|
|
132
|
+
{
|
|
133
|
+
key: task.key,
|
|
134
|
+
description: task.try(:description).presence || task.key.to_s.humanize,
|
|
135
|
+
queue_name: task.try(:queue_name).presence || "solid_queue_recurring",
|
|
136
|
+
schedule: task.schedule,
|
|
137
|
+
class_name: task.try(:class_name).presence || "SolidQueue::RecurringJob",
|
|
138
|
+
last_run_at: latest_execution&.run_at,
|
|
139
|
+
next_run_at: safe_next_run_at(task),
|
|
140
|
+
last_status: recurring_status_for(latest_job)
|
|
141
|
+
}
|
|
142
|
+
end.sort_by { |task| task[:key].to_s }
|
|
143
|
+
rescue StandardError
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def recurring_tasks
|
|
148
|
+
persisted_tasks = if defined?(::SolidQueue::RecurringTask)
|
|
149
|
+
::SolidQueue::RecurringTask.static.includes(recurring_executions: :job).to_a
|
|
150
|
+
else
|
|
151
|
+
[]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
configured_tasks = begin
|
|
155
|
+
::SolidQueue::Configuration.new.send(:recurring_tasks)
|
|
156
|
+
rescue StandardError
|
|
157
|
+
[]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
(persisted_tasks + configured_tasks).uniq { |task| task.key }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def recurring_task_executions_for(task)
|
|
164
|
+
if task.respond_to?(:recurring_executions)
|
|
165
|
+
Array(task.recurring_executions)
|
|
166
|
+
else
|
|
167
|
+
[]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def safe_next_run_at(task)
|
|
172
|
+
task.next_time if task.respond_to?(:next_time)
|
|
173
|
+
rescue StandardError
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def recurring_status_for(job)
|
|
178
|
+
return "Not yet run" unless job
|
|
179
|
+
|
|
180
|
+
case job.status.to_s
|
|
181
|
+
when "failed"
|
|
182
|
+
"Failed"
|
|
183
|
+
when "claimed"
|
|
184
|
+
"Running"
|
|
185
|
+
when "ready", "scheduled"
|
|
186
|
+
"Queued"
|
|
187
|
+
else
|
|
188
|
+
job.finished_at.present? ? "Succeeded" : job.status.to_s.humanize
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def normalize_timestamp(timestamp)
|
|
193
|
+
value = timestamp.respond_to?(:in_time_zone) ? timestamp.in_time_zone : Time.zone.parse(timestamp.to_s)
|
|
194
|
+
value || Time.current
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def prune!(cutoff: RETENTION_PERIOD.ago)
|
|
198
|
+
SolidQueueLite::Stat.where(SolidQueueLite::Stat.arel_table[:timestamp].lt(cutoff)).delete_all
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require "rails"
|
|
2
|
+
require "rails/engine"
|
|
3
|
+
require "solid_queue"
|
|
4
|
+
require "solid_queue_lite/install"
|
|
5
|
+
require "solid_queue_lite/version"
|
|
6
|
+
|
|
7
|
+
module SolidQueueLite
|
|
8
|
+
class Configuration
|
|
9
|
+
attr_reader :tenant_scope
|
|
10
|
+
attr_accessor :telemetry_backfill_on_boot
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
self.tenant_scope = ->(relation) { relation }
|
|
14
|
+
self.telemetry_backfill_on_boot = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def tenant_scope=(callable)
|
|
18
|
+
unless callable.respond_to?(:call)
|
|
19
|
+
raise ArgumentError, "tenant_scope must respond to #call"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@tenant_scope = callable
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def configuration
|
|
28
|
+
@configuration ||= Configuration.new
|
|
29
|
+
end
|
|
30
|
+
alias config configuration
|
|
31
|
+
|
|
32
|
+
def configure
|
|
33
|
+
yield(configuration)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def apply_tenant_scope(relation)
|
|
37
|
+
configuration.tenant_scope.call(relation)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
require "solid_queue_lite/approximate_counter"
|
|
43
|
+
require "solid_queue_lite/jobs"
|
|
44
|
+
require "solid_queue_lite/processes"
|
|
45
|
+
require "solid_queue_lite/telemetry"
|
|
46
|
+
require "solid_queue_lite/engine"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
namespace :solid_queue_lite do
|
|
2
|
+
desc "Install Solid Queue Lite into the host Rails application"
|
|
3
|
+
task install: :environment do
|
|
4
|
+
SolidQueueLite::Install.new.run!(migrate: ENV["MIGRATE"] == "1")
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
namespace :telemetry do
|
|
8
|
+
desc "Backfill a current telemetry snapshot for Solid Queue Lite"
|
|
9
|
+
task backfill: :environment do
|
|
10
|
+
stat = SolidQueueLite::Telemetry.backfill!
|
|
11
|
+
puts "Backfilled telemetry snapshot at #{stat.timestamp}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solid_queue_lite
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nanda Suhendra
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 7.1.0
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.1'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 7.1.0
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: solid_queue
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.0'
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '1.0'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - "~>"
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '1.0'
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '1.0'
|
|
52
|
+
description: Sidekiq-style operational visibility and telemetry without the asset
|
|
53
|
+
pipeline baggage or database locking.
|
|
54
|
+
email:
|
|
55
|
+
- nandhasuhendra@gmail.com
|
|
56
|
+
executables: []
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- CHANGELOG.md
|
|
61
|
+
- MIT-LICENSE
|
|
62
|
+
- README.md
|
|
63
|
+
- Rakefile
|
|
64
|
+
- app/assets/stylesheets/soliq_queue_lite/application.css
|
|
65
|
+
- app/controllers/concerns/solid_queue_lite/approximate_countable.rb
|
|
66
|
+
- app/controllers/solid_queue_lite/application_controller.rb
|
|
67
|
+
- app/controllers/solid_queue_lite/dashboards_controller.rb
|
|
68
|
+
- app/controllers/solid_queue_lite/jobs_controller.rb
|
|
69
|
+
- app/controllers/solid_queue_lite/processes_controller.rb
|
|
70
|
+
- app/controllers/solid_queue_lite/queues_controller.rb
|
|
71
|
+
- app/helpers/solid_queue_lite/application_helper.rb
|
|
72
|
+
- app/jobs/solid_queue_lite/application_job.rb
|
|
73
|
+
- app/jobs/solid_queue_lite/telemetry_sampler_job.rb
|
|
74
|
+
- app/models/solid_queue_lite/application_record.rb
|
|
75
|
+
- app/models/solid_queue_lite/stat.rb
|
|
76
|
+
- app/views/layouts/solid_queue_lite/application.html.erb
|
|
77
|
+
- app/views/solid_queue_lite/dashboards/show.html.erb
|
|
78
|
+
- config/routes.rb
|
|
79
|
+
- db/migrate/20260406000000_create_solid_queue_lite_stats.rb
|
|
80
|
+
- lib/solid_queue_lite.rb
|
|
81
|
+
- lib/solid_queue_lite/approximate_counter.rb
|
|
82
|
+
- lib/solid_queue_lite/engine.rb
|
|
83
|
+
- lib/solid_queue_lite/install.rb
|
|
84
|
+
- lib/solid_queue_lite/jobs.rb
|
|
85
|
+
- lib/solid_queue_lite/processes.rb
|
|
86
|
+
- lib/solid_queue_lite/telemetry.rb
|
|
87
|
+
- lib/solid_queue_lite/version.rb
|
|
88
|
+
- lib/tasks/solid_queue_lite_tasks.rake
|
|
89
|
+
homepage: https://github.com/nandhasuhendra/solid_queue_lite
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
homepage_uri: https://github.com/nandhasuhendra/solid_queue_lite
|
|
94
|
+
source_code_uri: https://github.com/nandhasuhendra/solid_queue_lite/tree/main
|
|
95
|
+
changelog_uri: https://github.com/nandhasuhendra/solid_queue_lite/blob/main/CHANGELOG.md
|
|
96
|
+
documentation_uri: https://github.com/nandhasuhendra/solid_queue_lite#readme
|
|
97
|
+
bug_tracker_uri: https://github.com/nandhasuhendra/solid_queue_lite/issues
|
|
98
|
+
rubygems_mfa_required: 'true'
|
|
99
|
+
rdoc_options: []
|
|
100
|
+
require_paths:
|
|
101
|
+
- lib
|
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '3.1'
|
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0'
|
|
112
|
+
requirements: []
|
|
113
|
+
rubygems_version: 3.6.9
|
|
114
|
+
specification_version: 4
|
|
115
|
+
summary: A lightweight, zero-build dashboard for Solid Queue.
|
|
116
|
+
test_files: []
|