cosmonats 0.6.0 → 0.7.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/LICENSE.txt +21 -169
- data/README.md +101 -17
- data/lib/cosmo/api/counter.rb +14 -11
- data/lib/cosmo/api/cron/entry.rb +23 -3
- data/lib/cosmo/api/cron.rb +22 -26
- data/lib/cosmo/api/job.rb +30 -0
- data/lib/cosmo/api/kv.rb +31 -11
- data/lib/cosmo/api/registry.rb +68 -0
- data/lib/cosmo/api/stats/busy.rb +63 -0
- data/lib/cosmo/api/stats/processes.rb +37 -0
- data/lib/cosmo/api/stats/totals.rb +35 -0
- data/lib/cosmo/api/stats.rb +5 -4
- data/lib/cosmo/api/stream.rb +1 -1
- data/lib/cosmo/api.rb +3 -2
- data/lib/cosmo/batch.rb +9 -2
- data/lib/cosmo/cli.rb +27 -7
- data/lib/cosmo/client.rb +33 -5
- data/lib/cosmo/config.rb +6 -2
- data/lib/cosmo/engine.rb +103 -4
- data/lib/cosmo/heartbeat.rb +129 -0
- data/lib/cosmo/http_server/handler.rb +44 -0
- data/lib/cosmo/http_server/health.rb +39 -0
- data/lib/cosmo/http_server/ping.rb +23 -0
- data/lib/cosmo/http_server.rb +62 -0
- data/lib/cosmo/job/failure.rb +52 -0
- data/lib/cosmo/job/processor.rb +87 -25
- data/lib/cosmo/job/stream_filter.rb +54 -0
- data/lib/cosmo/job.rb +2 -0
- data/lib/cosmo/processor.rb +39 -4
- data/lib/cosmo/sentry/job_processor_middleware.rb +1 -2
- data/lib/cosmo/stream/message.rb +2 -1
- data/lib/cosmo/stream/processor.rb +5 -0
- data/lib/cosmo/utils/json.rb +1 -1
- data/lib/cosmo/utils/overrides.rb +16 -10
- data/lib/cosmo/utils/thread_pool.rb +5 -0
- data/lib/cosmo/version.rb +1 -1
- data/lib/cosmo/web/assets/app.css +21 -0
- data/lib/cosmo/web/controllers/jobs.rb +28 -4
- data/lib/cosmo/web/helpers/application.rb +10 -1
- data/lib/cosmo/web/views/batches/index.erb +1 -1
- data/lib/cosmo/web/views/jobs/_busy.erb +70 -4
- data/lib/cosmo/web/views/jobs/_dead.erb +29 -16
- data/lib/cosmo/web/views/jobs/_enqueued.erb +10 -0
- data/lib/cosmo/web/views/jobs/_processes.erb +131 -0
- data/lib/cosmo/web/views/jobs/_scheduled.erb +1 -1
- data/lib/cosmo/web/views/jobs/busy.erb +1 -1
- data/lib/cosmo/web/views/jobs/dead.erb +2 -2
- data/lib/cosmo/web/views/jobs/enqueued.erb +1 -1
- data/lib/cosmo/web/views/jobs/index.erb +7 -1
- data/lib/cosmo/web/views/jobs/scheduled.erb +1 -1
- data/lib/cosmo/web/views/streams/index.erb +1 -1
- data/lib/cosmo/web.rb +2 -0
- data/lib/cosmo.rb +13 -0
- data/sig/cosmo/api/counter.rbs +2 -4
- data/sig/cosmo/api/cron/entry.rbs +2 -0
- data/sig/cosmo/api/cron.rbs +4 -2
- data/sig/cosmo/api/job.rbs +12 -0
- data/sig/cosmo/api/kv.rbs +7 -1
- data/sig/cosmo/api/registry.rbs +30 -0
- data/sig/cosmo/api/stats/busy.rbs +31 -0
- data/sig/cosmo/api/stats/processes.rbs +16 -0
- data/sig/cosmo/api/stats/totals.rbs +17 -0
- data/sig/cosmo/cli.rbs +4 -0
- data/sig/cosmo/client.rbs +12 -1
- data/sig/cosmo/engine.rbs +29 -0
- data/sig/cosmo/heartbeat.rbs +36 -0
- data/sig/cosmo/http_server/handler.rbs +17 -0
- data/sig/cosmo/http_server/health.rbs +21 -0
- data/sig/cosmo/http_server/ping.rbs +14 -0
- data/sig/cosmo/http_server.rbs +22 -0
- data/sig/cosmo/job/failure.rbs +28 -0
- data/sig/cosmo/job/processor.rbs +17 -1
- data/sig/cosmo/job/stream_filter.rbs +25 -0
- data/sig/cosmo/processor.rbs +12 -1
- data/sig/cosmo/stream/message.rbs +3 -1
- data/sig/cosmo/stream/processor.rbs +2 -0
- data/sig/cosmo/utils/thread_pool.rbs +3 -0
- data/sig/cosmo.rbs +4 -0
- metadata +33 -20
- data/lib/cosmo/api/busy.rb +0 -66
- data/sig/cosmo/api/busy.rbs +0 -35
data/lib/cosmo/processor.rb
CHANGED
|
@@ -5,20 +5,29 @@ module Cosmo
|
|
|
5
5
|
STREAM_PAUSED_RECHECK_TTL = "5s" # How long a stream's paused state is cached before re-checking (override via COSMO_STREAM_PAUSED_RECHECK_TTL)
|
|
6
6
|
STREAMS_PAUSED_IDLE_SLEEP = "1s" # How long to sleep when every stream is paused, preventing a tight CPU spin (override via COSMO_STREAMS_PAUSED_IDLE_SLEEP)
|
|
7
7
|
STREAM_EMPTY_BACKOFF_MAX = "5s" # Max sleep between empty fetches (override via COSMO_STREAM_EMPTY_BACKOFF_MAX)
|
|
8
|
+
QUIET_SLEEP = "5s" # How long to sleep between checks while in quiet mode (override via COSMO_QUIET_IDLE_SLEEP)
|
|
8
9
|
|
|
9
10
|
def self.run(...)
|
|
10
11
|
new(...).tap(&:run)
|
|
11
12
|
end
|
|
12
13
|
|
|
14
|
+
# Checks the command-line options this processor understands, before anything is booted or connected.
|
|
15
|
+
#
|
|
16
|
+
# @param options [Hash]
|
|
17
|
+
# @return [void]
|
|
18
|
+
def self.validate_options!(_options); end
|
|
19
|
+
|
|
13
20
|
attr_reader :consumers
|
|
14
21
|
|
|
15
|
-
def initialize(pool, running, options)
|
|
22
|
+
def initialize(pool, running, options, quiet:)
|
|
16
23
|
@pool = pool
|
|
17
24
|
@running = running
|
|
25
|
+
@quiet = quiet
|
|
18
26
|
@options = options
|
|
19
27
|
@threads = []
|
|
20
28
|
@consumers = []
|
|
21
29
|
@cache = Utils::TTLCache.new
|
|
30
|
+
@busy = Concurrent::AtomicFixnum.new
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
def run
|
|
@@ -29,6 +38,13 @@ module Cosmo
|
|
|
29
38
|
run_loop
|
|
30
39
|
end
|
|
31
40
|
|
|
41
|
+
# Threads processing fetched messages right now; threads waiting on a fetch don't count.
|
|
42
|
+
#
|
|
43
|
+
# @return [Integer]
|
|
44
|
+
def busy
|
|
45
|
+
@busy.value
|
|
46
|
+
end
|
|
47
|
+
|
|
32
48
|
def stop(timeout = Config[:timeout])
|
|
33
49
|
@running.make_false
|
|
34
50
|
@pool.shutdown
|
|
@@ -52,6 +68,11 @@ module Cosmo
|
|
|
52
68
|
while running?
|
|
53
69
|
break if shutdown
|
|
54
70
|
|
|
71
|
+
if quiet?
|
|
72
|
+
sleep(Utils::Duration.parse(ENV.fetch("COSMO_QUIET_IDLE_SLEEP", QUIET_SLEEP)))
|
|
73
|
+
next
|
|
74
|
+
end
|
|
75
|
+
|
|
55
76
|
all_empty = true # every stream is empty
|
|
56
77
|
all_paused = true # every stream is paused
|
|
57
78
|
consumers.each do |(subscription, config, processor)| # rubocop:disable Metrics/BlockLength
|
|
@@ -80,15 +101,18 @@ module Cosmo
|
|
|
80
101
|
|
|
81
102
|
timeout = fetch_timeout(config)
|
|
82
103
|
Logger.trace "fetching #{fetch_subjects(config).inspect}, timeout=#{timeout}"
|
|
104
|
+
started_at = Time.now
|
|
83
105
|
messages = fetch(subscription, batch_size: config[:batch_size], timeout:)
|
|
84
106
|
Logger.trace "fetched (#{messages&.size.to_i}) messages"
|
|
85
107
|
if messages&.any?
|
|
86
108
|
consumer_state.delete(stream_name)
|
|
87
|
-
process(messages, processor)
|
|
109
|
+
track_busy { process(messages, processor) }
|
|
88
110
|
else
|
|
89
111
|
max_backoff = Utils::Duration.parse(ENV.fetch("COSMO_STREAM_EMPTY_BACKOFF_MAX", STREAM_EMPTY_BACKOFF_MAX))
|
|
90
|
-
consumer_state.compute(stream_name) do |
|
|
91
|
-
count
|
|
112
|
+
consumer_state.compute(stream_name) do |(count, wake_at)|
|
|
113
|
+
next [count, wake_at] if wake_at && wake_at > started_at
|
|
114
|
+
|
|
115
|
+
count = count.to_i + 1
|
|
92
116
|
backoff = [timeout * (2**(count - 1)), max_backoff].min
|
|
93
117
|
[count, Time.now + backoff]
|
|
94
118
|
end
|
|
@@ -129,10 +153,21 @@ module Cosmo
|
|
|
129
153
|
raise NotImplementedError
|
|
130
154
|
end
|
|
131
155
|
|
|
156
|
+
def track_busy
|
|
157
|
+
@busy.increment
|
|
158
|
+
yield
|
|
159
|
+
ensure
|
|
160
|
+
@busy.decrement
|
|
161
|
+
end
|
|
162
|
+
|
|
132
163
|
def running?
|
|
133
164
|
@running.true?
|
|
134
165
|
end
|
|
135
166
|
|
|
167
|
+
def quiet?
|
|
168
|
+
@quiet.true?
|
|
169
|
+
end
|
|
170
|
+
|
|
136
171
|
def scheduler?
|
|
137
172
|
false
|
|
138
173
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
3
|
+
# rubocop:disable-next Metrics/MethodLength, Metrics/AbcSize
|
|
4
4
|
module Cosmo
|
|
5
5
|
module Sentry
|
|
6
6
|
module JobProcessorMiddleware
|
|
@@ -63,4 +63,3 @@ module Cosmo
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
data/lib/cosmo/stream/message.rb
CHANGED
|
@@ -7,8 +7,9 @@ module Cosmo
|
|
|
7
7
|
class Message
|
|
8
8
|
extend Forwardable
|
|
9
9
|
|
|
10
|
-
delegate %i[subject reply header metadata ack
|
|
10
|
+
delegate %i[subject reply header metadata ack nak term in_progress] => :@msg
|
|
11
11
|
delegate %i[timestamp num_delivered num_pending] => :metadata
|
|
12
|
+
alias nack nak
|
|
12
13
|
|
|
13
14
|
def initialize(msg, serializer: nil)
|
|
14
15
|
@msg = msg
|
data/lib/cosmo/utils/json.rb
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
Cosmo::Utils::Warnings.silence do
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
unless NATS::JetStream::API::StreamConfig.members.include?(:allow_msg_schedules)
|
|
5
|
+
members = NATS::JetStream::API::StreamConfig.members + %i[allow_msg_counter allow_msg_schedules]
|
|
6
|
+
NATS::JetStream::API::StreamConfig = Struct.new(*members, keyword_init: true) do
|
|
7
|
+
def initialize(opts = {})
|
|
8
|
+
rem = opts.keys - members
|
|
9
|
+
opts.delete_if { |k| rem.include?(k) }
|
|
10
|
+
super
|
|
11
|
+
end
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
unless NATS::JetStream::PubAck.members.include?(:val)
|
|
16
|
+
members = NATS::JetStream::PubAck.members + [:val]
|
|
17
|
+
NATS::JetStream::PubAck = Struct.new(*members, keyword_init: true)
|
|
18
|
+
end
|
|
15
19
|
end
|
|
16
20
|
|
|
17
|
-
# Upstream bug in nats-pure 2.5.0 (https://github.com/nats-io/nats.rb):
|
|
21
|
+
# Upstream bug in nats-pure 2.5.0 and 2.6.0 (https://github.com/nats-io/nats-pure.rb):
|
|
18
22
|
# NATS::JetStream::PullSubscription#fetch only assigns its `start_time` local in the
|
|
19
23
|
# `batch > 1` branch, but the final "did we time out" check outside the case statement
|
|
20
24
|
# reads it unconditionally. For `batch == 1` (what Cosmo::Processor always uses),
|
|
@@ -44,7 +48,9 @@ end
|
|
|
44
48
|
#
|
|
45
49
|
# Vendored copy of nats-pure's PullSubscription#fetch, kept as close to
|
|
46
50
|
# upstream as possible (two one-line fixes, see comments above), so it's easy to diff against the
|
|
47
|
-
# next nats-pure release and drop once fixed there.
|
|
51
|
+
# next nats-pure release and drop once fixed there. Fixed on nats-pure main by the fetch rewrite
|
|
52
|
+
# (#181), recognized by its private #take_pending, in which case this copy is not loaded.
|
|
53
|
+
return if NATS::JetStream.const_get(:PullSubscription).private_method_defined?(:take_pending)
|
|
48
54
|
|
|
49
55
|
# rubocop:disable all
|
|
50
56
|
module NATS
|
|
@@ -16,11 +16,16 @@ module Cosmo
|
|
|
16
16
|
|
|
17
17
|
def initialize(concurrency)
|
|
18
18
|
@mutex = Thread::Mutex.new
|
|
19
|
+
@concurrency = concurrency
|
|
19
20
|
@available = concurrency
|
|
20
21
|
@cond = ConditionVariable.new
|
|
21
22
|
@pool = Concurrent::FixedThreadPool.new(concurrency)
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def wait_idle
|
|
26
|
+
@mutex.synchronize { @cond.wait(@mutex) until @available == @concurrency }
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
def post
|
|
25
30
|
@mutex.synchronize do
|
|
26
31
|
@cond.wait(@mutex) while @available <= 0
|
data/lib/cosmo/version.rb
CHANGED
|
@@ -163,6 +163,14 @@ button:hover, .btn:hover {
|
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
.btn-sm {
|
|
167
|
+
box-shadow: none;
|
|
168
|
+
font-size: var(--font-size-small);
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
line-height: var(--space-2x);
|
|
171
|
+
padding: var(--space-1-2) var(--space);
|
|
172
|
+
}
|
|
173
|
+
|
|
166
174
|
/* ── Badges ────────────────────────────────────────────────────────────── */
|
|
167
175
|
.badge {
|
|
168
176
|
border-radius: var(--space-1-2);
|
|
@@ -343,6 +351,12 @@ section .pending {
|
|
|
343
351
|
align-items: center;
|
|
344
352
|
}
|
|
345
353
|
|
|
354
|
+
section .pending .poll-status {
|
|
355
|
+
align-items: center;
|
|
356
|
+
display: inline-flex;
|
|
357
|
+
gap: var(--space);
|
|
358
|
+
}
|
|
359
|
+
|
|
346
360
|
/* ── Stat cards ────────────────────────────────────────────────────────── */
|
|
347
361
|
.cards-container {
|
|
348
362
|
display: grid;
|
|
@@ -479,6 +493,13 @@ time {
|
|
|
479
493
|
font-size: var(--font-size-small);
|
|
480
494
|
word-wrap: break-word;
|
|
481
495
|
}
|
|
496
|
+
.error-message details code {
|
|
497
|
+
max-width: 600px;
|
|
498
|
+
max-height: 320px;
|
|
499
|
+
overflow: auto;
|
|
500
|
+
white-space: pre;
|
|
501
|
+
font-size: var(--font-size-small);
|
|
502
|
+
}
|
|
482
503
|
|
|
483
504
|
/* ── Links ─────────────────────────────────────────────────────────────── */
|
|
484
505
|
.stream-link { color: var(--color-primary); font-weight: 600; }
|
|
@@ -54,6 +54,13 @@ module Cosmo
|
|
|
54
54
|
ok
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def delete_enqueued
|
|
58
|
+
seq = path.split("/").last.to_i
|
|
59
|
+
stream_name, = streams
|
|
60
|
+
API::Stream.new(stream_name).delete(seq)
|
|
61
|
+
_enqueued
|
|
62
|
+
end
|
|
63
|
+
|
|
57
64
|
def _scheduled
|
|
58
65
|
stream = API::Stream.new("scheduled")
|
|
59
66
|
jobs = stream.messages(page: params["page"], limit: params["limit"])
|
|
@@ -67,9 +74,19 @@ module Cosmo
|
|
|
67
74
|
end
|
|
68
75
|
|
|
69
76
|
def _busy
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
busy = API::Stats::Busy.instance
|
|
78
|
+
total = busy.size
|
|
79
|
+
page, limit, total_pages = paginate(total, API::Stats::Busy::LIMIT)
|
|
80
|
+
polling = params["poll"].to_s != "0"
|
|
81
|
+
ok render("jobs/_busy", { jobs: busy.list(page:, limit:), total:, page:, limit:, total_pages:, polling: })
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def _processes
|
|
85
|
+
processes = API::Stats::Processes.instance
|
|
86
|
+
total = processes.size
|
|
87
|
+
page, limit, total_pages = paginate(total, API::Stats::Processes::LIMIT)
|
|
88
|
+
polling = params["poll"].to_s != "0"
|
|
89
|
+
ok render("jobs/_processes", { processes: processes.list(page:, limit:), total:, page:, limit:, total_pages:, polling: })
|
|
73
90
|
end
|
|
74
91
|
|
|
75
92
|
def _enqueued # rubocop:disable Metrics/AbcSize
|
|
@@ -80,8 +97,9 @@ module Cosmo
|
|
|
80
97
|
unless stream_name.to_s.empty?
|
|
81
98
|
stream = API::Stream.new(stream_name)
|
|
82
99
|
total = stream.total
|
|
83
|
-
jobs = stream.messages(page:, limit:)
|
|
84
100
|
total_pages = (total.to_f / limit).ceil
|
|
101
|
+
page = page.clamp(1, [total_pages, 1].max)
|
|
102
|
+
jobs = stream.messages(page:, limit:)
|
|
85
103
|
end
|
|
86
104
|
|
|
87
105
|
ok render("jobs/_enqueued", { jobs:, total:, stream_name:, stream_names:, page:, limit:, total_pages: })
|
|
@@ -93,6 +111,12 @@ module Cosmo
|
|
|
93
111
|
|
|
94
112
|
private
|
|
95
113
|
|
|
114
|
+
def paginate(total, default_limit)
|
|
115
|
+
limit = (params["limit"] || default_limit).to_i
|
|
116
|
+
total_pages = (total.to_f / limit).ceil
|
|
117
|
+
[params["page"].to_i.clamp(1, [total_pages, 1].max), limit, total_pages]
|
|
118
|
+
end
|
|
119
|
+
|
|
96
120
|
def streams
|
|
97
121
|
stream_names = API::Stream.jobs.map(&:name)
|
|
98
122
|
stream_name = params.fetch("stream_name", stream_names.first)
|
|
@@ -8,6 +8,13 @@ module Cosmo
|
|
|
8
8
|
module Application
|
|
9
9
|
include Renderer
|
|
10
10
|
|
|
11
|
+
# Seconds between htmx auto-refreshes. Override with COSMO_WEB_POLL_INTERVAL.
|
|
12
|
+
POLL_INTERVAL = 5
|
|
13
|
+
|
|
14
|
+
def poll_interval
|
|
15
|
+
@poll_interval ||= ENV.fetch("COSMO_WEB_POLL_INTERVAL", POLL_INTERVAL).to_i
|
|
16
|
+
end
|
|
17
|
+
|
|
11
18
|
def render(template, locals = nil)
|
|
12
19
|
defaults = { request: @request }
|
|
13
20
|
locals = Hash(locals).merge(defaults)
|
|
@@ -42,8 +49,10 @@ module Cosmo
|
|
|
42
49
|
"#{elapsed}s"
|
|
43
50
|
elsif elapsed < 3600
|
|
44
51
|
"#{elapsed / 60}m #{elapsed % 60}s"
|
|
45
|
-
|
|
52
|
+
elsif elapsed < 86_400
|
|
46
53
|
"#{elapsed / 3600}h #{(elapsed % 3600) / 60}m"
|
|
54
|
+
else
|
|
55
|
+
"#{elapsed / 86_400}d #{(elapsed % 86_400) / 3600}h"
|
|
47
56
|
end
|
|
48
57
|
end
|
|
49
58
|
|
|
@@ -1,10 +1,29 @@
|
|
|
1
|
+
<% nav = @polling ? { limit: @limit } : { limit: @limit, poll: 0 } -%>
|
|
1
2
|
<div id="busy-poller"
|
|
2
|
-
|
|
3
|
-
hx-
|
|
4
|
-
hx-
|
|
3
|
+
<% if @polling -%>
|
|
4
|
+
hx-get="<%= url_for('/jobs/_busy', page: @page, limit: @limit) %>"
|
|
5
|
+
hx-trigger="every <%= poll_interval %>s"
|
|
6
|
+
hx-swap="outerHTML"
|
|
7
|
+
<% end -%>
|
|
8
|
+
>
|
|
5
9
|
<div class="pending">
|
|
6
10
|
<span></span>
|
|
7
|
-
<span class="
|
|
11
|
+
<span class="poll-status">
|
|
12
|
+
<span class="text-muted"><%= @total %> job(s) running</span>
|
|
13
|
+
<% if @polling -%>
|
|
14
|
+
<a class="btn btn-sm"
|
|
15
|
+
hx-get="<%= url_for('/jobs/_busy', nav.merge(page: @page, poll: 0)) %>"
|
|
16
|
+
hx-target="#busy-poller"
|
|
17
|
+
hx-swap="outerHTML"
|
|
18
|
+
hx-indicator="#global-spinner">⏹ Stop Polling</a>
|
|
19
|
+
<% else -%>
|
|
20
|
+
<a class="btn btn-sm btn-primary"
|
|
21
|
+
hx-get="<%= url_for('/jobs/_busy', page: @page, limit: @limit) %>"
|
|
22
|
+
hx-target="#busy-poller"
|
|
23
|
+
hx-swap="outerHTML"
|
|
24
|
+
hx-indicator="#global-spinner">▶ Live Poll</a>
|
|
25
|
+
<% end -%>
|
|
26
|
+
</span>
|
|
8
27
|
</div>
|
|
9
28
|
|
|
10
29
|
<% if @jobs.empty? -%>
|
|
@@ -18,6 +37,7 @@
|
|
|
18
37
|
<th>JID[Args]</th>
|
|
19
38
|
<th>Stream</th>
|
|
20
39
|
<th>Worker</th>
|
|
40
|
+
<th>Attempt</th>
|
|
21
41
|
<th>Running for</th>
|
|
22
42
|
</tr>
|
|
23
43
|
</thead>
|
|
@@ -43,6 +63,14 @@
|
|
|
43
63
|
<code><%= h(job[:stream].to_s) %></code>
|
|
44
64
|
</td>
|
|
45
65
|
<td><code style="font-size: var(--font-size-small);"><%= h(job[:worker].to_s) %></code></td>
|
|
66
|
+
<td>
|
|
67
|
+
<% delivery = job[:delivery].to_i -%>
|
|
68
|
+
<% if delivery > 1 -%>
|
|
69
|
+
<span class="badge <%= delivery >= 5 ? 'badge-danger' : 'badge-warning' %>"><%= delivery %></span>
|
|
70
|
+
<% else -%>
|
|
71
|
+
<span class="text-muted"><%= delivery.zero? ? '—' : delivery %></span>
|
|
72
|
+
<% end -%>
|
|
73
|
+
</td>
|
|
46
74
|
<td>
|
|
47
75
|
<%= elapsed(job[:started_at]) %>
|
|
48
76
|
</td>
|
|
@@ -52,4 +80,42 @@
|
|
|
52
80
|
</table>
|
|
53
81
|
</div>
|
|
54
82
|
<% end -%>
|
|
83
|
+
|
|
84
|
+
<% if @total_pages.to_i > 1 -%>
|
|
85
|
+
<div class="pagination">
|
|
86
|
+
<% if @page > 1 -%>
|
|
87
|
+
<a hx-get="<%= url_for('/jobs/_busy', nav.merge(page: @page - 1)) %>"
|
|
88
|
+
hx-target="#busy-poller"
|
|
89
|
+
hx-swap="outerHTML"
|
|
90
|
+
hx-indicator="#global-spinner"
|
|
91
|
+
class="btn">← Prev</a>
|
|
92
|
+
<% else -%>
|
|
93
|
+
<span class="btn btn-disabled">← Prev</span>
|
|
94
|
+
<% end -%>
|
|
95
|
+
|
|
96
|
+
<% pages(@page, @total_pages).each do |p| -%>
|
|
97
|
+
<% if p == :gap -%>
|
|
98
|
+
<span class="pagination-gap">…</span>
|
|
99
|
+
<% elsif p == @page -%>
|
|
100
|
+
<span class="btn btn-primary btn-disabled"><%= p %></span>
|
|
101
|
+
<% else -%>
|
|
102
|
+
<a hx-get="<%= url_for('/jobs/_busy', nav.merge(page: p)) %>"
|
|
103
|
+
hx-target="#busy-poller"
|
|
104
|
+
hx-swap="outerHTML"
|
|
105
|
+
hx-indicator="#global-spinner"
|
|
106
|
+
class="btn"><%= p %></a>
|
|
107
|
+
<% end -%>
|
|
108
|
+
<% end -%>
|
|
109
|
+
|
|
110
|
+
<% if @page < @total_pages -%>
|
|
111
|
+
<a hx-get="<%= url_for('/jobs/_busy', nav.merge(page: @page + 1)) %>"
|
|
112
|
+
hx-target="#busy-poller"
|
|
113
|
+
hx-swap="outerHTML"
|
|
114
|
+
hx-indicator="#global-spinner"
|
|
115
|
+
class="btn">Next →</a>
|
|
116
|
+
<% else -%>
|
|
117
|
+
<span class="btn btn-disabled">Next →</span>
|
|
118
|
+
<% end -%>
|
|
119
|
+
</div>
|
|
120
|
+
<% end -%>
|
|
55
121
|
</div>
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
<div id="dead-
|
|
2
|
-
hx-get="<%= url_for('/jobs/_dead') %>"
|
|
3
|
-
hx-trigger="every 5s"
|
|
4
|
-
hx-swap="outerHTML">
|
|
1
|
+
<div id="dead-jobs">
|
|
5
2
|
<% if @jobs.empty? -%>
|
|
6
3
|
<div class="alert alert-success">No dead jobs found</div>
|
|
7
4
|
<% else -%>
|
|
@@ -35,31 +32,47 @@
|
|
|
35
32
|
</details>
|
|
36
33
|
</div>
|
|
37
34
|
</td>
|
|
38
|
-
<td>
|
|
39
|
-
|
|
35
|
+
<td style="white-space: nowrap;">
|
|
36
|
+
<% if job.x_stream -%><code><%= h(job.x_stream) %></code><% else -%><span class="text-muted">—</span><% end -%>
|
|
40
37
|
</td>
|
|
41
|
-
<td style="
|
|
42
|
-
<% if job.
|
|
38
|
+
<td style="min-width: 420px;">
|
|
39
|
+
<% if job.error? -%>
|
|
43
40
|
<div class="error-message">
|
|
44
|
-
<strong><%= h(job.
|
|
45
|
-
<p><%= h(job.
|
|
41
|
+
<strong><%= h(job.error_class.to_s.empty? ? "Error" : job.error_class) %></strong>
|
|
42
|
+
<p><%= h(job.error_message) %></p>
|
|
43
|
+
<% if job.error_backtrace -%>
|
|
44
|
+
<details>
|
|
45
|
+
<summary>Backtrace</summary>
|
|
46
|
+
<code><%= h(job.error_backtrace_lines.join("\n")) %></code>
|
|
47
|
+
</details>
|
|
48
|
+
<% end -%>
|
|
49
|
+
</div>
|
|
50
|
+
<% else -%>
|
|
51
|
+
<div class="error-message">
|
|
52
|
+
<strong>Unknown</strong>
|
|
53
|
+
<p>No error recorded for this job</p>
|
|
46
54
|
</div>
|
|
47
55
|
<% end -%>
|
|
48
56
|
</td>
|
|
49
57
|
<td style="white-space: nowrap;">
|
|
50
|
-
|
|
58
|
+
<% if (failed_at = job.stored_at) -%>
|
|
59
|
+
<time datetime="<%= h(job.timestamp) %>" title="<%= h(job.timestamp) %>"><%= h(format_timestamp(failed_at)) %></time>
|
|
60
|
+
<div class="text-muted" style="font-size: var(--font-size-small);"><%= elapsed(failed_at) %> ago</div>
|
|
61
|
+
<% else -%>
|
|
62
|
+
<span class="text-muted">—</span>
|
|
63
|
+
<% end -%>
|
|
51
64
|
</td>
|
|
52
|
-
<td>
|
|
65
|
+
<td style="white-space: nowrap;">
|
|
53
66
|
<a hx-patch="<%= url_for("/jobs/retry/#{job.seq}") %>"
|
|
54
67
|
hx-target="#dead-row-<%= job.seq %>"
|
|
55
68
|
hx-swap="innerHTML"
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
title="Retry"
|
|
70
|
+
class="btn btn-primary btn-sm">↻</a>
|
|
58
71
|
<a hx-delete="<%= url_for("/jobs/delete/#{job.seq}") %>"
|
|
59
72
|
hx-target="#dead-row-<%= job.seq %>"
|
|
60
73
|
hx-swap="innerHTML"
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
title="Delete"
|
|
75
|
+
class="btn btn-danger btn-sm">✕</a>
|
|
63
76
|
</td>
|
|
64
77
|
</tr>
|
|
65
78
|
<% end -%>
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
<th>JID[Args]</th>
|
|
31
31
|
<th>Stream</th>
|
|
32
32
|
<th>Seq</th>
|
|
33
|
+
<th>Actions</th>
|
|
33
34
|
</tr>
|
|
34
35
|
</thead>
|
|
35
36
|
<tbody>
|
|
@@ -56,6 +57,15 @@
|
|
|
56
57
|
<td>
|
|
57
58
|
<code><%= job.seq %></code>
|
|
58
59
|
</td>
|
|
60
|
+
<td>
|
|
61
|
+
<a hx-delete="<%= url_for("/jobs/enqueued/#{job.seq}", { stream_name: @stream_name, page: @page, limit: @limit }) %>"
|
|
62
|
+
hx-target="#enqueued-poller"
|
|
63
|
+
hx-swap="outerHTML"
|
|
64
|
+
hx-indicator="#global-spinner"
|
|
65
|
+
hx-confirm="Remove message #<%= job.seq %> from <%= h(@stream_name) %>?"
|
|
66
|
+
title="Remove message"
|
|
67
|
+
class="btn btn-danger btn-sm">✕</a>
|
|
68
|
+
</td>
|
|
59
69
|
</tr>
|
|
60
70
|
<% end -%>
|
|
61
71
|
</tbody>
|