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
@@ -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,3 @@
1
+ module SolidQueueLite
2
+ VERSION = "0.1.0"
3
+ 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: []