roundhouse_ui 0.10.0 → 0.11.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 +4 -4
- data/README.md +563 -50
- data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +190 -23
- data/app/controllers/roundhouse_ui/application_controller.rb +73 -0
- data/app/controllers/roundhouse_ui/audit_controller.rb +1 -0
- data/app/controllers/roundhouse_ui/busy_controller.rb +25 -5
- data/app/controllers/roundhouse_ui/dashboard_controller.rb +23 -4
- data/app/controllers/roundhouse_ui/dead_controller.rb +26 -12
- data/app/controllers/roundhouse_ui/errors_controller.rb +30 -8
- data/app/controllers/roundhouse_ui/jobs_controller.rb +5 -1
- data/app/controllers/roundhouse_ui/queues_controller.rb +56 -7
- data/app/controllers/roundhouse_ui/recurring_controller.rb +12 -0
- data/app/controllers/roundhouse_ui/retries_controller.rb +33 -12
- data/app/controllers/roundhouse_ui/scheduled_controller.rb +3 -8
- data/app/controllers/roundhouse_ui/settings_controller.rb +15 -0
- data/app/controllers/roundhouse_ui/snapshots_controller.rb +5 -6
- data/app/controllers/roundhouse_ui/workers_controller.rb +2 -7
- data/app/helpers/roundhouse_ui/application_helper.rb +253 -10
- data/app/helpers/roundhouse_ui/nav_helper.rb +3 -3
- data/app/helpers/roundhouse_ui/observability_helper.rb +113 -27
- data/app/helpers/roundhouse_ui/tags_helper.rb +30 -3
- data/app/views/layouts/roundhouse_ui/application.html.erb +1181 -92
- data/app/views/roundhouse_ui/busy/index.html.erb +30 -6
- data/app/views/roundhouse_ui/dashboard/show.html.erb +62 -17
- data/app/views/roundhouse_ui/dead/index.html.erb +23 -15
- data/app/views/roundhouse_ui/errors/index.html.erb +28 -9
- data/app/views/roundhouse_ui/jobs/show.html.erb +7 -3
- data/app/views/roundhouse_ui/metrics/show.html.erb +26 -3
- data/app/views/roundhouse_ui/queues/index.html.erb +53 -13
- data/app/views/roundhouse_ui/queues/show.html.erb +55 -0
- data/app/views/roundhouse_ui/recurring/index.html.erb +71 -0
- data/app/views/roundhouse_ui/retries/index.html.erb +17 -15
- data/app/views/roundhouse_ui/scheduled/index.html.erb +10 -10
- data/app/views/roundhouse_ui/settings/show.html.erb +91 -0
- data/app/views/roundhouse_ui/shared/_filter_error.html.erb +23 -0
- data/app/views/roundhouse_ui/shared/_pager.html.erb +2 -2
- data/app/views/roundhouse_ui/shared/_search_bar.html.erb +55 -0
- data/app/views/roundhouse_ui/shared/_search_help.html.erb +37 -0
- data/app/views/roundhouse_ui/shared/_tag_filter.html.erb +2 -4
- data/app/views/roundhouse_ui/shared/bulk_preview.html.erb +69 -0
- data/config/routes.rb +9 -0
- data/lib/roundhouse_ui/backends/sidekiq.rb +124 -5
- data/lib/roundhouse_ui/backends/solid_queue.rb +54 -3
- data/lib/roundhouse_ui/demo.rb +130 -0
- data/lib/roundhouse_ui/error_groups.rb +13 -3
- data/lib/roundhouse_ui/filter_query.rb +504 -0
- data/lib/roundhouse_ui/health.rb +43 -4
- data/lib/roundhouse_ui/history.rb +54 -0
- data/lib/roundhouse_ui/icons.rb +104 -0
- data/lib/roundhouse_ui/marks/datadog-lockup-white.svg +30 -0
- data/lib/roundhouse_ui/marks/datadog-lockup.svg +41 -0
- data/lib/roundhouse_ui/observability.rb +55 -7
- data/lib/roundhouse_ui/pause.rb +6 -11
- data/lib/roundhouse_ui/queue_summary.rb +11 -0
- data/lib/roundhouse_ui/recurring.rb +137 -0
- data/lib/roundhouse_ui/runbooks.rb +75 -0
- data/lib/roundhouse_ui/snapshots.rb +13 -2
- data/lib/roundhouse_ui/tags.rb +8 -11
- data/lib/roundhouse_ui/theme.rb +298 -0
- data/lib/roundhouse_ui/version.rb +1 -1
- data/lib/roundhouse_ui.rb +200 -5
- data/lib/tasks/roundhouse_ui_tasks.rake +84 -4
- metadata +29 -7
|
@@ -6,10 +6,9 @@ module RoundhouseUi
|
|
|
6
6
|
# backend (Solid Queue) can supply duck-typed equivalents without touching
|
|
7
7
|
# controllers or views.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# Queue backend is built against it.
|
|
9
|
+
# Mostly thin — most methods return the Sidekiq object the controllers used before
|
|
10
|
+
# the port was extracted. The exceptions are the batched reads (queue_summaries)
|
|
11
|
+
# and the capability list, which the Solid Queue backend firmed up.
|
|
13
12
|
class Sidekiq
|
|
14
13
|
def name = "Sidekiq"
|
|
15
14
|
|
|
@@ -18,7 +17,14 @@ module RoundhouseUi
|
|
|
18
17
|
# fetcher), so :native_pause is withheld and the "not enforced" warning
|
|
19
18
|
# applies — but Sidekiq Pro ships its own enforced pause, so there it is
|
|
20
19
|
# advertised and the warning drops away.
|
|
21
|
-
CAPABILITIES = %i[retries dead scheduled busy workers redis capsules
|
|
20
|
+
CAPABILITIES = %i[retries dead scheduled busy workers redis capsules cancel history
|
|
21
|
+
retry_job enqueue enqueue_now snapshots].freeze
|
|
22
|
+
|
|
23
|
+
# Process-local, deliberately: this is a cheap staleness gate, not shared
|
|
24
|
+
# state. Every process reaches its own conclusion within CONCURRENCY_TTL.
|
|
25
|
+
class << self
|
|
26
|
+
attr_accessor :concurrency_cache
|
|
27
|
+
end
|
|
22
28
|
|
|
23
29
|
def supports?(capability)
|
|
24
30
|
return RoundhouseUi::Pause.native? if capability == :native_pause
|
|
@@ -29,6 +35,80 @@ module RoundhouseUi
|
|
|
29
35
|
def stats = ::Sidekiq::Stats.new
|
|
30
36
|
def queues = ::Sidekiq::Queue.all
|
|
31
37
|
def queue(name) = ::Sidekiq::Queue.new(name)
|
|
38
|
+
|
|
39
|
+
# The jobs waiting on one queue. Sidekiq::Queue is Enumerable and pages
|
|
40
|
+
# through Redis in chunks, so callers that stop early (browse fills one
|
|
41
|
+
# page and breaks) never load a large queue into memory.
|
|
42
|
+
def queue_jobs(name) = ::Sidekiq::Queue.new(name)
|
|
43
|
+
|
|
44
|
+
# Sidekiq keeps a per-day counter whether anyone reads it or not, so this
|
|
45
|
+
# is free history — no storage of ours, no migration, works everywhere.
|
|
46
|
+
# Oldest first: a chart should not have to reverse it.
|
|
47
|
+
def history(days)
|
|
48
|
+
h = ::Sidekiq::Stats::History.new(days)
|
|
49
|
+
processed = h.processed
|
|
50
|
+
failed = h.failed
|
|
51
|
+
processed.keys.sort.map do |date|
|
|
52
|
+
RoundhouseUi::Day.new(date: date, processed: processed[date].to_i, failed: failed[date].to_i)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Total worker threads across every reporting process, for the capacity
|
|
57
|
+
# figure (#36). Deliberately not Stats#workers_size, which counts threads
|
|
58
|
+
# *busy this instant* — that goes to zero on an idle fleet, and dividing a
|
|
59
|
+
# required rate by it would claim the fleet has infinite headroom.
|
|
60
|
+
#
|
|
61
|
+
# Two things this call has to get right, because it runs on the poll
|
|
62
|
+
# endpoint that every open tab hits every few seconds:
|
|
63
|
+
#
|
|
64
|
+
# * `ProcessSet.new(false)` skips Sidekiq's dead-process cleanup, which
|
|
65
|
+
# issues a SET and can issue an SREM. Roundhouse is an observer; a
|
|
66
|
+
# read endpoint must not write to Redis, least of all on a read-only
|
|
67
|
+
# install. Sidekiq's own workers prune the set.
|
|
68
|
+
# * The result is cached briefly. Thread counts change on deploy or
|
|
69
|
+
# scale, not between polls, so re-reading the process set on every
|
|
70
|
+
# poll spends round-trips on an answer that has not moved.
|
|
71
|
+
CONCURRENCY_TTL = 15.0 # seconds
|
|
72
|
+
|
|
73
|
+
def concurrency
|
|
74
|
+
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
75
|
+
cached = self.class.concurrency_cache
|
|
76
|
+
return cached[:value] if cached && now - cached[:at] < CONCURRENCY_TTL
|
|
77
|
+
|
|
78
|
+
value = ::Sidekiq::ProcessSet.new(false).sum { |p| p["concurrency"].to_i }
|
|
79
|
+
self.class.concurrency_cache = { value: value, at: now }
|
|
80
|
+
value
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Depth and latency for every queue in two Redis round-trips, regardless
|
|
86
|
+
# of how many queues there are.
|
|
87
|
+
#
|
|
88
|
+
# Sidekiq::Queue#size and #latency each issue their own command, so the
|
|
89
|
+
# obvious `queues.map { ... }` costs one SSCAN plus a LLEN and an LRANGE
|
|
90
|
+
# per queue — on an app with sixty queues, a couple of hundred round-trips
|
|
91
|
+
# to render one page. Sidekiq's own Queues#lengths pipelines the LLENs for
|
|
92
|
+
# the same reason; this pipelines the LRANGEs alongside them.
|
|
93
|
+
#
|
|
94
|
+
# Queues with no ready work are kept, at size 0: an operator has to be able
|
|
95
|
+
# to pause or snapshot a queue that happens to be empty right now.
|
|
96
|
+
def queue_summaries
|
|
97
|
+
# Sidekiq 8 ships an equivalent (Stats#queue_summaries) but 6.5 and 7 do
|
|
98
|
+
# not, and its Data objects carry a `paused` field ours cannot. Branching
|
|
99
|
+
# on it would hand callers a different shape depending on which Sidekiq
|
|
100
|
+
# the host runs, so this stays one implementation for every supported
|
|
101
|
+
# version — the reason the gem is portable in the first place.
|
|
102
|
+
names = ::Sidekiq.redis { |c| c.call("SMEMBERS", "queues") }.sort
|
|
103
|
+
return [] if names.empty?
|
|
104
|
+
|
|
105
|
+
sizes, oldest = batch_read(names).each_slice(names.size).to_a
|
|
106
|
+
names.each_with_index.map do |name, i|
|
|
107
|
+
RoundhouseUi::QueueSummary.new(name: name, size: sizes[i].to_i,
|
|
108
|
+
latency: latency_from(oldest[i]))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
32
112
|
def retry_set = ::Sidekiq::RetrySet.new
|
|
33
113
|
def dead_set = ::Sidekiq::DeadSet.new
|
|
34
114
|
def scheduled_set = ::Sidekiq::ScheduledSet.new
|
|
@@ -60,6 +140,45 @@ module RoundhouseUi
|
|
|
60
140
|
end
|
|
61
141
|
end
|
|
62
142
|
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# Everything goes through conn.call, whose splat signature is identical on
|
|
146
|
+
# redis-rb (Sidekiq 6.x) and redis-client (7+) — the same reason the rest of
|
|
147
|
+
# this gem avoids the high-level command methods. Pipelining is
|
|
148
|
+
# feature-detected the way DurationCollector does it.
|
|
149
|
+
def batch_read(names)
|
|
150
|
+
commands = names.map { |n| [ "LLEN", "queue:#{n}" ] } +
|
|
151
|
+
names.map { |n| [ "LRANGE", "queue:#{n}", -1, -1 ] }
|
|
152
|
+
::Sidekiq.redis do |conn|
|
|
153
|
+
if conn.respond_to?(:pipelined)
|
|
154
|
+
conn.pipelined { |pipe| commands.map { |c| pipe.call(*c) } }
|
|
155
|
+
else
|
|
156
|
+
commands.map { |c| conn.call(*c) }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The tail entry of a list is its oldest.
|
|
162
|
+
#
|
|
163
|
+
# Two timestamp formats exist and the difference is three orders of
|
|
164
|
+
# magnitude: Sidekiq 8 writes enqueued_at as integer epoch milliseconds,
|
|
165
|
+
# 6.5 and 7 write float epoch seconds. Treating one as the other does not
|
|
166
|
+
# produce a slightly wrong latency, it produces nonsense. This mirrors the
|
|
167
|
+
# dispatch in Sidekiq 8's own ApiUtils#calculate_latency.
|
|
168
|
+
#
|
|
169
|
+
# A payload that will not parse must not take down the page.
|
|
170
|
+
def latency_from(entry)
|
|
171
|
+
payload = entry.is_a?(Array) ? entry.first : entry
|
|
172
|
+
return 0.0 unless payload
|
|
173
|
+
|
|
174
|
+
at = RoundhouseUi.enqueued_at(::Sidekiq.load_json(payload))
|
|
175
|
+
at ? (Time.now - at).to_f : 0.0
|
|
176
|
+
rescue StandardError
|
|
177
|
+
0.0
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
public
|
|
181
|
+
|
|
63
182
|
# --- pause (via the opt-in Roundhouse fetcher; not native) ---
|
|
64
183
|
def paused_queues = RoundhouseUi::Pause.paused_set
|
|
65
184
|
def pause(name) = RoundhouseUi::Pause.pause!(name)
|
|
@@ -14,7 +14,10 @@ module RoundhouseUi
|
|
|
14
14
|
|
|
15
15
|
# No retries/redis/capsules/workers (deferred); pause is native (no fetcher
|
|
16
16
|
# warning). dead/scheduled/busy are supported.
|
|
17
|
-
|
|
17
|
+
# retry_job: SolidQueue::FailedExecution#retry exists, so dead-set retry works.
|
|
18
|
+
# Absent on purpose: :enqueue and :enqueue_now (push and add_to_queue are not
|
|
19
|
+
# implemented), :snapshots and :redis (both need Redis).
|
|
20
|
+
CAPABILITIES = %i[dead scheduled busy native_pause retry_job].freeze
|
|
18
21
|
def supports?(capability) = CAPABILITIES.include?(capability)
|
|
19
22
|
|
|
20
23
|
def stats
|
|
@@ -32,6 +35,44 @@ module RoundhouseUi
|
|
|
32
35
|
def queues = ::SolidQueue::Queue.all
|
|
33
36
|
def queue(name) = ::SolidQueue::Queue.new(name)
|
|
34
37
|
|
|
38
|
+
# The jobs waiting on one queue, in the same JobSet shape the other sets
|
|
39
|
+
# use — so the queue detail page reuses the shared browse/search path
|
|
40
|
+
# rather than growing a second one.
|
|
41
|
+
# Solid Queue's processes table records threads per worker the same way.
|
|
42
|
+
def concurrency
|
|
43
|
+
sq(:Process).where(kind: "Worker").sum { |p| p.metadata&.dig("thread_pool_size").to_i }
|
|
44
|
+
rescue StandardError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def queue_jobs(name) = JobSet.new(sq(:ReadyExecution).queued_as(name))
|
|
49
|
+
|
|
50
|
+
# Depth and latency for every queue in two queries, not two per queue.
|
|
51
|
+
#
|
|
52
|
+
# SolidQueue::Queue#size and #latency each run their own COUNT and MIN, so
|
|
53
|
+
# rendering the Queues page over sixty queues meant well over a hundred
|
|
54
|
+
# round-trips to the queue database. One GROUP BY answers both for every
|
|
55
|
+
# queue at once.
|
|
56
|
+
#
|
|
57
|
+
# Queue.all is still consulted, because it lists queues by distinct job
|
|
58
|
+
# name — including ones with nothing ready right now — and an operator has
|
|
59
|
+
# to be able to pause or clear a queue that happens to be empty.
|
|
60
|
+
def queue_summaries
|
|
61
|
+
counts = sq(:ReadyExecution).group(:queue_name)
|
|
62
|
+
.pluck(Arel.sql("queue_name, COUNT(*), MIN(created_at)"))
|
|
63
|
+
.to_h { |name, size, oldest| [ name, [ size, oldest ] ] }
|
|
64
|
+
|
|
65
|
+
names = (::SolidQueue::Queue.all.map(&:name) | counts.keys).sort
|
|
66
|
+
now = Time.current
|
|
67
|
+
names.map do |name|
|
|
68
|
+
size, oldest = counts[name]
|
|
69
|
+
RoundhouseUi::QueueSummary.new(
|
|
70
|
+
name: name, size: size.to_i,
|
|
71
|
+
latency: oldest ? (now - oldest).to_i : 0
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
35
76
|
# No retry set in Solid Queue — surfaced via supports?(:retries) => false.
|
|
36
77
|
def retry_set = EMPTY_SET
|
|
37
78
|
|
|
@@ -105,8 +146,18 @@ module RoundhouseUi
|
|
|
105
146
|
}
|
|
106
147
|
end
|
|
107
148
|
|
|
108
|
-
|
|
109
|
-
|
|
149
|
+
# Guarded at CALL time. As a trailing `if` on an endless def this was evaluated
|
|
150
|
+
# when the class body loaded, against a nil class-level ivar — so the method
|
|
151
|
+
# was never defined and every Retry raised NoMethodError.
|
|
152
|
+
def retry
|
|
153
|
+
raise NotImplementedError, "this entry cannot be retried" unless @execution.respond_to?(:retry)
|
|
154
|
+
|
|
155
|
+
@execution.retry
|
|
156
|
+
end
|
|
157
|
+
# discard removes the job row along with the execution and takes the
|
|
158
|
+
# row lock while it does; destroy drops only the execution, leaving an
|
|
159
|
+
# orphaned solid_queue_jobs row that nothing will ever pick up again.
|
|
160
|
+
def delete = @execution.respond_to?(:discard) ? @execution.discard : @execution.destroy
|
|
110
161
|
|
|
111
162
|
private
|
|
112
163
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module RoundhouseUi
|
|
7
|
+
# Workers that do real work and really fail, so a console has something to show.
|
|
8
|
+
#
|
|
9
|
+
# NOT loaded by `require "roundhouse_ui"`. Ask for it explicitly, in an
|
|
10
|
+
# initializer you would not ship:
|
|
11
|
+
#
|
|
12
|
+
# require "roundhouse_ui/demo" if Rails.env.development?
|
|
13
|
+
#
|
|
14
|
+
# Then drive it with `rake roundhouse_ui:demo:load[10]`.
|
|
15
|
+
#
|
|
16
|
+
# Each class refuses to run outside development or test. A worker that sleeps
|
|
17
|
+
# for twelve seconds and then raises is a fine thing to have in a demo and a
|
|
18
|
+
# bad thing to have reachable in production, and "nobody will enqueue it" is
|
|
19
|
+
# not a control.
|
|
20
|
+
module Demo
|
|
21
|
+
class Base
|
|
22
|
+
include Sidekiq::Job
|
|
23
|
+
|
|
24
|
+
# Three attempts, so a failing job reaches the dead set inside a short
|
|
25
|
+
# demo instead of backing off for hours.
|
|
26
|
+
sidekiq_options retry: 3
|
|
27
|
+
|
|
28
|
+
# Subclasses override. Milliseconds of work, and the chance of failing.
|
|
29
|
+
DURATION = (100..400)
|
|
30
|
+
FAILURE_RATE = 0.0
|
|
31
|
+
ERRORS = [ [ RuntimeError, "something went wrong" ] ].freeze
|
|
32
|
+
|
|
33
|
+
def perform(payload = {})
|
|
34
|
+
unless %w[development test].include?(ENV["RAILS_ENV"] || Rails.env.to_s)
|
|
35
|
+
raise "RoundhouseUi::Demo workers do not run outside development"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
work_for(self.class::DURATION)
|
|
39
|
+
maybe_fail(payload)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Real elapsed time, and real CPU — a job that only sleeps shows up on the
|
|
45
|
+
# Busy page but never in a flame graph, and the point is to look like work.
|
|
46
|
+
def work_for(range)
|
|
47
|
+
budget = rand(range) / 1000.0
|
|
48
|
+
deadline = now + budget
|
|
49
|
+
digest = SecureRandom.hex(8)
|
|
50
|
+
while now < deadline
|
|
51
|
+
200.times { digest = Digest::SHA256.hexdigest(digest) }
|
|
52
|
+
sleep 0.01
|
|
53
|
+
end
|
|
54
|
+
digest
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def maybe_fail(payload)
|
|
58
|
+
return if rand >= self.class::FAILURE_RATE
|
|
59
|
+
|
|
60
|
+
klass, message = self.class::ERRORS.sample
|
|
61
|
+
raise klass, "#{message} (account #{payload['account_id'] || '—'})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A payment path: quick, and fails often enough to group.
|
|
68
|
+
class CheckoutWorker < Base
|
|
69
|
+
OWNER = :core
|
|
70
|
+
QUEUE = "stripe"
|
|
71
|
+
DURATION = (200..1_200)
|
|
72
|
+
FAILURE_RATE = 0.18
|
|
73
|
+
ERRORS = [
|
|
74
|
+
[ Timeout::Error, "Stripe API timed out after 8s" ],
|
|
75
|
+
[ ArgumentError, "no such price" ]
|
|
76
|
+
].freeze
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Mail: fast and mostly reliable, so the throughput line has volume.
|
|
80
|
+
class DigestMailerWorker < Base
|
|
81
|
+
OWNER = :growth
|
|
82
|
+
QUEUE = "invite_mailer"
|
|
83
|
+
DURATION = (60..300)
|
|
84
|
+
FAILURE_RATE = 0.04
|
|
85
|
+
ERRORS = [ [ IOError, "421 4.7.0 Try again later" ] ].freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Search reindex: slow enough to sit on the Busy page while you look at it.
|
|
89
|
+
class ReindexWorker < Base
|
|
90
|
+
OWNER = :platform
|
|
91
|
+
QUEUE = "default"
|
|
92
|
+
DURATION = (1_500..5_000)
|
|
93
|
+
FAILURE_RATE = 0.09
|
|
94
|
+
ERRORS = [ [ IOError, "connection reset by peer" ] ].freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The long one: guaranteed to be mid-flight whenever you open Busy.
|
|
98
|
+
class TranscodeWorker < Base
|
|
99
|
+
OWNER = :platform
|
|
100
|
+
QUEUE = "uploaded_files"
|
|
101
|
+
DURATION = (6_000..20_000)
|
|
102
|
+
FAILURE_RATE = 0.07
|
|
103
|
+
ERRORS = [ [ RuntimeError, "ffmpeg exited with status 1" ] ].freeze
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The flaky one: the group that dominates the Errors page.
|
|
107
|
+
class EmbeddingWorker < Base
|
|
108
|
+
OWNER = :ai
|
|
109
|
+
QUEUE = "ai"
|
|
110
|
+
DURATION = (400..2_500)
|
|
111
|
+
FAILURE_RATE = 0.24
|
|
112
|
+
ERRORS = [
|
|
113
|
+
[ Timeout::Error, "embedding endpoint timed out" ],
|
|
114
|
+
[ RuntimeError, "429 rate limited" ],
|
|
115
|
+
[ KeyError, "key not found: :embedding" ]
|
|
116
|
+
].freeze
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class GamificationWorker < Base
|
|
120
|
+
OWNER = :training
|
|
121
|
+
QUEUE = "gamification"
|
|
122
|
+
DURATION = (100..600)
|
|
123
|
+
FAILURE_RATE = 0.06
|
|
124
|
+
ERRORS = [ [ RuntimeError, "badge already awarded" ] ].freeze
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
ALL = [ CheckoutWorker, DigestMailerWorker, ReindexWorker,
|
|
128
|
+
TranscodeWorker, EmbeddingWorker, GamificationWorker ].freeze
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -62,9 +62,19 @@ module RoundhouseUi
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def record(groups, source, entry)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
# The bucket key and the stored label must unwrap together — split them and
|
|
66
|
+
# rows merge under one name while a different one renders. Everything
|
|
67
|
+
# downstream reads group[:klass]: the label, the search haystack, the tag
|
|
68
|
+
# lookup, the Datadog link, and the dashboard's errors_path(q:) href.
|
|
69
|
+
#
|
|
70
|
+
# item is bound once because Solid Queue's Entry#item builds a fresh Hash
|
|
71
|
+
# and touches the execution on every call, and this runs up to
|
|
72
|
+
# DEFAULT_SCAN_LIMIT times per render.
|
|
73
|
+
item = entry.item
|
|
74
|
+
klass = RoundhouseUi.unwrapped_class(entry.klass, item)
|
|
75
|
+
error = item["error_class"] || "UnknownError"
|
|
76
|
+
group = (groups["#{klass}|#{error}"] ||= {
|
|
77
|
+
klass: klass, error: error, count: 0, last_at: nil, queues: [], sources: []
|
|
68
78
|
})
|
|
69
79
|
group[:count] += 1
|
|
70
80
|
group[:queues] |= [ entry.queue ]
|