cosmonats 0.1.4 → 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 +129 -67
- data/lib/cosmo/api/busy.rb +66 -0
- data/lib/cosmo/api/counter.rb +70 -0
- data/lib/cosmo/api/job.rb +46 -0
- data/lib/cosmo/api/kv.rb +63 -0
- data/lib/cosmo/api/stats.rb +44 -0
- data/lib/cosmo/api/stream.rb +123 -0
- data/lib/cosmo/api.rb +11 -0
- data/lib/cosmo/cli.rb +8 -5
- data/lib/cosmo/client.rb +58 -3
- data/lib/cosmo/config.rb +13 -38
- data/lib/cosmo/engine.rb +1 -1
- data/lib/cosmo/job/processor.rb +66 -57
- data/lib/cosmo/job.rb +1 -1
- data/lib/cosmo/logger.rb +8 -1
- data/lib/cosmo/processor.rb +110 -2
- 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/overrides.rb +15 -0
- data/lib/cosmo/utils/ttl_cache.rb +44 -0
- data/lib/cosmo/utils/warnings.rb +17 -0
- data/lib/cosmo/utils.rb +15 -0
- data/lib/cosmo/version.rb +1 -1
- data/lib/cosmo/web/assets/app.css +477 -0
- data/lib/cosmo/web/assets/htmx.2.0.8.min.js.gz +0 -0
- data/lib/cosmo/web/context.rb +28 -0
- data/lib/cosmo/web/controllers/actions.rb +16 -0
- data/lib/cosmo/web/controllers/application.rb +43 -0
- data/lib/cosmo/web/controllers/jobs.rb +97 -0
- data/lib/cosmo/web/controllers/streams.rb +70 -0
- data/lib/cosmo/web/helpers/application.rb +87 -0
- data/lib/cosmo/web/renderer.rb +58 -0
- data/lib/cosmo/web/views/actions/index.erb +7 -0
- data/lib/cosmo/web/views/jobs/_busy.erb +50 -0
- data/lib/cosmo/web/views/jobs/_dead.erb +65 -0
- data/lib/cosmo/web/views/jobs/_enqueued.erb +60 -0
- data/lib/cosmo/web/views/jobs/_scheduled.erb +49 -0
- data/lib/cosmo/web/views/jobs/_stats.erb +69 -0
- data/lib/cosmo/web/views/jobs/busy.erb +16 -0
- data/lib/cosmo/web/views/jobs/dead.erb +17 -0
- data/lib/cosmo/web/views/jobs/enqueued.erb +16 -0
- data/lib/cosmo/web/views/jobs/index.erb +12 -0
- data/lib/cosmo/web/views/jobs/scheduled.erb +17 -0
- data/lib/cosmo/web/views/layout.erb +33 -0
- data/lib/cosmo/web/views/streams/_info.erb +92 -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 +25 -0
- data/lib/cosmo/web/views/streams/index.erb +11 -0
- data/lib/cosmo/web/views/streams/info.erb +11 -0
- data/lib/cosmo/web.rb +68 -0
- data/lib/cosmo.rb +2 -7
- data/sig/cosmo/api/busy.rbs +35 -0
- data/sig/cosmo/api/counter.rbs +34 -0
- data/sig/cosmo/api/job.rbs +31 -0
- data/sig/cosmo/api/kv.rbs +30 -0
- data/sig/cosmo/api/stats.rbs +21 -0
- data/sig/cosmo/api/stream.rbs +50 -0
- data/sig/cosmo/client.rbs +21 -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 +62 -3
- data/lib/cosmo/defaults.yml +0 -69
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
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### 3. Configure (config/cosmo.yml)
|
|
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.>"]
|
|
154
|
+
10.times { |i| SendEmailJob.perform_async(i, "welcome") }
|
|
144
155
|
```
|
|
145
156
|
|
|
146
|
-
### 4.
|
|
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,31 +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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
284
|
+
|
|
285
|
+
setup:
|
|
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
|
|
257
319
|
```
|
|
258
320
|
|
|
259
321
|
**Programmatic:**
|
|
260
322
|
```ruby
|
|
261
323
|
Cosmo::Config.set(:concurrency, 20)
|
|
262
|
-
Cosmo::Config.set(:streams, :custom, { storage:
|
|
324
|
+
Cosmo::Config.set(:setup, :streams, :custom, { storage: "file", subjects: ["custom.>"] })
|
|
263
325
|
```
|
|
264
326
|
|
|
265
327
|
**Environment variables:**
|
|
@@ -327,10 +389,10 @@ end
|
|
|
327
389
|
**Testing:**
|
|
328
390
|
```ruby
|
|
329
391
|
# Synchronous execution
|
|
330
|
-
SendEmailJob.perform_sync(123,
|
|
392
|
+
SendEmailJob.perform_sync(123, "test")
|
|
331
393
|
|
|
332
394
|
# Test job creation
|
|
333
|
-
jid = SendEmailJob.perform_async(123,
|
|
395
|
+
jid = SendEmailJob.perform_async(123, "welcome")
|
|
334
396
|
assert_kind_of String, jid
|
|
335
397
|
```
|
|
336
398
|
|
|
@@ -437,7 +499,7 @@ sudo systemctl status cosmo
|
|
|
437
499
|
**Stream Metrics:**
|
|
438
500
|
```ruby
|
|
439
501
|
client = Cosmo::Client.instance
|
|
440
|
-
info = client.stream_info(
|
|
502
|
+
info = client.stream_info("default")
|
|
441
503
|
|
|
442
504
|
info.state.messages # Total messages
|
|
443
505
|
info.state.bytes # Total bytes
|
|
@@ -464,8 +526,8 @@ class EmailJob
|
|
|
464
526
|
end
|
|
465
527
|
end
|
|
466
528
|
|
|
467
|
-
EmailJob.perform_async(123,
|
|
468
|
-
EmailJob.perform_in(1.day, 123,
|
|
529
|
+
EmailJob.perform_async(123, "welcome")
|
|
530
|
+
EmailJob.perform_in(1.day, 123, "followup")
|
|
469
531
|
```
|
|
470
532
|
|
|
471
533
|
**Image Processing Pipeline:**
|
|
@@ -474,12 +536,12 @@ class ImageProcessor
|
|
|
474
536
|
include Cosmo::Stream
|
|
475
537
|
options(
|
|
476
538
|
stream: :images,
|
|
477
|
-
consumer: { subjects: [
|
|
539
|
+
consumer: { subjects: ["images.uploaded.>"] }
|
|
478
540
|
)
|
|
479
541
|
|
|
480
542
|
def process_one
|
|
481
|
-
processed = ImageService.process(message.data[
|
|
482
|
-
publish(processed, subject:
|
|
543
|
+
processed = ImageService.process(message.data["url"])
|
|
544
|
+
publish(processed, subject: "images.processed.optimized")
|
|
483
545
|
message.ack
|
|
484
546
|
rescue => e
|
|
485
547
|
logger.error "Processing failed: #{e.message}"
|
|
@@ -487,18 +549,18 @@ class ImageProcessor
|
|
|
487
549
|
end
|
|
488
550
|
end
|
|
489
551
|
|
|
490
|
-
ImageProcessor.publish({ url:
|
|
552
|
+
ImageProcessor.publish({ url: "https://example.com/image.jpg" }, subject: "images.uploaded.user")
|
|
491
553
|
```
|
|
492
554
|
|
|
493
555
|
**Real-Time Analytics:**
|
|
494
556
|
```ruby
|
|
495
557
|
class AnalyticsAggregator
|
|
496
558
|
include Cosmo::Stream
|
|
497
|
-
options batch_size: 1000, consumer: { subjects: [
|
|
559
|
+
options batch_size: 1000, consumer: { subjects: ["events.*.>"] }
|
|
498
560
|
|
|
499
561
|
def process(messages)
|
|
500
562
|
events = messages.map(&:data)
|
|
501
|
-
aggregates = events.group_by { |e| e[
|
|
563
|
+
aggregates = events.group_by { |e| e["type"] }.transform_values(&:count)
|
|
502
564
|
Analytics.bulk_insert(aggregates)
|
|
503
565
|
messages.each(&:ack)
|
|
504
566
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module Cosmo
|
|
6
|
+
module API
|
|
7
|
+
class Busy
|
|
8
|
+
TTL = 70
|
|
9
|
+
HEARTBEAT = 30
|
|
10
|
+
BUCKET = "cosmostats"
|
|
11
|
+
|
|
12
|
+
def self.instance
|
|
13
|
+
@instance ||= new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@messages = {}
|
|
18
|
+
@kv = KV.new(BUCKET, { ttl: TTL })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def with(message)
|
|
22
|
+
add(message)
|
|
23
|
+
yield
|
|
24
|
+
ensure
|
|
25
|
+
delete(message)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add(message)
|
|
29
|
+
@thread ||= Thread.new { heartbeat_loop }
|
|
30
|
+
seq = message.metadata.sequence.stream
|
|
31
|
+
value = Utils::Json.dump({ data: message.data, stream: message.metadata.stream, worker: worker_id, started_at: Time.now.to_i })
|
|
32
|
+
@messages[seq] = value
|
|
33
|
+
@kv.set(seq, value)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delete(message)
|
|
37
|
+
seq = message.metadata.sequence.stream
|
|
38
|
+
@messages.delete(seq)
|
|
39
|
+
@kv.purge(seq)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def list(limit: 25)
|
|
43
|
+
@kv.keys(limit:).filter_map { Utils::Json.parse(@kv.get(_1)) }.map { _1.merge(data: Utils::Json.parse(_1[:data])) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def size
|
|
47
|
+
@kv.size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def heartbeat_loop
|
|
53
|
+
loop do
|
|
54
|
+
sleep(HEARTBEAT)
|
|
55
|
+
@messages.dup.each { |seq, value| @kv.set(seq, value) rescue StandardError }
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
Logger.debug "Busy heartbeat error: #{e.class} #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def worker_id
|
|
62
|
+
@worker_id ||= "#{Socket.gethostname}-#{Process.pid}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cosmo
|
|
4
|
+
module API
|
|
5
|
+
class Counter
|
|
6
|
+
STREAM_NAME = "cosmostats"
|
|
7
|
+
|
|
8
|
+
def self.instance
|
|
9
|
+
@instance ||= new("jobs")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(namespace)
|
|
13
|
+
@namespace = namespace
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def with
|
|
17
|
+
result = yield
|
|
18
|
+
increment(:processed) if result == true
|
|
19
|
+
increment(:failed) if result == false
|
|
20
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
21
|
+
increment(:failed)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def increment(key, by: 1)
|
|
25
|
+
publish(key, "+#{by}")
|
|
26
|
+
end
|
|
27
|
+
alias incr increment
|
|
28
|
+
|
|
29
|
+
def decrement(key, by: 1)
|
|
30
|
+
publish(key, "-#{by}")
|
|
31
|
+
end
|
|
32
|
+
alias decr decrement
|
|
33
|
+
|
|
34
|
+
def reset(key)
|
|
35
|
+
client.purge(STREAM_NAME, subject(key))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get(key)
|
|
39
|
+
raw = client.get_message(STREAM_NAME, direct: true, subject: subject(key))
|
|
40
|
+
Utils::Json.parse(raw.data, default: { "val" => 0 })[:val].to_i
|
|
41
|
+
rescue NATS::JetStream::Error::NotFound, NATS::JetStream::Error::ServiceUnavailable
|
|
42
|
+
0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def publish(key, value)
|
|
48
|
+
rescued = nil
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
client.publish(subject(key), "", header: { "Nats-Incr" => value }).val.to_i
|
|
52
|
+
rescue NATS::JetStream::Error::NoStreamResponse
|
|
53
|
+
raise if rescued
|
|
54
|
+
|
|
55
|
+
rescued = true
|
|
56
|
+
client.create_stream(STREAM_NAME, subjects: ["#{STREAM_NAME}.>"], allow_msg_counter: true, allow_direct: true, description: "Cosmo statistics")
|
|
57
|
+
retry
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def subject(key)
|
|
62
|
+
"#{STREAM_NAME}.#{@namespace}.#{key}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def client
|
|
66
|
+
@client ||= Client.instance
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cosmo
|
|
4
|
+
module API
|
|
5
|
+
class Job
|
|
6
|
+
attr_reader :message, :stream
|
|
7
|
+
|
|
8
|
+
def initialize(stream, message)
|
|
9
|
+
@stream = stream
|
|
10
|
+
@message = message
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def data
|
|
14
|
+
@data ||= Utils::Json.parse(@message.data)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def seq
|
|
18
|
+
@message.seq
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def headers
|
|
22
|
+
@message.headers
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execute_at
|
|
26
|
+
headers&.dig("X-Execute-At")&.to_i
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def x_stream
|
|
30
|
+
headers&.dig("X-Stream")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def x_subject
|
|
34
|
+
headers&.dig("X-Subject")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def subject
|
|
38
|
+
@message.subject
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def timestamp
|
|
42
|
+
headers&.dig("Nats-Time-Stamp")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/cosmo/api/kv.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cosmo
|
|
4
|
+
module API
|
|
5
|
+
class KV
|
|
6
|
+
def initialize(name, options = nil)
|
|
7
|
+
@name = name
|
|
8
|
+
@options = Hash(options)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def set(key, value)
|
|
12
|
+
kv.put(key, value.to_s)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(key)
|
|
16
|
+
kv.get(key).value
|
|
17
|
+
rescue NATS::KeyValue::KeyNotFoundError
|
|
18
|
+
# nop
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def delete(key)
|
|
22
|
+
kv.delete(key)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def keys(subject = nil, limit: 25)
|
|
26
|
+
results = []
|
|
27
|
+
params = { ignore_deletes: true, meta_only: true }
|
|
28
|
+
watcher = kv.watch(subject || ">", params)
|
|
29
|
+
|
|
30
|
+
watcher.each do |entry|
|
|
31
|
+
break unless entry
|
|
32
|
+
|
|
33
|
+
results << entry.key
|
|
34
|
+
break if results.size >= limit
|
|
35
|
+
end
|
|
36
|
+
watcher.stop
|
|
37
|
+
|
|
38
|
+
results
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def purge(key)
|
|
42
|
+
kv.purge(key)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def clean
|
|
46
|
+
Client.instance.purge("KV_#{@name}", ">")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def count
|
|
50
|
+
keys.size
|
|
51
|
+
rescue NATS::KeyValue::NoKeysFoundError, NATS::JetStream::Error::NotFound
|
|
52
|
+
0
|
|
53
|
+
end
|
|
54
|
+
alias size count
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def kv
|
|
59
|
+
@kv ||= Client.instance.kv(@name, **@options)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cosmo/api/counter"
|
|
4
|
+
require "cosmo/api/busy"
|
|
5
|
+
|
|
6
|
+
module Cosmo
|
|
7
|
+
module API
|
|
8
|
+
module Stats
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def summary
|
|
12
|
+
{ processed:, failed:, busy:, enqueued:, retries:, scheduled:, dead: }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def processed
|
|
16
|
+
Counter.instance.get(:processed)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def failed
|
|
20
|
+
Counter.instance.get(:failed)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def busy
|
|
24
|
+
Busy.instance.size
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def enqueued
|
|
28
|
+
Stream.jobs.sum(&:size)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def retries
|
|
32
|
+
Stream.jobs.sum(&:retries)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def scheduled
|
|
36
|
+
Stream.new("scheduled").size
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dead
|
|
40
|
+
Stream.new("dead").size
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|