cosmonats 0.5.1 → 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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -169
  3. data/README.md +102 -18
  4. data/lib/cosmo/api/counter.rb +14 -11
  5. data/lib/cosmo/api/cron/entry.rb +23 -3
  6. data/lib/cosmo/api/cron.rb +22 -26
  7. data/lib/cosmo/api/job.rb +30 -0
  8. data/lib/cosmo/api/kv.rb +31 -11
  9. data/lib/cosmo/api/registry.rb +68 -0
  10. data/lib/cosmo/api/stats/busy.rb +63 -0
  11. data/lib/cosmo/api/stats/processes.rb +37 -0
  12. data/lib/cosmo/api/stats/totals.rb +35 -0
  13. data/lib/cosmo/api/stats.rb +5 -4
  14. data/lib/cosmo/api/stream.rb +1 -1
  15. data/lib/cosmo/api.rb +3 -2
  16. data/lib/cosmo/batch.rb +9 -2
  17. data/lib/cosmo/cli.rb +27 -7
  18. data/lib/cosmo/client.rb +69 -6
  19. data/lib/cosmo/config.rb +6 -2
  20. data/lib/cosmo/engine.rb +103 -4
  21. data/lib/cosmo/heartbeat.rb +129 -0
  22. data/lib/cosmo/http_server/handler.rb +44 -0
  23. data/lib/cosmo/http_server/health.rb +39 -0
  24. data/lib/cosmo/http_server/ping.rb +23 -0
  25. data/lib/cosmo/http_server.rb +62 -0
  26. data/lib/cosmo/job/failure.rb +52 -0
  27. data/lib/cosmo/job/processor.rb +87 -25
  28. data/lib/cosmo/job/stream_filter.rb +54 -0
  29. data/lib/cosmo/job.rb +2 -0
  30. data/lib/cosmo/logger.rb +21 -3
  31. data/lib/cosmo/processor.rb +46 -16
  32. data/lib/cosmo/sentry/job_processor_middleware.rb +1 -2
  33. data/lib/cosmo/stream/message.rb +2 -1
  34. data/lib/cosmo/stream/processor.rb +5 -0
  35. data/lib/cosmo/utils/json.rb +1 -1
  36. data/lib/cosmo/utils/overrides.rb +199 -8
  37. data/lib/cosmo/utils/thread_pool.rb +5 -0
  38. data/lib/cosmo/version.rb +1 -1
  39. data/lib/cosmo/web/assets/app.css +21 -0
  40. data/lib/cosmo/web/controllers/jobs.rb +28 -4
  41. data/lib/cosmo/web/helpers/application.rb +10 -1
  42. data/lib/cosmo/web/views/batches/index.erb +1 -1
  43. data/lib/cosmo/web/views/jobs/_busy.erb +70 -4
  44. data/lib/cosmo/web/views/jobs/_dead.erb +29 -16
  45. data/lib/cosmo/web/views/jobs/_enqueued.erb +10 -0
  46. data/lib/cosmo/web/views/jobs/_processes.erb +131 -0
  47. data/lib/cosmo/web/views/jobs/_scheduled.erb +1 -1
  48. data/lib/cosmo/web/views/jobs/busy.erb +1 -1
  49. data/lib/cosmo/web/views/jobs/dead.erb +2 -2
  50. data/lib/cosmo/web/views/jobs/enqueued.erb +1 -1
  51. data/lib/cosmo/web/views/jobs/index.erb +7 -1
  52. data/lib/cosmo/web/views/jobs/scheduled.erb +1 -1
  53. data/lib/cosmo/web/views/streams/index.erb +1 -1
  54. data/lib/cosmo/web.rb +2 -0
  55. data/lib/cosmo.rb +13 -0
  56. data/sig/cosmo/api/counter.rbs +2 -4
  57. data/sig/cosmo/api/cron/entry.rbs +2 -0
  58. data/sig/cosmo/api/cron.rbs +4 -2
  59. data/sig/cosmo/api/job.rbs +12 -0
  60. data/sig/cosmo/api/kv.rbs +7 -1
  61. data/sig/cosmo/api/registry.rbs +30 -0
  62. data/sig/cosmo/api/stats/busy.rbs +31 -0
  63. data/sig/cosmo/api/stats/processes.rbs +16 -0
  64. data/sig/cosmo/api/stats/totals.rbs +17 -0
  65. data/sig/cosmo/cli.rbs +4 -0
  66. data/sig/cosmo/client.rbs +17 -2
  67. data/sig/cosmo/engine.rbs +29 -0
  68. data/sig/cosmo/heartbeat.rbs +36 -0
  69. data/sig/cosmo/http_server/handler.rbs +17 -0
  70. data/sig/cosmo/http_server/health.rbs +21 -0
  71. data/sig/cosmo/http_server/ping.rbs +14 -0
  72. data/sig/cosmo/http_server.rbs +22 -0
  73. data/sig/cosmo/job/failure.rbs +28 -0
  74. data/sig/cosmo/job/processor.rbs +17 -1
  75. data/sig/cosmo/job/stream_filter.rbs +25 -0
  76. data/sig/cosmo/logger.rbs +10 -0
  77. data/sig/cosmo/processor.rbs +12 -4
  78. data/sig/cosmo/stream/message.rbs +3 -1
  79. data/sig/cosmo/stream/processor.rbs +2 -0
  80. data/sig/cosmo/utils/thread_pool.rbs +3 -0
  81. data/sig/cosmo.rbs +4 -0
  82. metadata +33 -20
  83. data/lib/cosmo/api/busy.rb +0 -66
  84. data/sig/cosmo/api/busy.rbs +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ade68e6438fc5ea6cec3fd97bf2794c87f2117414c56c69e49e67544f35e7ab
4
- data.tar.gz: db98a44b41d057795b6a9f8c56316252b4cad1ab2f2a07912a66e16c09a86013
3
+ metadata.gz: 96e901f7365784bf2fb7087311112efb263f295eddbe25a96ff32de4ebfe0652
4
+ data.tar.gz: b1f7327b126f85e50222af5adf2172b8fab84aa051209e9c0e9b478254bcb8a8
5
5
  SHA512:
6
- metadata.gz: bd359b85f69bcdc85f800ec8dc0e4b24ff09e905851e412047dd96a0154c21b459bf52465493ee1a05c664968817828124fa35919cd7174d0f3c0ebe0ba867ca
7
- data.tar.gz: dbb223d897a14e457bcf78085896ed0c7f1548eb7565621e35b62257879065dfb653d8cc58a396c921d508afe92e1b9640cbfd3cf9845d00ee471b577609ffa7
6
+ metadata.gz: 1ab5c6cc101edf8dbf8909f544ed9a9fd8603c76f073dad60c5251e086195518e07ec5a6815bcf382b05c7b80f67fb6994a4615be933b33c29103ea3082990af
7
+ data.tar.gz: 85b15998b6286a331558e847835f6a94cf0936adae88e70a18035335b2414096d583e4619b7b2e5f5c7e579117c0d3626dbb99e189084fc3cf7aedc25dfc4fba
data/LICENSE.txt CHANGED
@@ -1,169 +1,21 @@
1
- GNU LESSER GENERAL PUBLIC LICENSE
2
- Version 3, 29 June 2007
3
-
4
- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
- Everyone is permitted to copy and distribute verbatim copies
6
- of this license document, but changing it is not allowed.
7
-
8
-
9
- This version of the GNU Lesser General Public License incorporates
10
- the terms and conditions of version 3 of the GNU General Public
11
- License, supplemented by the additional permissions listed below.
12
-
13
- 0. Additional Definitions.
14
-
15
- As used herein, "this License" refers to version 3 of the GNU Lesser
16
- General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
- General Public License.
18
-
19
- "The Library" refers to a covered work governed by this License,
20
- other than an Application or a Combined Work as defined below.
21
-
22
- An "Application" is any work that makes use of an interface provided
23
- by the Library, but which is not otherwise based on the Library.
24
- Defining a subclass of a class defined by the Library is deemed a mode
25
- of using an interface provided by the Library.
26
-
27
- A "Combined Work" is a work produced by combining or linking an
28
- Application with the Library. The particular version of the Library
29
- with which the Combined Work was made is also called the "Linked
30
- Version".
31
-
32
- The "Minimal Corresponding Source" for a Combined Work means the
33
- Corresponding Source for the Combined Work, excluding any source code
34
- for portions of the Combined Work that, considered in isolation, are
35
- based on the Application, and not on the Linked Version.
36
-
37
- The "Corresponding Application Code" for a Combined Work means the
38
- object code and/or source code for the Application, including any data
39
- and utility programs needed for reproducing the Combined Work from the
40
- Application, but excluding the System Libraries of the Combined Work.
41
-
42
- 1. Exception to Section 3 of the GNU GPL.
43
-
44
- You may convey a covered work under sections 3 and 4 of this License
45
- without being bound by section 3 of the GNU GPL.
46
-
47
- 2. Conveying Modified Versions.
48
-
49
- If you modify a copy of the Library, and, in your modifications, a
50
- facility refers to a function or data to be supplied by an Application
51
- that uses the facility (other than as an argument passed when the
52
- facility is invoked), then you may convey a copy of the modified
53
- version:
54
-
55
- a) under this License, provided that you make a good faith effort to
56
- ensure that, in the event an Application does not supply the
57
- function or data, the facility still operates, and performs
58
- whatever part of its purpose remains meaningful, or
59
-
60
- b) under the GNU GPL, with none of the additional permissions of
61
- this License applicable to that copy.
62
-
63
- 3. Object Code Incorporating Material from Library Header Files.
64
-
65
- The object code form of an Application may incorporate material from
66
- a header file that is part of the Library. You may convey such object
67
- code under terms of your choice, provided that, if the incorporated
68
- material is not limited to numerical parameters, data structure
69
- layouts and accessors, or small macros, inline functions and templates
70
- (ten or fewer lines in length), you do both of the following:
71
-
72
- a) Give prominent notice with each copy of the object code that the
73
- Library is used in it and that the Library and its use are
74
- covered by this License.
75
-
76
- b) Accompany the object code with a copy of the GNU GPL and this license
77
- document.
78
-
79
- 4. Combined Works.
80
-
81
- You may convey a Combined Work under terms of your choice that,
82
- taken together, effectively do not restrict modification of the
83
- portions of the Library contained in the Combined Work and reverse
84
- engineering for debugging such modifications, if you also do each of
85
- the following:
86
-
87
- a) Give prominent notice with each copy of the Combined Work that
88
- the Library is used in it and that the Library and its use are
89
- covered by this License.
90
-
91
- b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
- document.
93
-
94
- c) For a Combined Work that displays copyright notices during
95
- execution, include the copyright notice for the Library among
96
- these notices, as well as a reference directing the user to the
97
- copies of the GNU GPL and this license document.
98
-
99
- d) Do one of the following:
100
-
101
- 0) Convey the Minimal Corresponding Source under the terms of this
102
- License, and the Corresponding Application Code in a form
103
- suitable for, and under terms that permit, the user to
104
- recombine or relink the Application with a modified version of
105
- the Linked Version to produce a modified Combined Work, in the
106
- manner specified by section 6 of the GNU GPL for conveying
107
- Corresponding Source.
108
-
109
- 1) Use a suitable shared library mechanism for linking with the
110
- Library. A suitable mechanism is one that (a) uses at run time
111
- a copy of the Library already present on the user's computer
112
- system, and (b) will operate properly with a modified version
113
- of the Library that is interface-compatible with the Linked
114
- Version.
115
-
116
- e) Provide Installation Information, but only if you would otherwise
117
- be required to provide such information under section 6 of the
118
- GNU GPL, and only to the extent that such information is
119
- necessary to install and execute a modified version of the
120
- Combined Work produced by recombining or relinking the
121
- Application with a modified version of the Linked Version. (If
122
- you use option 4d0, the Installation Information must accompany
123
- the Minimal Corresponding Source and Corresponding Application
124
- Code. If you use option 4d1, you must provide the Installation
125
- Information in the manner specified by section 6 of the GNU GPL
126
- for conveying Corresponding Source.)
127
-
128
- 5. Combined Libraries.
129
-
130
- You may place library facilities that are a work based on the
131
- Library side by side in a single library together with other library
132
- facilities that are not Applications and are not covered by this
133
- License, and convey such a combined library under terms of your
134
- choice, if you do both of the following:
135
-
136
- a) Accompany the combined library with a copy of the same work based
137
- on the Library, uncombined with any other library facilities,
138
- conveyed under the terms of this License.
139
-
140
- b) Give prominent notice with the combined library that part of it
141
- is a work based on the Library, and explaining where to find the
142
- accompanying uncombined form of the same work.
143
-
144
- 6. Revised Versions of the GNU Lesser General Public License.
145
-
146
- The Free Software Foundation may publish revised and/or new versions
147
- of the GNU Lesser General Public License from time to time. Such new
148
- versions will be similar in spirit to the present version, but may
149
- differ in detail to address new problems or concerns.
150
-
151
- Each version is given a distinguishing version number. If the
152
- Library as you received it specifies that a certain numbered version
153
- of the GNU Lesser General Public License "or any later version"
154
- applies to it, you have the option of following the terms and
155
- conditions either of that published version or of any later version
156
- published by the Free Software Foundation. If the Library as you
157
- received it does not specify a version number of the GNU Lesser
158
- General Public License, you may choose any version of the GNU Lesser
159
- General Public License ever published by the Free Software Foundation.
160
-
161
- If the Library as you received it specifies that a proxy can decide
162
- whether future versions of the GNU Lesser General Public License shall
163
- apply, that proxy's public statement of acceptance of any version is
164
- permanent authorization for you to choose that version for the
165
- Library.
166
-
167
- Copyright (c) BitsBeam LLC
168
-
169
- https://www.gnu.org/licenses/lgpl-3.0.txt
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitry Vorotilin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -10,7 +10,7 @@ Background jobs + real-time event streaming for Ruby — unified, in one gem, ba
10
10
  [![Gem Version](https://badge.fury.io/rb/cosmonats.svg)](https://rubygems.org/gems/cosmonats)
11
11
  [![Downloads](https://img.shields.io/gem/dt/cosmonats.svg)](https://rubygems.org/gems/cosmonats)
12
12
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-red)](https://www.ruby-lang.org)
13
- [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](LICENSE.txt)
13
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.txt)
14
14
  [![Build Status](https://github.com/bitsbeam/cosmonats/actions/workflows/ci.yml/badge.svg)](https://github.com/bitsbeam/cosmonats/actions)
15
15
 
16
16
  *Battle-tested in production. Tens of millions of jobs processed and counting.*
@@ -168,6 +168,19 @@ gem "cosmonats"
168
168
 
169
169
  **Requirements:** Ruby ≥ 3.1, NATS Server ([install guide](https://docs.nats.io/running-a-nats-service/introduction/installation))
170
170
 
171
+ **Optional gems** — not installed with `cosmonats`, add them only for the features you use:
172
+
173
+ | Feature | Gems |
174
+ |-----------------------------------------------|-------------------|
175
+ | Monitoring UI (`Cosmo::Web`) | `rack` |
176
+ | Health check HTTP server (`/health`, `/ping`) | `rack`, `webrick` |
177
+
178
+ ```ruby
179
+ # Gemfile
180
+ gem "rack", "~> 3.0"
181
+ gem "webrick", "~> 1.9"
182
+ ```
183
+
171
184
  Spin up NATS instantly with Docker — one command, that's it:
172
185
  ```bash
173
186
  docker run -p 4222:4222 -p 8222:8222 nats:alpine -js
@@ -336,7 +349,7 @@ stream_config: &stream_config
336
349
  storage: file # storage type (file or memory)
337
350
  retention: workqueue # retention policy (limits, interest, workqueue)
338
351
  duplicate_window: 120 # time window for duplicate message detection in seconds
339
- discard: old # discard new messages when stream is full (discard new or old)
352
+ discard: old # what to drop when the stream is full (new rejects publishes, old evicts the oldest)
340
353
  allow_direct: true # allow direct messages to stream, required for web UI
341
354
  subjects:
342
355
  - jobs.%{name}.> # subject pattern for stream, %{name} will be replaced with stream name
@@ -364,7 +377,7 @@ consumers:
364
377
  priority: 5
365
378
  scheduled:
366
379
  <<: *consumer_config
367
- max_deliver: 1
380
+ max_deliver: 5
368
381
  max_ack_pending: 100
369
382
  ack_wait: 10
370
383
 
@@ -384,6 +397,7 @@ setup:
384
397
  description: Lower priority jobs
385
398
  scheduled:
386
399
  <<: *stream_config
400
+ discard: old # required here: NATS refuses `discard: new` on the stream holding cron schedules
387
401
  description: Scheduled jobs
388
402
  dead:
389
403
  <<: *stream_config
@@ -423,10 +437,16 @@ export COSMO_STREAMS_FETCH_TIMEOUT=0.1
423
437
  ### Cron
424
438
 
425
439
  Recurring jobs, without a separate scheduler process. A schedule is just a message parked in the
426
- job's own NATS stream (requires NATS Server 2.14+) — NATS fires it on the cron expression, and it
427
- lands back in the stream as a regular job. Deploy it once; whatever's in NATS is exactly what runs
428
- and exactly what shows up in the web UI's **Crons** tab, where each entry can be inspected, run
429
- immediately, or deleted.
440
+ `scheduled` stream (requires NATS Server 2.14+) — NATS fires it on the cron expression, and the
441
+ worker's scheduler dispatches the job on to the stream that runs it, the same path a delayed job
442
+ takes. Deploy it once; whatever's in NATS is exactly what runs and exactly what shows up in the web
443
+ UI's **Crons** tab, where each entry can be inspected, run immediately, or deleted.
444
+
445
+ Every schedule lives in `scheduled` because NATS only lets a schedule fire at a subject its own
446
+ stream covers, and refuses `discard: new` on any stream with scheduling enabled. Keeping schedules
447
+ in one stream leaves every stream that actually runs jobs free to choose its own discard policy.
448
+ The trade is that cron firings are dispatched by the scheduler, so at least one worker has to be
449
+ running without `--no-scheduler`.
430
450
 
431
451
  Declare schedules right in `config/cosmo.yml`:
432
452
 
@@ -641,19 +661,79 @@ setup beyond having `sentry-ruby` initialized.
641
661
  ## 🖥️ CLI Reference
642
662
 
643
663
  ```bash
644
- cosmo -C config/cosmo.yml --setup # Create streams in NATS (idempotent)
645
- cosmo -C config/cosmo.yml -c 20 -r ./app/jobs jobs # Jobs only
646
- cosmo -C config/cosmo.yml -c 20 streams # Streams only
647
- cosmo -C config/cosmo.yml -c 20 # Both
664
+ cosmo -C config/cosmo.yml --setup # Create streams in NATS (idempotent)
665
+ cosmo -C config/cosmo.yml -c 20 -r ./app/jobs jobs # Jobs only
666
+ cosmo -C config/cosmo.yml -c 20 streams # Streams only
667
+ cosmo -C config/cosmo.yml -c 20 # Both
668
+ cosmo -C config/cosmo.yml jobs --streams default,high # Jobs, limited to these streams
669
+ cosmo -C config/cosmo.yml jobs --stream critical # Jobs, limited to one stream
670
+ cosmo -C config/cosmo.yml jobs --no-scheduler # Jobs, without dispatching scheduled jobs
671
+ COSMO_JOBS_STREAMS=default,high cosmo jobs # Same filter, without touching the command line
672
+ ```
673
+
674
+ Each selected stream gets its own durable consumer, named `consumer-<stream>`. Every process that subscribes to a stream
675
+ pulls from that same consumer, so running a dedicated fleet per stream splits the work instead of duplicating it.
676
+ The `scheduled` stream is a service stream: it is always dispatched, and naming it in `--streams` selects nothing, use
677
+ `--no-scheduler` on workers that should not dispatch scheduled jobs. Unknown stream names abort the process.
678
+
679
+ **Global flags** (before the command):
680
+
681
+ | Flag | Description | Example |
682
+ |-------------------------|---------------------------------------|-----------------------|
683
+ | `-C, --config PATH` | Config file path | `-C config/cosmo.yml` |
684
+ | `-c, --concurrency INT` | Worker threads | `-c 20` |
685
+ | `-r, --require PATH` | Auto-require directory | `-r ./app/jobs` |
686
+ | `-t, --timeout NUM` | Shutdown timeout (sec) | `-t 60` |
687
+ | `-S, --setup` | Setup streams & sync cron, then exit | `--setup` |
688
+ | `-v, --version` | Print version and exit | `--version` |
689
+ | `-h, --help` | Show help and exit | `--help` |
690
+
691
+ **`jobs` command options:**
692
+
693
+ | Flag | Description | Example |
694
+ |-------------------------|--------------------------------------------------------------------|---------------------------------|
695
+ | `--streams NAMES` | Only subscribe to these job streams, instead of all configured ones | `--streams default,high` |
696
+ | `--stream NAME` | Same as `--streams`, for a single stream | `--stream default` |
697
+ | `--[no-]scheduler` | Dispatch scheduled jobs (default: yes) | `--no-scheduler` |
698
+ | `--subject NAME` | Job's subject | `--subject jobs.default.foo` |
699
+
700
+ `--streams`/`--stream` also read from `COSMO_JOBS_STREAMS` (comma-separated) when neither flag is given.
701
+
702
+ **`streams` command options** (`--stream`/`--subject`/`--consumer_name`/`--batch_size` apply only when running a single processor; ignored once `--processors` selects more than one):
703
+
704
+ | Flag | Description | Example |
705
+ |--------------------------|---------------------------------------|---------------------------------|
706
+ | `--processors NAMES` | Only run these stream processor classes | `--processors OrderProcessor` |
707
+ | `--stream NAME` | Stream name | `--stream orders` |
708
+ | `--subject NAME` | Subject name | `--subject orders.created` |
709
+ | `--consumer_name NAME` | Consumer name | `--consumer_name orders-consumer` |
710
+ | `--batch_size NUM` | Messages per fetch batch | `--batch_size 50` |
711
+
712
+
713
+ ### Health check
714
+
715
+ Pass `-p/--http-port` (or set `http.port` in the config) to start a small WEBrick HTTP server alongside the workers.
716
+ It needs the optional `rack` and `webrick` gems (see [Installation](#-installation)); without a port they're never loaded.
717
+
718
+ ```yaml
719
+ http:
720
+ port: 9090
721
+ host: 0.0.0.0 # default
722
+ ```
723
+
724
+ `GET /health` returns `200` when processors are running and NATS is connected, `503` otherwise:
725
+
726
+ ```bash
727
+ curl localhost:9090/health
728
+ # {"status":"ok","checks":{"engine":true,"nats":true}}
648
729
  ```
649
730
 
650
- | Flag | Description | Example |
651
- |-------------------------|------------------------|-----------------------|
652
- | `-C, --config PATH` | Config file path | `-C config/cosmo.yml` |
653
- | `-c, --concurrency INT` | Worker threads | `-c 20` |
654
- | `-r, --require PATH` | Auto-require directory | `-r ./app/jobs` |
655
- | `-t, --timeout NUM` | Shutdown timeout (sec) | `-t 60` |
656
- | `-S, --setup` | Setup streams & exit | `--setup` |
731
+ `GET /ping` always returns `200 pong` without checking anything, handy as a liveness probe (use `/health` for readiness):
732
+
733
+ ```bash
734
+ curl localhost:9090/ping
735
+ # pong
736
+ ```
657
737
 
658
738
 
659
739
  ## 🚢 Deployment
@@ -725,6 +805,10 @@ sudo systemctl enable cosmo && sudo systemctl start cosmo
725
805
 
726
806
  **Web UI** — mount `Cosmo::Web` (see [Installation](#-installation)) for a live, htmx-powered dashboard:
727
807
  - **Jobs** — enqueued, scheduled, busy, and dead views, with per-job retry and delete
808
+ - **Processes** — the Jobs dashboard lists every live worker (idle ones included): host, pid, IP, NATS connection
809
+ name, command line, subscribed streams, busy/concurrency, running/quiet/stopping state, memory, NATS RTT and
810
+ reconnects. Each worker
811
+ beats into the `cosmo_processes` KV bucket every 10s, and a crashed worker drops out within 60s
728
812
  - **Streams** — per-stream state (messages, bytes, consumers) with pause/resume
729
813
  - **Crons** — every schedule deployed in NATS, with run-now and delete
730
814
  - **Batches** — open and finished batches with pending/succeeded/failed counts
@@ -4,23 +4,26 @@ module Cosmo
4
4
  module API
5
5
  class Counter
6
6
  STREAM_NAME = "_cosmostats"
7
+ STREAM_CONFIG = {
8
+ subjects: ["#{STREAM_NAME}.>"],
9
+ allow_msg_counter: true,
10
+ allow_direct: true,
11
+ max_msgs_per_subject: 1,
12
+ description: "Cosmo statistics"
13
+ }.freeze
7
14
 
8
- def self.instance
9
- @instance ||= new("jobs")
15
+ # Creates the counters stream, or brings an existing one up to STREAM_CONFIG. Every stored
16
+ # counter message carries the running total, so only the last one per subject is kept.
17
+ #
18
+ # @return [void]
19
+ def self.setup!
20
+ Client.instance.setup_stream(STREAM_NAME, STREAM_CONFIG)
10
21
  end
11
22
 
12
23
  def initialize(namespace)
13
24
  @namespace = namespace
14
25
  end
15
26
 
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
27
  def increment(key, by: 1, msg_id: nil)
25
28
  publish(key, "+#{by}", msg_id: msg_id)
26
29
  end
@@ -64,7 +67,7 @@ module Cosmo
64
67
  raise if rescued
65
68
 
66
69
  rescued = true
67
- client.create_stream(STREAM_NAME, subjects: ["#{STREAM_NAME}.>"], allow_msg_counter: true, allow_direct: true, description: "Cosmo statistics")
70
+ client.create_stream(STREAM_NAME, STREAM_CONFIG)
68
71
  retry
69
72
  end
70
73
  end
@@ -54,12 +54,31 @@ module Cosmo
54
54
  parts.join(".")
55
55
  end
56
56
 
57
- # Subject where NATS fires the generated job message.
58
- # Must be a subject covered by the same stream.
57
+ # Subject where NATS fires the generated job message. A schedule may only target a
58
+ # subject its own stream covers, so every schedule fires into the scheduled stream and
59
+ # the scheduler forwards the job on to {#dispatch_subject}.
59
60
  def target_subject
61
+ "jobs.#{::Cosmo::Job::StreamFilter::SCHEDULED}.#{job_name}"
62
+ end
63
+
64
+ # Subject the scheduler dispatches the fired job to, on the stream that runs it.
65
+ def dispatch_subject
60
66
  "jobs.#{@stream}.#{job_name}"
61
67
  end
62
68
 
69
+ # Headers stored on the schedule template. NATS copies the +X-+ ones onto every message
70
+ # it fires, which is how the scheduler learns where the job belongs.
71
+ def schedule_headers
72
+ headers = {
73
+ "Nats-Schedule" => @expression,
74
+ "Nats-Schedule-Target" => target_subject,
75
+ "X-Stream" => @stream,
76
+ "X-Subject" => dispatch_subject
77
+ }
78
+ headers["Nats-Schedule-Time-Zone"] = @timezone if @timezone
79
+ headers
80
+ end
81
+
63
82
  def job_name
64
83
  @job_name ||= Utils::String.underscore(@class_name)
65
84
  end
@@ -73,7 +92,8 @@ module Cosmo
73
92
  args: @args,
74
93
  name: @name,
75
94
  schedule_subject: schedule_subject,
76
- target_subject: target_subject
95
+ target_subject: target_subject,
96
+ dispatch_subject: dispatch_subject
77
97
  }.compact
78
98
  end
79
99
 
@@ -10,18 +10,22 @@ module Cosmo
10
10
  # Derives the schedule list entirely from NATS.
11
11
  # Whatever is deployed in NATS is exactly what appears in the UI.
12
12
  #
13
- # Schedule templates live in the same job stream they target (e.g. +default+),
14
- # stored at subjects matching +cosmo.cron.<stream>.>+. NATS 2.14 fires each
15
- # template by publishing the body to +Nats-Schedule-Target+ as a regular
16
- # JetStream message that accumulates alongside pending jobs.
13
+ # Every schedule template lives in the +scheduled+ stream, at a subject matching
14
+ # +cosmo.cron.<target stream>.>+. NATS fires a template by publishing its body to
15
+ # +Nats-Schedule-Target+, which has to be a subject the storing stream covers, so a firing
16
+ # lands back in +scheduled+ and the job processor's scheduler dispatches it to the stream
17
+ # that runs it. That confines message scheduling - and the +discard: old+ NATS demands
18
+ # wherever scheduling is enabled - to a single stream.
17
19
  class Cron
20
+ STREAM = ::Cosmo::Job::StreamFilter::SCHEDULED.to_s
21
+
18
22
  def self.instance
19
23
  @instance ||= new
20
24
  end
21
25
 
22
26
  # @return [Array<Hash>] every cron schedule currently deployed in NATS
23
27
  def all
24
- Stream.jobs.flat_map { |s| schedules_from_stream(s.name) }
28
+ schedules
25
29
  rescue StandardError
26
30
  []
27
31
  end
@@ -31,20 +35,14 @@ module Cosmo
31
35
  def upsert!(class_name: nil, stream: nil, schedule: nil, args: [], timezone: nil, name: nil)
32
36
  e = Entry.new(class_name: class_name, stream: stream, expression: schedule,
33
37
  args: args, timezone: timezone, name: name)
34
- headers = {
35
- "Nats-Schedule" => e.expression,
36
- "Nats-Schedule-Target" => e.target_subject
37
- }
38
- headers["Nats-Schedule-Time-Zone"] = e.timezone if e.timezone
39
- client.publish(e.schedule_subject, e.job_payload, stream: e.stream, header: headers)
40
- build_from_nats(e.stream, e.schedule_subject)
38
+ client.publish(e.schedule_subject, e.job_payload, stream: STREAM, header: e.schedule_headers)
39
+ build_from_nats(e.schedule_subject)
41
40
  end
42
41
 
43
42
  # Purge the schedule message from NATS (stops future firings).
44
43
  # @param subject [String]
45
44
  def delete!(subject)
46
- stream_name = subject.to_s.split(".")[2]
47
- client.purge(stream_name, subject)
45
+ client.purge(STREAM, subject)
48
46
  rescue NATS::JetStream::Error::NotFound, NATS::IO::Timeout
49
47
  nil
50
48
  end
@@ -52,13 +50,12 @@ module Cosmo
52
50
  # Dispatch the job immediately to the target stream, bypassing the timer.
53
51
  # @param schedule_subject [String] e.g. "cosmo.cron.default.report_job.daily"
54
52
  def run_now!(schedule_subject) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
55
- stream_name = schedule_subject.to_s.split(".")[2]
56
- msg = client.get_message(stream_name, subject: schedule_subject)
53
+ msg = client.get_message(STREAM, subject: schedule_subject)
57
54
  return unless msg
58
55
 
59
56
  headers = msg.headers || {}
60
57
  body = Utils::Json.parse(msg.data) || {}
61
- target = headers["Nats-Schedule-Target"]
58
+ target = headers["X-Subject"]
62
59
  return unless target && body[:class]
63
60
 
64
61
  payload = Utils::Json.dump({
@@ -68,7 +65,7 @@ module Cosmo
68
65
  retry: body[:retry] || Job::Data.default_retry,
69
66
  dead: body[:dead].nil? ? Job::Data::DEFAULTS[:dead] : body[:dead]
70
67
  })
71
- client.publish(target, payload, stream: stream_name)
68
+ client.publish(target, payload, stream: headers["X-Stream"])
72
69
  rescue NATS::JetStream::Error::NotFound
73
70
  nil
74
71
  end
@@ -79,14 +76,13 @@ module Cosmo
79
76
  @client ||= Client.instance
80
77
  end
81
78
 
82
- def schedules_from_stream(stream_name)
83
- filter = "#{Entry::SUBJECT_PREFIX}.#{stream_name}.>"
84
- subjects = client.cron_subjects_in_stream(stream_name, filter)
85
- subjects.filter_map { |subj| build_from_nats(stream_name, subj) }
79
+ def schedules
80
+ subjects = client.cron_subjects_in_stream(STREAM, "#{Entry::SUBJECT_PREFIX}.>")
81
+ subjects.filter_map { |subj| build_from_nats(subj) }
86
82
  end
87
83
 
88
- def build_from_nats(stream_name, subject)
89
- msg = client.get_message(stream_name, subject: subject)
84
+ def build_from_nats(subject)
85
+ msg = client.get_message(STREAM, subject: subject)
90
86
  return unless msg
91
87
 
92
88
  headers = msg.headers || {}
@@ -94,13 +90,13 @@ module Cosmo
94
90
 
95
91
  {
96
92
  class: body[:class],
97
- stream: stream_name,
93
+ stream: headers["X-Stream"],
98
94
  schedule: headers["Nats-Schedule"],
99
95
  timezone: headers["Nats-Schedule-Time-Zone"],
100
96
  args: body[:args] || [],
101
97
  name: name_from_subject(subject),
102
98
  schedule_subject: subject,
103
- target_subject: headers["Nats-Schedule-Target"],
99
+ dispatch_subject: headers["X-Subject"],
104
100
  registry_key: subject.split(".").drop(2).join("/")
105
101
  }
106
102
  rescue StandardError
data/lib/cosmo/api/job.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
4
+
3
5
  module Cosmo
4
6
  module API
5
7
  class Job
@@ -38,9 +40,37 @@ module Cosmo
38
40
  @message.subject
39
41
  end
40
42
 
43
+ def error_class
44
+ headers&.dig("X-Error-Class")
45
+ end
46
+
47
+ def error_message
48
+ headers&.dig("X-Error-Message")
49
+ end
50
+
51
+ def error_backtrace
52
+ headers&.dig("X-Error-Backtrace")
53
+ end
54
+
55
+ # @return [Array<String>] the backtrace frames, which travel as a single header line
56
+ def error_backtrace_lines
57
+ error_backtrace.to_s.split(::Cosmo::Job::Failure::BACKTRACE_SEPARATOR)
58
+ end
59
+
60
+ def error?
61
+ !error_class.to_s.empty? || !error_message.to_s.empty?
62
+ end
63
+
41
64
  def timestamp
42
65
  headers&.dig("Nats-Time-Stamp")
43
66
  end
67
+
68
+ # @return [Time, nil] when the message was stored in its stream, parsed from +Nats-Time-Stamp+
69
+ def stored_at
70
+ Time.iso8601(timestamp) if timestamp
71
+ rescue ::ArgumentError
72
+ nil
73
+ end
44
74
  end
45
75
  end
46
76
  end