cosmonats 0.2.0 → 0.3.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 +127 -66
- data/lib/cosmo/api/kv.rb +1 -1
- data/lib/cosmo/api/stream.rb +17 -4
- data/lib/cosmo/cli.rb +3 -2
- data/lib/cosmo/client.rb +27 -3
- data/lib/cosmo/config.rb +5 -32
- data/lib/cosmo/engine.rb +1 -1
- data/lib/cosmo/job/processor.rb +34 -59
- data/lib/cosmo/logger.rb +4 -1
- data/lib/cosmo/processor.rb +109 -1
- data/lib/cosmo/stream/processor.rb +23 -59
- data/lib/cosmo/stream.rb +2 -2
- data/lib/cosmo/utils/hash.rb +3 -27
- data/lib/cosmo/utils/ttl_cache.rb +44 -0
- data/lib/cosmo/utils.rb +1 -0
- data/lib/cosmo/version.rb +1 -1
- data/lib/cosmo/web/assets/app.css +46 -0
- data/lib/cosmo/web/controllers/streams.rb +36 -10
- data/lib/cosmo/web/helpers/application.rb +13 -2
- data/lib/cosmo/web/views/streams/_info.erb +3 -0
- data/lib/cosmo/web/views/streams/_pause_banner.erb +17 -0
- data/lib/cosmo/web/views/streams/_stream_row.erb +42 -0
- data/lib/cosmo/web/views/streams/_table.erb +4 -21
- data/lib/cosmo/web.rb +2 -0
- data/sig/cosmo/api/stream.rbs +7 -1
- data/sig/cosmo/client.rbs +11 -3
- data/sig/cosmo/config.rbs +3 -15
- data/sig/cosmo/job/processor.rbs +16 -8
- data/sig/cosmo/processor.rbs +26 -0
- data/sig/cosmo/stream/processor.rbs +4 -10
- data/sig/cosmo/utils/hash.rbs +0 -8
- data/sig/cosmo/utils/ttl_cache.rbs +20 -0
- metadata +6 -3
- data/lib/cosmo/defaults.yml +0 -70
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f850638b66506538b131180c03b64a13e31cd86f924503703812da49b6ba3d20
|
|
4
|
+
data.tar.gz: 0dfdab08193600f3bca2c32558ffb016a38657f9488789770eea5e60612281e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56f46ac8a8c1409b1583033ad8b5cf9e513652fae2643b07344aa8f8bf1cca80f948463af92e2a989ec9679cc830ac05a6b873b1f0c9b5e10a1e6ef5e8399ad0
|
|
7
|
+
data.tar.gz: '086ae5c5b26471417e54c37d2f782c0c103340953e9a27ca8c02131478122f01ede30f2f8ab5a0b29c81ce7b4ea07a5c8de8e7f51957e4d9d5dfd5d670a0ff08'
|
data/README.md
CHANGED
|
@@ -95,62 +95,69 @@ gem "cosmonats"
|
|
|
95
95
|
|
|
96
96
|
**Requirements:** Ruby 3.1.0+, NATS Server ([installation guide](https://docs.nats.io/running-a-nats-service/introduction/installation))
|
|
97
97
|
|
|
98
|
+
Add these lines to config/routes.rb:
|
|
99
|
+
```ruby
|
|
100
|
+
require "cosmo/web"
|
|
101
|
+
|
|
102
|
+
Rails.application.routes.draw do
|
|
103
|
+
mount Cosmo::Web => "/cosmo" # access web UI at http://localhost:3000/cosmo
|
|
104
|
+
...
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
98
108
|
|
|
99
109
|
## 🚀 Quick Start
|
|
100
110
|
|
|
101
|
-
### 1. Create
|
|
111
|
+
### 1. Create `config/cosmo.yml` and run `bundle exec cosmo -S` to create streams in NATS:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
concurrency: 5
|
|
115
|
+
max_retries: 3
|
|
116
|
+
|
|
117
|
+
consumers:
|
|
118
|
+
jobs:
|
|
119
|
+
default:
|
|
120
|
+
ack_policy: explicit
|
|
121
|
+
max_deliver: 10
|
|
122
|
+
max_ack_pending: 10
|
|
123
|
+
ack_wait: 15
|
|
124
|
+
subject: jobs.%{name}.>
|
|
125
|
+
|
|
126
|
+
setup:
|
|
127
|
+
jobs:
|
|
128
|
+
default:
|
|
129
|
+
storage: file
|
|
130
|
+
retention: workqueue
|
|
131
|
+
subjects: ["jobs.%{name}.>"]
|
|
132
|
+
allow_direct: true
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 2. Create a Job in app/workers
|
|
102
136
|
|
|
103
137
|
```ruby
|
|
104
138
|
class SendEmailJob
|
|
105
139
|
include Cosmo::Job
|
|
106
140
|
|
|
107
|
-
# configure job options (optional)
|
|
108
141
|
options stream: :default, retry: 3, dead: true
|
|
109
142
|
|
|
110
143
|
def perform(user_id, email_type)
|
|
111
|
-
user
|
|
112
|
-
|
|
144
|
+
# Pretend to send email to user: UserMailer.send(email_type, user_id).deliver_now
|
|
145
|
+
sleep 0.5 # Simulate work
|
|
146
|
+
puts "#{user_id}, #{email_type}"
|
|
113
147
|
end
|
|
114
148
|
end
|
|
115
149
|
```
|
|
116
150
|
|
|
117
|
-
###
|
|
151
|
+
### 3. Enqueue Jobs
|
|
118
152
|
|
|
119
153
|
```ruby
|
|
120
|
-
SendEmailJob.perform_async(
|
|
121
|
-
SendEmailJob.perform_in(1.hour, 123, 'reminder') # Delayed
|
|
122
|
-
SendEmailJob.perform_at(1.day.from_now, 123, 'test') # Scheduled
|
|
154
|
+
10.times { |i| SendEmailJob.perform_async(i, "welcome") }
|
|
123
155
|
```
|
|
124
156
|
|
|
125
|
-
###
|
|
126
|
-
|
|
127
|
-
```yaml
|
|
128
|
-
concurrency: 10
|
|
129
|
-
max_retries: 3
|
|
130
|
-
|
|
131
|
-
consumers:
|
|
132
|
-
jobs:
|
|
133
|
-
default:
|
|
134
|
-
ack_policy: explicit
|
|
135
|
-
max_deliver: 3
|
|
136
|
-
max_ack_pending: 3
|
|
137
|
-
ack_wait: 60
|
|
138
|
-
|
|
139
|
-
streams:
|
|
140
|
-
default:
|
|
141
|
-
storage: file
|
|
142
|
-
retention: workqueue
|
|
143
|
-
subjects: ["jobs.default.>"]
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### 4. Setup & Run
|
|
157
|
+
### 4. Run
|
|
147
158
|
|
|
148
159
|
```bash
|
|
149
|
-
|
|
150
|
-
cosmo -C config/cosmo.yml --setup
|
|
151
|
-
|
|
152
|
-
# Start processing
|
|
153
|
-
cosmo -C config/cosmo.yml -c 10 -r ./app/jobs jobs
|
|
160
|
+
bundle exec cosmo jobs
|
|
154
161
|
```
|
|
155
162
|
|
|
156
163
|
|
|
@@ -182,7 +189,7 @@ end
|
|
|
182
189
|
# Usage
|
|
183
190
|
ReportJob.perform_async(42) # Enqueue now
|
|
184
191
|
ReportJob.perform_in(30.minutes, 42) # Delayed
|
|
185
|
-
ReportJob.perform_at(Time.parse(
|
|
192
|
+
ReportJob.perform_at(Time.parse("2026-01-25 10:00"), 42) # Scheduled
|
|
186
193
|
```
|
|
187
194
|
|
|
188
195
|
### Streams
|
|
@@ -221,8 +228,8 @@ end
|
|
|
221
228
|
|
|
222
229
|
# Publishing
|
|
223
230
|
ClicksProcessor.publish(
|
|
224
|
-
{ user_id: 123, page:
|
|
225
|
-
subject:
|
|
231
|
+
{ user_id: 123, page: "/home" },
|
|
232
|
+
subject: "events.clicks.homepage"
|
|
226
233
|
)
|
|
227
234
|
|
|
228
235
|
# Message acknowledgment strategies
|
|
@@ -235,32 +242,86 @@ message.term # Permanent failure, no retry
|
|
|
235
242
|
|
|
236
243
|
**File-based (config/cosmo.yml):**
|
|
237
244
|
```yaml
|
|
238
|
-
timeout: 25
|
|
239
|
-
concurrency:
|
|
240
|
-
max_retries: 3
|
|
245
|
+
timeout: 25 # Shutdown timeout in seconds
|
|
246
|
+
concurrency: &concurrency 1 # Number of worker threads
|
|
247
|
+
max_retries: &max_retries 3 # Default max retries
|
|
248
|
+
|
|
249
|
+
stream_config: &stream_config
|
|
250
|
+
storage: file # storage type (file or memory)
|
|
251
|
+
retention: workqueue # retention policy (limits, interest, workqueue)
|
|
252
|
+
duplicate_window: 120 # time window for duplicate message detection in seconds
|
|
253
|
+
discard: old # discard new messages when stream is full (discard new or old)
|
|
254
|
+
allow_direct: true # allow direct messages to stream, required for web UI
|
|
255
|
+
subjects:
|
|
256
|
+
- jobs.%{name}.> # subject pattern for stream, %{name} will be replaced with stream name
|
|
257
|
+
|
|
258
|
+
consumer_config: &consumer_config
|
|
259
|
+
ack_policy: explicit # ack policy (explicit, none, all), each individual message must be acknowledged
|
|
260
|
+
max_deliver: 10 # maximum number of times a message will be delivered before it's considered failed
|
|
261
|
+
max_ack_pending: 20 # maximum number of messages with pending ack for this consumer
|
|
262
|
+
ack_wait: 60 # time in seconds to wait for an ack before redelivering the message
|
|
263
|
+
subject: jobs.%{name}.> # subject pattern for consumer, %{name} will be replaced with stream name
|
|
241
264
|
|
|
242
265
|
consumers:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
266
|
+
jobs:
|
|
267
|
+
critical:
|
|
268
|
+
<<: *consumer_config
|
|
269
|
+
priority: 50
|
|
270
|
+
high:
|
|
271
|
+
<<: *consumer_config
|
|
272
|
+
priority: 30
|
|
273
|
+
default:
|
|
274
|
+
<<: *consumer_config
|
|
275
|
+
priority: 15
|
|
276
|
+
low:
|
|
277
|
+
<<: *consumer_config
|
|
278
|
+
priority: 5
|
|
279
|
+
scheduled:
|
|
280
|
+
<<: *consumer_config
|
|
281
|
+
max_deliver: 1
|
|
282
|
+
max_ack_pending: 100
|
|
283
|
+
ack_wait: 10
|
|
250
284
|
|
|
251
285
|
setup:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
286
|
+
jobs:
|
|
287
|
+
critical:
|
|
288
|
+
<<: *stream_config
|
|
289
|
+
description: Very critical priority jobs
|
|
290
|
+
high:
|
|
291
|
+
<<: *stream_config
|
|
292
|
+
description: Higher priority jobs
|
|
293
|
+
default:
|
|
294
|
+
<<: *stream_config
|
|
295
|
+
description: Default priority jobs
|
|
296
|
+
low:
|
|
297
|
+
<<: *stream_config
|
|
298
|
+
description: Lower priority jobs
|
|
299
|
+
scheduled:
|
|
300
|
+
<<: *stream_config
|
|
301
|
+
description: Scheduled jobs
|
|
302
|
+
dead:
|
|
303
|
+
<<: *stream_config
|
|
304
|
+
retention: limits
|
|
305
|
+
max_msgs: 10000
|
|
306
|
+
max_age: 604800 # 7d
|
|
307
|
+
description: Broken jobs (DLQ)
|
|
308
|
+
|
|
309
|
+
development:
|
|
310
|
+
verbose: false
|
|
311
|
+
concurrency: *concurrency
|
|
312
|
+
|
|
313
|
+
staging:
|
|
314
|
+
verbose: true
|
|
315
|
+
concurrency: 3
|
|
316
|
+
|
|
317
|
+
production:
|
|
318
|
+
concurrency: 3
|
|
258
319
|
```
|
|
259
320
|
|
|
260
321
|
**Programmatic:**
|
|
261
322
|
```ruby
|
|
262
323
|
Cosmo::Config.set(:concurrency, 20)
|
|
263
|
-
Cosmo::Config.set(:setup, :streams, :custom, { storage:
|
|
324
|
+
Cosmo::Config.set(:setup, :streams, :custom, { storage: "file", subjects: ["custom.>"] })
|
|
264
325
|
```
|
|
265
326
|
|
|
266
327
|
**Environment variables:**
|
|
@@ -328,10 +389,10 @@ end
|
|
|
328
389
|
**Testing:**
|
|
329
390
|
```ruby
|
|
330
391
|
# Synchronous execution
|
|
331
|
-
SendEmailJob.perform_sync(123,
|
|
392
|
+
SendEmailJob.perform_sync(123, "test")
|
|
332
393
|
|
|
333
394
|
# Test job creation
|
|
334
|
-
jid = SendEmailJob.perform_async(123,
|
|
395
|
+
jid = SendEmailJob.perform_async(123, "welcome")
|
|
335
396
|
assert_kind_of String, jid
|
|
336
397
|
```
|
|
337
398
|
|
|
@@ -438,7 +499,7 @@ sudo systemctl status cosmo
|
|
|
438
499
|
**Stream Metrics:**
|
|
439
500
|
```ruby
|
|
440
501
|
client = Cosmo::Client.instance
|
|
441
|
-
info = client.stream_info(
|
|
502
|
+
info = client.stream_info("default")
|
|
442
503
|
|
|
443
504
|
info.state.messages # Total messages
|
|
444
505
|
info.state.bytes # Total bytes
|
|
@@ -465,8 +526,8 @@ class EmailJob
|
|
|
465
526
|
end
|
|
466
527
|
end
|
|
467
528
|
|
|
468
|
-
EmailJob.perform_async(123,
|
|
469
|
-
EmailJob.perform_in(1.day, 123,
|
|
529
|
+
EmailJob.perform_async(123, "welcome")
|
|
530
|
+
EmailJob.perform_in(1.day, 123, "followup")
|
|
470
531
|
```
|
|
471
532
|
|
|
472
533
|
**Image Processing Pipeline:**
|
|
@@ -475,12 +536,12 @@ class ImageProcessor
|
|
|
475
536
|
include Cosmo::Stream
|
|
476
537
|
options(
|
|
477
538
|
stream: :images,
|
|
478
|
-
consumer: { subjects: [
|
|
539
|
+
consumer: { subjects: ["images.uploaded.>"] }
|
|
479
540
|
)
|
|
480
541
|
|
|
481
542
|
def process_one
|
|
482
|
-
processed = ImageService.process(message.data[
|
|
483
|
-
publish(processed, subject:
|
|
543
|
+
processed = ImageService.process(message.data["url"])
|
|
544
|
+
publish(processed, subject: "images.processed.optimized")
|
|
484
545
|
message.ack
|
|
485
546
|
rescue => e
|
|
486
547
|
logger.error "Processing failed: #{e.message}"
|
|
@@ -488,18 +549,18 @@ class ImageProcessor
|
|
|
488
549
|
end
|
|
489
550
|
end
|
|
490
551
|
|
|
491
|
-
ImageProcessor.publish({ url:
|
|
552
|
+
ImageProcessor.publish({ url: "https://example.com/image.jpg" }, subject: "images.uploaded.user")
|
|
492
553
|
```
|
|
493
554
|
|
|
494
555
|
**Real-Time Analytics:**
|
|
495
556
|
```ruby
|
|
496
557
|
class AnalyticsAggregator
|
|
497
558
|
include Cosmo::Stream
|
|
498
|
-
options batch_size: 1000, consumer: { subjects: [
|
|
559
|
+
options batch_size: 1000, consumer: { subjects: ["events.*.>"] }
|
|
499
560
|
|
|
500
561
|
def process(messages)
|
|
501
562
|
events = messages.map(&:data)
|
|
502
|
-
aggregates = events.group_by { |e| e[
|
|
563
|
+
aggregates = events.group_by { |e| e["type"] }.transform_values(&:count)
|
|
503
564
|
Analytics.bulk_insert(aggregates)
|
|
504
565
|
messages.each(&:ack)
|
|
505
566
|
end
|
data/lib/cosmo/api/kv.rb
CHANGED
data/lib/cosmo/api/stream.rb
CHANGED
|
@@ -10,12 +10,13 @@ module Cosmo
|
|
|
10
10
|
include Enumerable
|
|
11
11
|
|
|
12
12
|
def self.all
|
|
13
|
-
client.list_streams.
|
|
13
|
+
client.list_streams.map { new(_1.dig("config", "name")) }
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def self.jobs
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
client.list_streams.select { _1.dig("config", "metadata", "_cosmo.type") == "jobs" }
|
|
18
|
+
.reject { %w[scheduled dead].include?(_1.dig("config", "name")) }
|
|
19
|
+
.map { new(_1.dig("config", "name")) }
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def self.client
|
|
@@ -41,7 +42,7 @@ module Cosmo
|
|
|
41
42
|
alias size total
|
|
42
43
|
|
|
43
44
|
def retries
|
|
44
|
-
client.list_consumers(name).sum {
|
|
45
|
+
client.list_consumers(name).sum { _1["num_redelivered"].to_i }
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def each
|
|
@@ -100,6 +101,18 @@ module Cosmo
|
|
|
100
101
|
client.delete_message(name, seq)
|
|
101
102
|
end
|
|
102
103
|
|
|
104
|
+
def pause!
|
|
105
|
+
client.pause_stream(name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def unpause!
|
|
109
|
+
client.unpause_stream(name)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def paused?
|
|
113
|
+
client.stream_paused?(name)
|
|
114
|
+
end
|
|
115
|
+
|
|
103
116
|
private
|
|
104
117
|
|
|
105
118
|
def client
|
data/lib/cosmo/cli.rb
CHANGED
|
@@ -106,11 +106,12 @@ module Cosmo
|
|
|
106
106
|
configs.each do |name, config|
|
|
107
107
|
Client.instance.stream_info(name)
|
|
108
108
|
rescue NATS::JetStream::Error::NotFound
|
|
109
|
-
|
|
109
|
+
meta = { metadata: { "_cosmo.type" => "jobs" } }
|
|
110
|
+
Client.instance.create_stream(name, config.merge(meta))
|
|
110
111
|
end
|
|
111
112
|
end
|
|
112
113
|
|
|
113
|
-
puts "Cosmo streams were created
|
|
114
|
+
puts "Cosmo streams were created successfully"
|
|
114
115
|
exit(0)
|
|
115
116
|
end
|
|
116
117
|
|
data/lib/cosmo/client.rb
CHANGED
|
@@ -38,18 +38,42 @@ module Cosmo
|
|
|
38
38
|
js.delete_stream(name, params)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def update_stream(name, config)
|
|
42
|
+
js.update_stream(name: name, **config)
|
|
43
|
+
end
|
|
44
|
+
|
|
41
45
|
def list_streams
|
|
42
46
|
response = nc.request("$JS.API.STREAM.LIST", "")
|
|
43
47
|
data = Utils::Json.parse(response.data, symbolize_names: false)
|
|
44
48
|
return [] if data.nil? || data["streams"].nil?
|
|
45
49
|
|
|
46
|
-
data["streams"]
|
|
50
|
+
data["streams"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def pause_stream(name)
|
|
54
|
+
config = stream_info(name).config.to_h
|
|
55
|
+
config[:metadata] ||= {}
|
|
56
|
+
config[:metadata][:"_cosmo.paused"] = "true"
|
|
57
|
+
update_stream(name, config)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def unpause_stream(name)
|
|
61
|
+
config = stream_info(name).config.to_h
|
|
62
|
+
config[:metadata] ||= {}
|
|
63
|
+
config[:metadata].delete(:"_cosmo.paused")
|
|
64
|
+
update_stream(name, config)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stream_paused?(name)
|
|
68
|
+
stream_info(name).config.metadata&.[](:"_cosmo.paused") == "true"
|
|
69
|
+
rescue NATS::IO::Timeout
|
|
70
|
+
false
|
|
47
71
|
end
|
|
48
72
|
|
|
49
73
|
def list_consumers(stream_name)
|
|
50
74
|
response = nc.request("$JS.API.CONSUMER.LIST.#{stream_name}", "")
|
|
51
|
-
data = Utils::Json.parse(response.data, symbolize_names: false)
|
|
52
|
-
data["consumers"]
|
|
75
|
+
data = Utils::Json.parse(response.data, default: {}, symbolize_names: false)
|
|
76
|
+
Array(data["consumers"])
|
|
53
77
|
end
|
|
54
78
|
|
|
55
79
|
def consumer_info(stream_name, consumer_name)
|
data/lib/cosmo/config.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "yaml"
|
|
|
4
4
|
require "forwardable"
|
|
5
5
|
|
|
6
6
|
module Cosmo
|
|
7
|
-
class Config
|
|
7
|
+
class Config < ::Hash
|
|
8
8
|
NANO = 1_000_000_000
|
|
9
9
|
DEFAULT_PATH = "config/cosmo.yml"
|
|
10
10
|
|
|
@@ -59,45 +59,18 @@ module Cosmo
|
|
|
59
59
|
@instance ||= new
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
def self.
|
|
63
|
-
@
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def initialize
|
|
67
|
-
@config = nil
|
|
68
|
-
@system = {}
|
|
69
|
-
@defaults = self.class.parse_file(File.expand_path("defaults.yml", __dir__))
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def [](key)
|
|
73
|
-
dig(key)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def fetch(key, default = nil)
|
|
77
|
-
return @config.fetch(key, default) if @config && Utils::Hash.keys?(@config, key)
|
|
78
|
-
|
|
79
|
-
@defaults.fetch(key, default)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def dig(*keys)
|
|
83
|
-
return @config&.dig(*keys) if @config && Utils::Hash.keys?(@config, *keys)
|
|
84
|
-
|
|
85
|
-
@defaults.dig(*keys)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def to_h
|
|
89
|
-
Utils::Hash.merge(@defaults, @config)
|
|
62
|
+
def self.internal
|
|
63
|
+
@internal ||= {}
|
|
90
64
|
end
|
|
91
65
|
|
|
92
66
|
def set(...)
|
|
93
|
-
|
|
94
|
-
Utils::Hash.set(@config, ...)
|
|
67
|
+
Utils::Hash.set(self, ...)
|
|
95
68
|
end
|
|
96
69
|
|
|
97
70
|
def load(path = nil)
|
|
98
71
|
return unless path
|
|
99
72
|
|
|
100
|
-
|
|
73
|
+
replace(self.class.parse_file(path))
|
|
101
74
|
end
|
|
102
75
|
end
|
|
103
76
|
end
|
data/lib/cosmo/engine.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Cosmo
|
|
|
25
25
|
|
|
26
26
|
def run(type, options)
|
|
27
27
|
handler = Utils::Signal.trap(:INT, :TERM)
|
|
28
|
-
Logger.info "Starting processing, hit Ctrl-C to stop"
|
|
28
|
+
Logger.info "Starting processing, hit Ctrl-C to stop [concurrency=#{@concurrency}]"
|
|
29
29
|
|
|
30
30
|
processor_classes = type && PROCESSORS.key?(type.to_sym) ? [PROCESSORS[type.to_sym]] : PROCESSORS.values
|
|
31
31
|
@processors = processor_classes.map { _1.run(@pool, @running, options) }
|
data/lib/cosmo/job/processor.rb
CHANGED
|
@@ -2,74 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Cosmo
|
|
4
4
|
module Job
|
|
5
|
-
class Processor < ::Cosmo::Processor
|
|
6
|
-
def initialize(pool, running, options)
|
|
7
|
-
super
|
|
8
|
-
@weights = []
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def stop(timeout = Config[:timeout])
|
|
12
|
-
@running.make_false
|
|
13
|
-
@pool.shutdown
|
|
14
|
-
@consumers.each { |(s, _)| s.unsubscribe rescue nil }
|
|
15
|
-
@pool.wait_for_termination(timeout)
|
|
16
|
-
[@work_thread, @schedule_thread].compact.each { _1.join(timeout) || _1.kill }
|
|
17
|
-
@consumers.clear
|
|
18
|
-
end
|
|
19
|
-
|
|
5
|
+
class Processor < ::Cosmo::Processor
|
|
20
6
|
private
|
|
21
7
|
|
|
22
|
-
def run_loop
|
|
23
|
-
@work_thread = Thread.new { work_loop }
|
|
24
|
-
@schedule_thread = Thread.new { schedule_loop }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
8
|
def setup
|
|
28
9
|
jobs_config = Config.dig(:consumers, :jobs)
|
|
29
10
|
jobs_config&.each do |stream_name, config|
|
|
30
|
-
|
|
31
|
-
consumer_name = "consumer-#{stream_name}"
|
|
32
|
-
subject = config.delete(:subject)
|
|
33
|
-
priority = config.delete(:priority)
|
|
34
|
-
@weights += ([stream_name] * priority.to_i) if priority
|
|
35
|
-
subscription = client.subscribe(subject, consumer_name, config)
|
|
36
|
-
@consumers << [subscription, stream_name]
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def work_loop # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
41
|
-
shutdown = false
|
|
11
|
+
next if stream_name == :scheduled # scheduled jobs are handled in schedule_loop
|
|
42
12
|
|
|
43
|
-
|
|
44
|
-
break if shutdown
|
|
45
|
-
|
|
46
|
-
@weights.shuffle.each do |stream_name|
|
|
47
|
-
break unless running?
|
|
48
|
-
|
|
49
|
-
begin
|
|
50
|
-
timeout = ENV.fetch("COSMO_JOBS_FETCH_TIMEOUT", 0.1).to_f
|
|
51
|
-
@pool.post do
|
|
52
|
-
subscription = @consumers.find { |(_, sn)| sn == stream_name }&.first
|
|
53
|
-
messages = lock(stream_name) { fetch(subscription, batch_size: 1, timeout:) }
|
|
54
|
-
process(messages) if messages&.any?
|
|
55
|
-
end
|
|
56
|
-
rescue Concurrent::RejectedExecutionError
|
|
57
|
-
shutdown = true
|
|
58
|
-
break # pool doesn't accept new jobs, we are shutting down
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
break unless running?
|
|
62
|
-
end
|
|
13
|
+
@consumers << subscribe(stream_name, config)
|
|
63
14
|
end
|
|
64
15
|
end
|
|
65
16
|
|
|
66
17
|
def schedule_loop # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
18
|
+
config = Config.dig(:consumers, :jobs, :scheduled)
|
|
19
|
+
return unless config
|
|
20
|
+
|
|
21
|
+
subscription, = subscribe(:scheduled, config)
|
|
67
22
|
while running?
|
|
68
23
|
break unless running?
|
|
69
24
|
|
|
70
25
|
now = Time.now.to_i
|
|
71
26
|
timeout = ENV.fetch("COSMO_JOBS_SCHEDULER_FETCH_TIMEOUT", 5).to_f
|
|
72
|
-
subscription = @consumers.find { |(_, sn)| sn == :scheduled }&.first
|
|
73
27
|
messages = fetch(subscription, batch_size: 100, timeout:)
|
|
74
28
|
messages&.each do |message|
|
|
75
29
|
headers = message.header.except("X-Stream", "X-Subject", "X-Execute-At", "Nats-Expected-Stream")
|
|
@@ -90,7 +44,7 @@ module Cosmo
|
|
|
90
44
|
end
|
|
91
45
|
end
|
|
92
46
|
|
|
93
|
-
def process(messages) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
47
|
+
def process(messages, _) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
94
48
|
message = messages.first
|
|
95
49
|
Logger.debug "received messages #{messages.inspect}"
|
|
96
50
|
data = Utils::Json.parse(message.data)
|
|
@@ -148,6 +102,15 @@ module Cosmo
|
|
|
148
102
|
true
|
|
149
103
|
end
|
|
150
104
|
|
|
105
|
+
def subscribe(stream_name, config)
|
|
106
|
+
config = config.dup
|
|
107
|
+
config[:batch_size] = 1
|
|
108
|
+
config[:stream] = stream_name
|
|
109
|
+
consumer_name = "consumer-#{stream_name}"
|
|
110
|
+
subscription = client.subscribe(config[:subject], consumer_name, config.except(:subject, :priority, :stream, :batch_size))
|
|
111
|
+
[subscription, config, nil]
|
|
112
|
+
end
|
|
113
|
+
|
|
151
114
|
def drop_message(message, data)
|
|
152
115
|
message.term
|
|
153
116
|
Logger.debug "job dropped #{data[:jid]}"
|
|
@@ -161,16 +124,28 @@ module Cosmo
|
|
|
161
124
|
Logger.debug "job moved #{data&.dig(:jid)} to DLQ"
|
|
162
125
|
end
|
|
163
126
|
|
|
127
|
+
def scheduler?
|
|
128
|
+
true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def consumers
|
|
132
|
+
@weights ||= @consumers.filter_map { |(_, c, _)| [c[:stream]] * [c[:priority].to_i, 1].max }.flatten
|
|
133
|
+
@weights.shuffle.map { |s| @consumers.find { |(_, c, _)| c[:stream] == s } }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def fetch_subjects(config)
|
|
137
|
+
config[:subject]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def fetch_timeout(_config)
|
|
141
|
+
ENV.fetch("COSMO_JOBS_FETCH_TIMEOUT", 0.1).to_f
|
|
142
|
+
end
|
|
143
|
+
|
|
164
144
|
def with_stats(message, &block)
|
|
165
145
|
API::Busy.instance.with(message) do
|
|
166
146
|
API::Counter.instance.with(&block)
|
|
167
147
|
end
|
|
168
148
|
end
|
|
169
|
-
|
|
170
|
-
def lock(stream_name, &)
|
|
171
|
-
@mutexes ||= Hash.new { |h, k| h[k] = Mutex.new }
|
|
172
|
-
@mutexes[stream_name].synchronize(&)
|
|
173
|
-
end
|
|
174
149
|
end
|
|
175
150
|
end
|
|
176
151
|
end
|
data/lib/cosmo/logger.rb
CHANGED
|
@@ -60,7 +60,10 @@ module Cosmo
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def self.instance
|
|
63
|
-
@instance ||= ::Logger.new($stdout).tap
|
|
63
|
+
@instance ||= ::Logger.new($stdout).tap do |logger|
|
|
64
|
+
logger.formatter = SimpleFormatter.new
|
|
65
|
+
logger.level = ::Logger::Severity.coerce(ENV.fetch("COSMO_LOG_LEVEL", "info"))
|
|
66
|
+
end
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def self.instance=(logger)
|