cosmonats 0.4.0 → 0.4.1
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/lib/cosmo/api/stream.rb +36 -6
- data/lib/cosmo/cli.rb +1 -1
- data/lib/cosmo/client.rb +4 -2
- data/lib/cosmo/job/processor.rb +19 -7
- data/lib/cosmo/sentry/auto.rb +6 -0
- data/lib/cosmo/sentry/job_processor_middleware.rb +66 -0
- data/lib/cosmo/version.rb +1 -1
- data/lib/cosmo/web/assets/app.css +17 -0
- data/lib/cosmo/web/views/jobs/_enqueued.erb +3 -0
- data/lib/cosmo/web/views/jobs/_stats.erb +5 -1
- data/lib/cosmo/web/views/layout.erb +1 -0
- data/sig/cosmo/api/stream.rbs +6 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bb0db4ef3d9dac8fe723ab7440ccc6b2fcd3d5c746efd2cb3dd4c5a28c2d6f6
|
|
4
|
+
data.tar.gz: 06b65ca804dda9a66bc239feefb54cb9bdad2c23d63a0e514c97c33f602616f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0525004e2df9b1d37488e698a83ce5decf8681a673e6d66e720dc74788c78f7ebd16abd63510f830a62a8efbdad09bcc06278747e8423bce892bcaa9663f43ad
|
|
7
|
+
data.tar.gz: b1b522586095c52f72631c80f1e6d8f22fc8853450515ecf49c91469bed0cd2269273cb0a2de95f46fd8c6e18d9a71e3ee7b5c9fad25cc60bf16cd2a62d2c1c2
|
data/lib/cosmo/api/stream.rb
CHANGED
|
@@ -45,21 +45,24 @@ module Cosmo
|
|
|
45
45
|
|
|
46
46
|
def retries
|
|
47
47
|
client.list_consumers(name).sum { _1["num_redelivered"].to_i }
|
|
48
|
+
rescue NATS::Error
|
|
49
|
+
0
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def each
|
|
51
53
|
return if total.zero?
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
current
|
|
55
|
-
last = state.last_seq.to_i
|
|
55
|
+
candidates = {}
|
|
56
|
+
current, last, subjects = scan_range
|
|
56
57
|
|
|
57
58
|
loop do
|
|
58
59
|
break if current > last
|
|
59
60
|
|
|
60
|
-
job =
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
subject, job = next_candidate(subjects, candidates, current)
|
|
62
|
+
break unless job
|
|
63
|
+
|
|
64
|
+
candidates.delete(subject)
|
|
65
|
+
current = job.seq.to_i + 1
|
|
63
66
|
|
|
64
67
|
yield job
|
|
65
68
|
end
|
|
@@ -120,6 +123,33 @@ module Cosmo
|
|
|
120
123
|
|
|
121
124
|
private
|
|
122
125
|
|
|
126
|
+
def scan_range
|
|
127
|
+
data = info
|
|
128
|
+
state = data[:state]
|
|
129
|
+
current = @offset || state.first_seq.to_i
|
|
130
|
+
subjects = Array(data[:config].subjects).reject { _1.start_with?(Cron::Entry::SUBJECT_PREFIX) }
|
|
131
|
+
[current, state.last_seq.to_i, subjects]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Lowest-seq message at or after current across all job subject filters,
|
|
135
|
+
# caching each subject's next candidate so it isn't re-queried every step.
|
|
136
|
+
def next_candidate(subjects, candidates, current)
|
|
137
|
+
subjects.each do |subject|
|
|
138
|
+
next if candidates.key?(subject)
|
|
139
|
+
|
|
140
|
+
candidates[subject] = next_message(subject, current)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
candidates.compact.min_by { |_, msg| msg.seq.to_i }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Jump straight to the next message on the subject after seq, skipping any acked/deleted gaps
|
|
147
|
+
def next_message(subject, seq)
|
|
148
|
+
Job.new(name, client.get_message(name, next: true, seq: seq, subject: subject, direct: true))
|
|
149
|
+
rescue NATS::JetStream::Error::NotFound
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
123
153
|
def client
|
|
124
154
|
self.class.client
|
|
125
155
|
end
|
data/lib/cosmo/cli.rb
CHANGED
|
@@ -124,7 +124,7 @@ module Cosmo
|
|
|
124
124
|
class_name = entry.delete(:class)
|
|
125
125
|
API::Cron.instance.upsert!(**entry, name: name, class_name: class_name)
|
|
126
126
|
sum + 1
|
|
127
|
-
end
|
|
127
|
+
end.to_i
|
|
128
128
|
|
|
129
129
|
puts "Cron sync complete: #{schedules} schedule(s) registered" unless schedules.zero?
|
|
130
130
|
puts "Cosmo streams#{" and cron schedules" unless schedules.zero?} set up successfully."
|
data/lib/cosmo/client.rb
CHANGED
|
@@ -69,6 +69,8 @@ module Cosmo
|
|
|
69
69
|
return [] if data.nil? || data["streams"].nil?
|
|
70
70
|
|
|
71
71
|
data["streams"]
|
|
72
|
+
rescue NATS::Error
|
|
73
|
+
[]
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
def pause_stream(name)
|
|
@@ -101,8 +103,8 @@ module Cosmo
|
|
|
101
103
|
js.consumer_info(stream_name, consumer_name)
|
|
102
104
|
end
|
|
103
105
|
|
|
104
|
-
def get_message(
|
|
105
|
-
js.get_msg(
|
|
106
|
+
def get_message(stream_name, **options)
|
|
107
|
+
js.get_msg(stream_name, **options)
|
|
106
108
|
end
|
|
107
109
|
|
|
108
110
|
def delete_message(name, seq)
|
data/lib/cosmo/job/processor.rb
CHANGED
|
@@ -79,13 +79,10 @@ module Cosmo
|
|
|
79
79
|
sw = stopwatch
|
|
80
80
|
Logger.with(jid: data[:jid])
|
|
81
81
|
Logger.info "start"
|
|
82
|
-
|
|
83
|
-
instance.jid = data[:jid]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
else
|
|
87
|
-
instance.perform(*data[:args])
|
|
88
|
-
end
|
|
82
|
+
|
|
83
|
+
instance = worker_class.new.tap { |w| w.jid = data[:jid] }
|
|
84
|
+
perform_job(instance, data: data, message: message, duration: duration)
|
|
85
|
+
|
|
89
86
|
message.ack
|
|
90
87
|
Logger.with(elapsed: sw.elapsed_seconds) { Logger.info "done" }
|
|
91
88
|
true
|
|
@@ -190,6 +187,21 @@ module Cosmo
|
|
|
190
187
|
API::Counter.instance.with(&block)
|
|
191
188
|
end
|
|
192
189
|
end
|
|
190
|
+
|
|
191
|
+
# @param job_instance [Cosmo::Job]
|
|
192
|
+
# @param data [Hash]
|
|
193
|
+
# @param message [NATS::Msg]
|
|
194
|
+
# @param duration [Float, nil]
|
|
195
|
+
#
|
|
196
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
197
|
+
def perform_job(job_instance, data:, message:, duration: nil)
|
|
198
|
+
if duration
|
|
199
|
+
Timeout.timeout(duration) { job_instance.perform(*data[:args]) }
|
|
200
|
+
else
|
|
201
|
+
job_instance.perform(*data[:args])
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
193
205
|
end
|
|
194
206
|
end
|
|
195
207
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
4
|
+
module Cosmo
|
|
5
|
+
module Sentry
|
|
6
|
+
module JobProcessorMiddleware
|
|
7
|
+
NAME_PREFIX = "Cosmonats"
|
|
8
|
+
OP_NAME = "queue.cosmonats"
|
|
9
|
+
SPAN_ORIGIN = "auto.queue.cosmonats"
|
|
10
|
+
STATUS_OK = 200
|
|
11
|
+
STATUS_FAIL = 500
|
|
12
|
+
|
|
13
|
+
# @param job_instance [Cosmo::Job]
|
|
14
|
+
# @param data [Hash]
|
|
15
|
+
# @param message [NATS::Msg]
|
|
16
|
+
# @param duration [Float, nil]
|
|
17
|
+
def perform_job(job_instance, data:, message:, duration: nil)
|
|
18
|
+
unless ::Sentry.initialized?
|
|
19
|
+
super
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
scope = ::Sentry.get_current_scope
|
|
24
|
+
transaction_name = "#{NAME_PREFIX}/#{job_instance.class.name}"
|
|
25
|
+
scope.set_transaction_name(transaction_name, source: :task)
|
|
26
|
+
|
|
27
|
+
transaction = ::Sentry.start_transaction(
|
|
28
|
+
name: scope.transaction_name,
|
|
29
|
+
source: scope.transaction_source,
|
|
30
|
+
op: OP_NAME,
|
|
31
|
+
origin: SPAN_ORIGIN
|
|
32
|
+
)
|
|
33
|
+
transaction&.set_data("messaging.message.id", data[:jid])
|
|
34
|
+
transaction&.set_data("messaging.destination.name", "#{message.metadata.stream}:#{message.subject}")
|
|
35
|
+
transaction&.set_data("messaging.message.retry.count", data[:retry] || 0)
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
super
|
|
39
|
+
|
|
40
|
+
transaction&.set_http_status(STATUS_OK)
|
|
41
|
+
transaction&.finish
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
::Sentry.capture_exception(
|
|
44
|
+
e,
|
|
45
|
+
contexts: {
|
|
46
|
+
cosmonats: data.merge(
|
|
47
|
+
nats_stream: message.metadata.stream,
|
|
48
|
+
nats_subject: message.subject,
|
|
49
|
+
timeout_duration: duration
|
|
50
|
+
)
|
|
51
|
+
},
|
|
52
|
+
hint: {
|
|
53
|
+
background: true,
|
|
54
|
+
integration: "cosmonats"
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
transaction&.set_http_status(STATUS_FAIL)
|
|
58
|
+
transaction&.finish
|
|
59
|
+
|
|
60
|
+
raise e
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
data/lib/cosmo/version.rb
CHANGED
|
@@ -509,6 +509,23 @@ details code { display: block; max-width: 400px; white-space: pre-wrap; }
|
|
|
509
509
|
.htmx-request .htmx-indicator,
|
|
510
510
|
.htmx-request.htmx-indicator { opacity: 1; }
|
|
511
511
|
|
|
512
|
+
#global-spinner {
|
|
513
|
+
position: fixed;
|
|
514
|
+
top: var(--space-2x);
|
|
515
|
+
right: var(--space-2x);
|
|
516
|
+
width: 24px;
|
|
517
|
+
height: 24px;
|
|
518
|
+
border: 3px solid var(--color-border);
|
|
519
|
+
border-top-color: var(--color-primary);
|
|
520
|
+
border-radius: 50%;
|
|
521
|
+
animation: spin 0.6s linear infinite;
|
|
522
|
+
z-index: 100;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
@keyframes spin {
|
|
526
|
+
to { transform: rotate(360deg); }
|
|
527
|
+
}
|
|
528
|
+
|
|
512
529
|
/* ── Responsive ────────────────────────────────────────────────────────── */
|
|
513
530
|
@media (max-width: 768px) {
|
|
514
531
|
.nav { flex-wrap: wrap; }
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
hx-target="#content"
|
|
6
6
|
hx-swap="innerHTML"
|
|
7
7
|
hx-push-url="true"
|
|
8
|
+
hx-indicator="#global-spinner"
|
|
8
9
|
class="btn <%= "btn-primary" if @stream_name.to_s == stream_name.to_s %>">
|
|
9
10
|
<%= h(stream_name) %>
|
|
10
11
|
</button>
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
<a hx-get="<%= url_for('/jobs/_enqueued', stream_name: @stream_name, page: @page - 1, limit: @limit) %>"
|
|
67
68
|
hx-target="#enqueued-poller"
|
|
68
69
|
hx-swap="outerHTML"
|
|
70
|
+
hx-indicator="#global-spinner"
|
|
69
71
|
class="btn">← Prev</a>
|
|
70
72
|
<% else -%>
|
|
71
73
|
<span class="btn btn-disabled">← Prev</span>
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
<a hx-get="<%= url_for('/jobs/_enqueued', stream_name: @stream_name, page: @page + 1, limit: @limit) %>"
|
|
78
80
|
hx-target="#enqueued-poller"
|
|
79
81
|
hx-swap="outerHTML"
|
|
82
|
+
hx-indicator="#global-spinner"
|
|
80
83
|
class="btn">Next →</a>
|
|
81
84
|
<% else -%>
|
|
82
85
|
<span class="btn btn-disabled">Next →</span>
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
<a href="<%= url_for('/jobs/busy') %>"
|
|
20
20
|
hx-get="<%= url_for('/jobs/busy') %>"
|
|
21
21
|
hx-target="#content"
|
|
22
|
-
hx-push-url="true"
|
|
22
|
+
hx-push-url="true"
|
|
23
|
+
hx-indicator="#global-spinner">⏳ Busy</a>
|
|
23
24
|
</p>
|
|
24
25
|
</article>
|
|
25
26
|
<article class="stat-card">
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
hx-get="<%= url_for('/jobs/enqueued') %>"
|
|
30
31
|
hx-target="#content"
|
|
31
32
|
hx-push-url="true"
|
|
33
|
+
hx-indicator="#global-spinner"
|
|
32
34
|
class="<%= 'active' if referrer?('/jobs/enqueued') %>">📬 Enqueued</a>
|
|
33
35
|
</p>
|
|
34
36
|
</article>
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
hx-get="<%= url_for('/jobs/scheduled') %>"
|
|
40
42
|
hx-target="#content"
|
|
41
43
|
hx-push-url="true"
|
|
44
|
+
hx-indicator="#global-spinner"
|
|
42
45
|
class="<%= 'active' if referrer?('/jobs/scheduled') %>">⏰ Scheduled</a>
|
|
43
46
|
</p>
|
|
44
47
|
</article>
|
|
@@ -49,6 +52,7 @@
|
|
|
49
52
|
hx-get="<%= url_for('/jobs/dead') %>"
|
|
50
53
|
hx-target="#content"
|
|
51
54
|
hx-push-url="true"
|
|
55
|
+
hx-indicator="#global-spinner"
|
|
52
56
|
class="<%= 'active' if referrer?('/jobs/dead') %>">💀 Dead</a>
|
|
53
57
|
</p>
|
|
54
58
|
</article>
|
data/sig/cosmo/api/stream.rbs
CHANGED
|
@@ -44,6 +44,12 @@ module Cosmo
|
|
|
44
44
|
|
|
45
45
|
private
|
|
46
46
|
|
|
47
|
+
def scan_range: () -> [::Integer, ::Integer, Array[::String]]
|
|
48
|
+
|
|
49
|
+
def next_candidate: (Array[::String] subjects, Hash[::String, Job?] candidates, ::Integer current) -> [::String, Job]?
|
|
50
|
+
|
|
51
|
+
def next_message: (::String subject, ::Integer seq) -> Job?
|
|
52
|
+
|
|
47
53
|
def client: () -> Client
|
|
48
54
|
end
|
|
49
55
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cosmonats
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Vorotilin
|
|
@@ -88,6 +88,8 @@ files:
|
|
|
88
88
|
- lib/cosmo/processor.rb
|
|
89
89
|
- lib/cosmo/publisher.rb
|
|
90
90
|
- lib/cosmo/railtie.rb
|
|
91
|
+
- lib/cosmo/sentry/auto.rb
|
|
92
|
+
- lib/cosmo/sentry/job_processor_middleware.rb
|
|
91
93
|
- lib/cosmo/stream.rb
|
|
92
94
|
- lib/cosmo/stream/data.rb
|
|
93
95
|
- lib/cosmo/stream/message.rb
|
|
@@ -197,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
197
199
|
- !ruby/object:Gem::Version
|
|
198
200
|
version: '0'
|
|
199
201
|
requirements: []
|
|
200
|
-
rubygems_version: 4.0.
|
|
202
|
+
rubygems_version: 4.0.16
|
|
201
203
|
specification_version: 4
|
|
202
204
|
summary: Lightweight background and stream processing
|
|
203
205
|
test_files: []
|