kaal 0.2.1 → 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 +81 -286
- data/Rakefile +4 -2
- data/config/kaal.rb +15 -0
- data/config/scheduler.yml +12 -0
- data/{lib/tasks/kaal_tasks.rake → exe/kaal} +5 -3
- data/lib/kaal/backend/adapter.rb +0 -1
- data/lib/kaal/backend/dispatch_attempt_logger.rb +33 -0
- data/lib/kaal/backend/dispatch_logging.rb +36 -23
- data/lib/kaal/backend/dispatch_registry_accessor.rb +43 -0
- data/lib/kaal/backend/memory_adapter.rb +7 -5
- data/lib/kaal/backend/redis_adapter.rb +6 -6
- data/lib/kaal/cli.rb +230 -0
- data/lib/kaal/{configuration.rb → config/configuration.rb} +0 -1
- data/lib/kaal/{scheduler_config_error.rb → config/scheduler_config_error.rb} +0 -1
- data/lib/kaal/config/scheduler_time_zone_resolver.rb +50 -0
- data/lib/kaal/config.rb +19 -0
- data/lib/kaal/{coordinator.rb → core/coordinator.rb} +42 -62
- data/lib/kaal/core/enabled_entry_enumerator.rb +51 -0
- data/lib/kaal/core/occurrence_finder.rb +38 -0
- data/lib/kaal/core.rb +18 -0
- data/lib/kaal/definition/memory_engine.rb +11 -18
- data/lib/kaal/definition/persistence_helpers.rb +31 -0
- data/lib/kaal/definition/redis_engine.rb +9 -6
- data/lib/kaal/definition/registry.rb +24 -2
- data/lib/kaal/definitions/registration_service.rb +62 -0
- data/lib/kaal/definitions/registry_accessor.rb +33 -0
- data/lib/kaal/dispatch/memory_engine.rb +3 -4
- data/lib/kaal/dispatch/redis_engine.rb +2 -3
- data/lib/kaal/dispatch/registry.rb +0 -1
- data/lib/kaal/register_conflict_support.rb +0 -1
- data/lib/kaal/registry.rb +0 -1
- data/lib/kaal/runtime/runtime_context.rb +41 -0
- data/lib/kaal/runtime/scheduler_boot_loader.rb +52 -0
- data/lib/kaal/runtime/signal_handler_chain.rb +42 -0
- data/lib/kaal/runtime/signal_handler_installer.rb +39 -0
- data/lib/kaal/runtime.rb +20 -0
- data/lib/kaal/scheduler_file/hash_transform.rb +22 -0
- data/lib/kaal/scheduler_file/helper_bundle.rb +28 -0
- data/lib/kaal/scheduler_file/job_applier.rb +242 -0
- data/lib/kaal/scheduler_file/job_normalizer.rb +90 -0
- data/lib/kaal/scheduler_file/loader.rb +152 -0
- data/lib/kaal/scheduler_file/payload_loader.rb +95 -0
- data/lib/kaal/{scheduler_placeholder_support.rb → scheduler_file/placeholder_support.rb} +0 -1
- data/lib/kaal/scheduler_file.rb +18 -0
- data/lib/kaal/support/hash_tools.rb +93 -0
- data/lib/kaal/{cron_humanizer.rb → utils/cron_humanizer.rb} +19 -1
- data/lib/kaal/{cron_utils.rb → utils/cron_utils.rb} +0 -1
- data/lib/kaal/{idempotency_key_generator.rb → utils/idempotency_key_generator.rb} +3 -3
- data/lib/kaal/utils.rb +18 -0
- data/lib/kaal/version.rb +1 -2
- data/lib/kaal.rb +77 -397
- metadata +64 -44
- data/app/models/kaal/cron_definition.rb +0 -76
- data/app/models/kaal/cron_dispatch.rb +0 -50
- data/app/models/kaal/cron_lock.rb +0 -38
- data/lib/generators/kaal/install/install_generator.rb +0 -72
- data/lib/generators/kaal/install/templates/create_kaal_definitions.rb.tt +0 -21
- data/lib/generators/kaal/install/templates/create_kaal_dispatches.rb.tt +0 -20
- data/lib/generators/kaal/install/templates/create_kaal_locks.rb.tt +0 -17
- data/lib/generators/kaal/install/templates/kaal.rb.tt +0 -31
- data/lib/generators/kaal/install/templates/scheduler.yml.tt +0 -22
- data/lib/kaal/backend/mysql_adapter.rb +0 -170
- data/lib/kaal/backend/postgres_adapter.rb +0 -134
- data/lib/kaal/backend/sqlite_adapter.rb +0 -116
- data/lib/kaal/definition/database_engine.rb +0 -50
- data/lib/kaal/dispatch/database_engine.rb +0 -94
- data/lib/kaal/railtie.rb +0 -183
- data/lib/kaal/rake_tasks.rb +0 -184
- data/lib/kaal/scheduler_file_loader.rb +0 -321
- data/lib/kaal/scheduler_hash_transform.rb +0 -45
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54a0eb1cebfc4adc18c4b4ed105d47bb26a84d3512fbce891243a356bca5715a
|
|
4
|
+
data.tar.gz: dd1fb18c3060c4688ea0f96c5628cd40001fb318b7745d411c46108fa06b45b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fda7173306897889b750707be46778bfc0b4893ce87759f30f060a37879212eab2731316a31c875acdcf0e7aad9db1caddbc11e7c0b0b16e3707edbfd1b1eec3
|
|
7
|
+
data.tar.gz: a8d64f8ca728665167e8be7ad5e1e6a417066f6016ff2b838aa47cde5aa3369045b1ea2c3aaf326ce4b00cc94d0f519ebe464978660ad13cc29f6db90cf3c834
|
data/README.md
CHANGED
|
@@ -1,340 +1,135 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Kaal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Distributed cron scheduling for plain Ruby.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
[](https://github.com/Code-Vedas/kaal/actions/workflows/ci.yml)
|
|
7
|
-
[](https://qlty.sh/gh/Code-Vedas/projects/kaal)
|
|
8
|
-
[](https://qlty.sh/gh/Code-Vedas/projects/kaal)
|
|
9
|
-

|
|
10
|
-

|
|
11
|
-

|
|
5
|
+
`kaal` is the core engine gem. It owns scheduler/runtime behavior, the registry APIs, and the plain Ruby CLI. SQL persistence lives in adapter gems such as `kaal-sequel` and `kaal-activerecord`.
|
|
12
6
|
|
|
13
|
-
|
|
7
|
+
## Installation
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`kaal` lets you **bind cron expressions to Ruby code or shell commands** without depending on any specific job system or scheduler.
|
|
18
|
-
|
|
19
|
-
It guarantees that:
|
|
20
|
-
|
|
21
|
-
- Each scheduled tick **enqueues work exactly once** across all running Ruby nodes.
|
|
22
|
-
- You remain **agnostic** to your background job system (`ActiveJob`, `Sidekiq`, `Resque`, etc.).
|
|
23
|
-
- Locks are coordinated safely via **Redis** or **PostgreSQL advisory locks**.
|
|
24
|
-
- Cron syntax can be **validated, linted, humanized, and translated** with i18n.
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## 🧩 Why Kaal?
|
|
29
|
-
|
|
30
|
-
| Problem | Kaal Solution |
|
|
31
|
-
| ------------------------------------ | ----------------------------------------------------- |
|
|
32
|
-
| Multiple nodes running the same cron | Distributed locks → exactly-once execution |
|
|
33
|
-
| Cron syntax not human-friendly | Built-in parser + `to_human` translations |
|
|
34
|
-
| Missed runs during downtime | Configurable _lookback_ window replays missed ticks |
|
|
35
|
-
| Coupled to job system | Scheduler-agnostic, works with any Ruby queue backend |
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## ⚙️ Installation
|
|
40
|
-
|
|
41
|
-
Add to your Gemfile:
|
|
9
|
+
Use `kaal` by itself when you want the engine plus non-SQL coordination backends.
|
|
42
10
|
|
|
43
11
|
```ruby
|
|
44
|
-
gem
|
|
12
|
+
gem 'kaal'
|
|
45
13
|
```
|
|
46
14
|
|
|
47
|
-
Then
|
|
15
|
+
Then install and initialize:
|
|
48
16
|
|
|
49
17
|
```bash
|
|
50
18
|
bundle install
|
|
51
|
-
|
|
19
|
+
bundle exec kaal init --backend=memory
|
|
52
20
|
```
|
|
53
21
|
|
|
54
|
-
|
|
22
|
+
`kaal init` creates:
|
|
55
23
|
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
- `--backend=redis` or `--backend=memory`: no database migrations
|
|
59
|
-
|
|
60
|
-
Example initializer (`config/initializers/kaal.rb`):
|
|
61
|
-
|
|
62
|
-
```ruby
|
|
63
|
-
Kaal.configure do |c|
|
|
64
|
-
# Choose your backend adapter
|
|
65
|
-
# See the Kaal documentation for backend-specific setup and the full
|
|
66
|
-
# configuration reference.
|
|
67
|
-
#
|
|
68
|
-
# Redis (recommended)
|
|
69
|
-
# c.backend = Kaal::Backend::RedisAdapter.new(Redis.new(url: ENV["REDIS_URL"]))
|
|
70
|
-
|
|
71
|
-
# or Postgres advisory locks
|
|
72
|
-
# c.backend = Kaal::Backend::PostgresAdapter.new
|
|
73
|
-
|
|
74
|
-
c.tick_interval = 5 # seconds between scheduler ticks
|
|
75
|
-
c.window_lookback = 120 # recover missed runs (seconds)
|
|
76
|
-
c.lease_ttl = 125 # must be >= window_lookback + tick_interval
|
|
77
|
-
c.recovery_window = 3600
|
|
78
|
-
c.enable_dispatch_recovery = true
|
|
79
|
-
c.enable_log_dispatch_registry = false
|
|
80
|
-
|
|
81
|
-
# Scheduler file loading
|
|
82
|
-
c.scheduler_config_path = "config/scheduler.yml"
|
|
83
|
-
c.scheduler_conflict_policy = :error # :error, :code_wins, :file_wins
|
|
84
|
-
c.scheduler_missing_file_policy = :warn # :warn, :error
|
|
85
|
-
end
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Example scheduler file (`config/scheduler.yml`):
|
|
89
|
-
|
|
90
|
-
```yaml
|
|
91
|
-
defaults:
|
|
92
|
-
jobs:
|
|
93
|
-
- key: "reports:weekly_summary"
|
|
94
|
-
cron: "0 9 * * 1"
|
|
95
|
-
job_class: "WeeklySummaryJob"
|
|
96
|
-
enabled: true
|
|
97
|
-
queue: "default"
|
|
98
|
-
args:
|
|
99
|
-
- "{{fire_time.iso8601}}"
|
|
100
|
-
kwargs:
|
|
101
|
-
idempotency_key: "{{idempotency_key}}"
|
|
102
|
-
metadata:
|
|
103
|
-
owner: "ops"
|
|
24
|
+
- `config/kaal.rb`
|
|
25
|
+
- `config/scheduler.yml`
|
|
104
26
|
|
|
105
|
-
|
|
106
|
-
jobs:
|
|
107
|
-
- key: "reports:daily_digest"
|
|
108
|
-
cron: "<%= ENV.fetch('DAILY_DIGEST_CRON', '0 7 * * *') %>"
|
|
109
|
-
job_class: "DailyDigestJob"
|
|
110
|
-
```
|
|
27
|
+
Supported backends:
|
|
111
28
|
|
|
112
|
-
|
|
113
|
-
|
|
29
|
+
- `memory`
|
|
30
|
+
- `redis`
|
|
114
31
|
|
|
115
|
-
|
|
32
|
+
If you want SQL persistence instead, add one of:
|
|
116
33
|
|
|
117
|
-
|
|
34
|
+
- `kaal-sequel` for Sequel-backed SQL in plain Ruby
|
|
35
|
+
- `kaal-activerecord` for Active Record-backed SQL in plain Ruby
|
|
36
|
+
- `kaal-rails` for Rails
|
|
118
37
|
|
|
119
|
-
##
|
|
38
|
+
## Configuration
|
|
120
39
|
|
|
121
|
-
|
|
40
|
+
Generated `config/kaal.rb` is the primary entrypoint:
|
|
122
41
|
|
|
123
42
|
```ruby
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
Start the scheduler:
|
|
134
|
-
|
|
135
|
-
`Kaal` does **not** auto-start by default. Start it explicitly via `Kaal.start!` or `kaal:start`.
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
bundle exec rails kaal:start
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
> 💡 Recommended: run as a **dedicated process** in production (Procfile, systemd, Kubernetes).
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## 🧰 CLI & Rake Tasks
|
|
146
|
-
|
|
147
|
-
### CLI Examples
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
$ kaal explain "*/15 * * * *"
|
|
151
|
-
Every 15 minutes
|
|
152
|
-
|
|
153
|
-
$ kaal next "0 9 * * 1" --count 3
|
|
154
|
-
2025-11-03 09:00:00 UTC
|
|
155
|
-
2025-11-10 09:00:00 UTC
|
|
156
|
-
2025-11-17 09:00:00 UTC
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Rails Tasks
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
bin/rails kaal:start # Start scheduler loop
|
|
163
|
-
bin/rails kaal:status # Show registry & configuration
|
|
164
|
-
bin/rails kaal:tick # Trigger one tick manually
|
|
165
|
-
bin/rails kaal:explain["*/5 * * * *"] # Humanize cron expression
|
|
43
|
+
require 'kaal'
|
|
44
|
+
|
|
45
|
+
Kaal.configure do |config|
|
|
46
|
+
config.backend = Kaal::Backend::MemoryAdapter.new
|
|
47
|
+
config.tick_interval = 5
|
|
48
|
+
config.window_lookback = 120
|
|
49
|
+
config.lease_ttl = 125
|
|
50
|
+
config.scheduler_config_path = 'config/scheduler.yml'
|
|
51
|
+
end
|
|
166
52
|
```
|
|
167
53
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
## 🧠 Cron Utilities
|
|
54
|
+
Redis path:
|
|
171
55
|
|
|
172
56
|
```ruby
|
|
173
|
-
|
|
174
|
-
Kaal.simplify("0 0 * * *") # => "@daily"
|
|
175
|
-
Kaal.lint("*/61 * * * *") # => ["invalid minute step: 61"]
|
|
176
|
-
|
|
177
|
-
I18n.locale = :fr
|
|
178
|
-
Kaal.to_human("0 9 * * 1") # => "À 09h00 chaque lundi"
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
> 🈶 All tokens are i18n-based — override translations under `kaal.*` keys.
|
|
57
|
+
require 'redis'
|
|
182
58
|
|
|
183
|
-
|
|
59
|
+
redis = Redis.new(url: ENV.fetch('REDIS_URL'))
|
|
184
60
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
61
|
+
Kaal.configure do |config|
|
|
62
|
+
config.backend = Kaal::Backend::RedisAdapter.new(redis)
|
|
63
|
+
config.scheduler_config_path = 'config/scheduler.yml'
|
|
64
|
+
end
|
|
65
|
+
```
|
|
189
66
|
|
|
190
|
-
|
|
67
|
+
Time zone behavior is explicit:
|
|
191
68
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
scheduler: bundle exec rails kaal:start
|
|
195
|
-
```
|
|
69
|
+
- use `config.time_zone = 'America/Toronto'` when needed
|
|
70
|
+
- otherwise scheduling runs in `UTC`
|
|
196
71
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```ini
|
|
200
|
-
[Unit]
|
|
201
|
-
Description=Kaal scheduler
|
|
202
|
-
After=network.target
|
|
203
|
-
|
|
204
|
-
[Service]
|
|
205
|
-
Type=simple
|
|
206
|
-
User=deploy
|
|
207
|
-
WorkingDirectory=/var/apps/myapp/current
|
|
208
|
-
Environment=RAILS_ENV=production
|
|
209
|
-
ExecStart=/usr/bin/bash -lc 'bundle exec rails kaal:start'
|
|
210
|
-
ExecStartPre=/usr/bin/bash -lc 'bundle exec rails kaal:status'
|
|
211
|
-
ExecReload=/bin/kill -TERM $MAINPID
|
|
212
|
-
Restart=always
|
|
213
|
-
RestartSec=5
|
|
214
|
-
|
|
215
|
-
[Install]
|
|
216
|
-
WantedBy=multi-user.target
|
|
217
|
-
```
|
|
72
|
+
## Scheduler File
|
|
218
73
|
|
|
219
|
-
|
|
74
|
+
Default scheduler definitions live at `config/scheduler.yml`:
|
|
220
75
|
|
|
221
76
|
```yaml
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
metadata:
|
|
233
|
-
labels:
|
|
234
|
-
app: kaal-scheduler
|
|
235
|
-
spec:
|
|
236
|
-
containers:
|
|
237
|
-
- name: scheduler
|
|
238
|
-
image: your-app:latest
|
|
239
|
-
command: ["bash", "-lc", "bundle exec rails kaal:start"]
|
|
77
|
+
defaults:
|
|
78
|
+
jobs:
|
|
79
|
+
- key: "example:heartbeat"
|
|
80
|
+
cron: "*/5 * * * *"
|
|
81
|
+
job_class: "ExampleHeartbeatJob"
|
|
82
|
+
enabled: true
|
|
83
|
+
args:
|
|
84
|
+
- "{{fire_time.iso8601}}"
|
|
85
|
+
kwargs:
|
|
86
|
+
idempotency_key: "{{idempotency_key}}"
|
|
240
87
|
```
|
|
241
88
|
|
|
242
|
-
|
|
243
|
-
Do not use `kaal:status` as a liveness/readiness probe for scheduler thread health because it runs in a separate process.
|
|
244
|
-
|
|
245
|
-
Use one of these for health checks:
|
|
246
|
-
|
|
247
|
-
- Process-level checks from your runtime/supervisor for the main scheduler process.
|
|
248
|
-
- A shared heartbeat/lease signal (Redis, Postgres, pidfile, etc.) written by the scheduler and read by probes.
|
|
249
|
-
|
|
250
|
-
---
|
|
251
|
-
|
|
252
|
-
## 🔍 Troubleshooting
|
|
253
|
-
|
|
254
|
-
| Symptom | Likely Cause | Fix |
|
|
255
|
-
| -------------------------- | --------------------- | ---------------------------------------- |
|
|
256
|
-
| Jobs run multiple times | Using memory lock | Use Redis or Postgres adapter |
|
|
257
|
-
| Missed jobs after downtime | Short lookback window | Increase `window_lookback` |
|
|
258
|
-
| Scheduler exits early | Normal SIGTERM | Exits gracefully after tick |
|
|
259
|
-
| Redis timeout | Network latency | Increase Redis timeout or switch adapter |
|
|
89
|
+
`job_class` must resolve to a Ruby constant that responds to one of:
|
|
260
90
|
|
|
261
|
-
|
|
91
|
+
- `.perform(*args, **kwargs)`
|
|
92
|
+
- `.perform_later(*args, **kwargs)`
|
|
93
|
+
- `.set(queue: ...).perform_later(*args, **kwargs)`
|
|
262
94
|
|
|
263
|
-
|
|
95
|
+
For plain Ruby `.perform` jobs, Kaal treats the dispatch as successful unless the job raises an exception. Return values are ignored.
|
|
264
96
|
|
|
265
|
-
##
|
|
266
|
-
|
|
267
|
-
```ruby
|
|
268
|
-
RSpec.describe "multi-node safety" do
|
|
269
|
-
it "dispatches exactly once across two threads" do
|
|
270
|
-
redis = FakeRedis::Redis.new
|
|
271
|
-
lock = Kaal::Backend::RedisAdapter.new(redis)
|
|
272
|
-
Kaal.configure { |c| c.backend = lock }
|
|
273
|
-
|
|
274
|
-
threads = 2.times.map { Thread.new { Kaal.tick! } }
|
|
275
|
-
threads.each(&:join)
|
|
276
|
-
|
|
277
|
-
expect(redis.keys.grep(/dispatch/).size).to eq(1)
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Local Test Commands
|
|
283
|
-
|
|
284
|
-
Run the fast unit suite from the gem directory:
|
|
97
|
+
## CLI
|
|
285
98
|
|
|
286
99
|
```bash
|
|
287
|
-
|
|
100
|
+
bundle exec kaal init --backend=memory
|
|
101
|
+
bundle exec kaal start
|
|
102
|
+
bundle exec kaal status
|
|
103
|
+
bundle exec kaal tick
|
|
104
|
+
bundle exec kaal explain "*/15 * * * *"
|
|
105
|
+
bundle exec kaal next "0 9 * * 1" --count 3
|
|
288
106
|
```
|
|
289
107
|
|
|
290
|
-
|
|
108
|
+
## E2E Verification
|
|
291
109
|
|
|
292
110
|
```bash
|
|
293
111
|
bin/rspec-e2e memory
|
|
294
|
-
bin/rspec-e2e sqlite
|
|
295
|
-
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/kaal_test bin/rspec-e2e pg
|
|
296
|
-
DATABASE_URL=mysql2://root:rootROOT\!1@127.0.0.1:3306/kaal_test bin/rspec-e2e mysql
|
|
297
112
|
REDIS_URL=redis://127.0.0.1:6379/0 bin/rspec-e2e redis
|
|
298
113
|
```
|
|
299
114
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## 🧩 Roadmap
|
|
305
|
-
|
|
306
|
-
| Area | Description | Label |
|
|
307
|
-
| ---------------- | -------------------------------- | --------- |
|
|
308
|
-
| Registry & API | Cron registration and validation | `feature` |
|
|
309
|
-
| Coordinator Loop | Safe ticking and dispatch | `feature` |
|
|
310
|
-
| Lock Adapters | Redis / Postgres | `build` |
|
|
311
|
-
| CLI Tool | `kaal` executable | `build` |
|
|
312
|
-
| i18n Humanizer | Multi-language support | `lang` |
|
|
313
|
-
| Docs & Examples | Developer onboarding | `lang` |
|
|
115
|
+
## Runtime API
|
|
314
116
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
Labels: `feature`, `build`, `ci`, `lang`
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
|
-
## 📚 Documentation
|
|
117
|
+
```ruby
|
|
118
|
+
Kaal.register(
|
|
119
|
+
key: 'reports:daily',
|
|
120
|
+
cron: '0 9 * * *',
|
|
121
|
+
enqueue: ->(fire_time:, idempotency_key:) {
|
|
122
|
+
ReportsJob.perform(fire_time: fire_time, idempotency_key: idempotency_key)
|
|
123
|
+
}
|
|
124
|
+
)
|
|
329
125
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
- [Usage](https://kaal.codevedas.com/usage)
|
|
333
|
-
- [FAQ / Troubleshooting](https://kaal.codevedas.com/faq)
|
|
126
|
+
Kaal.start!
|
|
127
|
+
```
|
|
334
128
|
|
|
335
|
-
|
|
129
|
+
## Adapter Gems
|
|
336
130
|
|
|
337
|
-
|
|
131
|
+
Use adapter gems when you want persisted SQL registries:
|
|
338
132
|
|
|
339
|
-
|
|
340
|
-
|
|
133
|
+
- `kaal-sequel` for Sequel-backed persistence
|
|
134
|
+
- `kaal-activerecord` for Active Record-backed persistence
|
|
135
|
+
- `kaal-rails` for Rails plugin integration over `kaal-activerecord`
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
3
7
|
require 'bundler/setup'
|
|
4
|
-
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
|
5
|
-
load 'rails/tasks/engine.rake'
|
|
6
8
|
require 'bundler/gem_tasks'
|
data/config/kaal.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'kaal'
|
|
8
|
+
|
|
9
|
+
Kaal.configure do |config|
|
|
10
|
+
config.backend = Kaal::Backend::MemoryAdapter.new
|
|
11
|
+
config.tick_interval = 5
|
|
12
|
+
config.window_lookback = 120
|
|
13
|
+
config.lease_ttl = 125
|
|
14
|
+
config.scheduler_config_path = 'config/scheduler.yml'
|
|
15
|
+
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
# Copyright Codevedas Inc. 2025-present
|
|
4
5
|
#
|
|
5
6
|
# This source code is licensed under the MIT license found in the
|
|
6
7
|
# LICENSE file in the root directory of this source tree.
|
|
8
|
+
gemfile_path = File.expand_path('../Gemfile', __dir__)
|
|
9
|
+
require 'bundler/setup' if File.exist?(gemfile_path)
|
|
10
|
+
require 'kaal/cli'
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Kaal::RakeTasks.install
|
|
12
|
+
Kaal::CLI.start(ARGV)
|
data/lib/kaal/backend/adapter.rb
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'socket'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
module Backend
|
|
11
|
+
# Logs dispatch attempts through a backend-provided dispatch registry.
|
|
12
|
+
class DispatchAttemptLogger
|
|
13
|
+
def initialize(configuration:, dispatch_registry_provider:, logger: nil, node_id_provider: Socket.method(:gethostname))
|
|
14
|
+
@configuration = configuration
|
|
15
|
+
@dispatch_registry_provider = dispatch_registry_provider
|
|
16
|
+
@logger = logger
|
|
17
|
+
@node_id_provider = node_id_provider
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(lock_key)
|
|
21
|
+
return unless @configuration.enable_log_dispatch_registry
|
|
22
|
+
|
|
23
|
+
registry = @dispatch_registry_provider.call
|
|
24
|
+
return unless registry
|
|
25
|
+
|
|
26
|
+
cron_key, fire_time = DispatchLogging.parse_lock_key(lock_key)
|
|
27
|
+
registry.log_dispatch(cron_key, fire_time, @node_id_provider.call, 'dispatched')
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
(@logger || @configuration.logger)&.error("Failed to log dispatch for #{lock_key}: #{e.message}")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# This source code is licensed under the MIT license found in the
|
|
6
6
|
# LICENSE file in the root directory of this source tree.
|
|
7
|
-
|
|
8
|
-
require 'socket'
|
|
7
|
+
require_relative 'dispatch_attempt_logger'
|
|
9
8
|
|
|
10
9
|
module Kaal
|
|
11
10
|
module Backend
|
|
@@ -25,6 +24,10 @@ module Kaal
|
|
|
25
24
|
# end
|
|
26
25
|
# end
|
|
27
26
|
module DispatchLogging
|
|
27
|
+
def dispatch_registry
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
28
31
|
##
|
|
29
32
|
# Log a dispatch attempt via the dispatch registry.
|
|
30
33
|
#
|
|
@@ -33,23 +36,7 @@ module Kaal
|
|
|
33
36
|
# @param key [String] the lock key (format: "namespace:dispatch:cron_key:fire_time")
|
|
34
37
|
# @return [void]
|
|
35
38
|
def log_dispatch_attempt(key)
|
|
36
|
-
|
|
37
|
-
logging_enabled = Kaal.configuration.then do |configuration|
|
|
38
|
-
logger = configuration.logger
|
|
39
|
-
configuration.enable_log_dispatch_registry
|
|
40
|
-
end
|
|
41
|
-
return unless logging_enabled
|
|
42
|
-
return unless respond_to?(:dispatch_registry)
|
|
43
|
-
|
|
44
|
-
registry = dispatch_registry
|
|
45
|
-
return unless registry
|
|
46
|
-
|
|
47
|
-
cron_key, fire_time = parse_lock_key(key)
|
|
48
|
-
node_id = Socket.gethostname
|
|
49
|
-
|
|
50
|
-
registry.log_dispatch(cron_key, fire_time, node_id, 'dispatched')
|
|
51
|
-
rescue StandardError => e
|
|
52
|
-
logger&.error("Failed to log dispatch for #{key}: #{e.message}")
|
|
39
|
+
dispatch_attempt_logger.call(key)
|
|
53
40
|
end
|
|
54
41
|
|
|
55
42
|
##
|
|
@@ -67,13 +54,39 @@ module Kaal
|
|
|
67
54
|
|
|
68
55
|
def self.parse_lock_key(key)
|
|
69
56
|
parts = key.split(':')
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
57
|
+
invalid_message = "Invalid dispatch lock key format: #{key.inspect}"
|
|
58
|
+
dispatch_index = parts[0...-1].rindex('dispatch')
|
|
59
|
+
timestamp = parts[-1]
|
|
60
|
+
valid_key = parts.length >= 4 && dispatch_index&.positive? && timestamp.match?(/\A\d+\z/)
|
|
61
|
+
validate_lock_key!(valid_key, invalid_message)
|
|
62
|
+
|
|
63
|
+
fire_time_unix = timestamp.to_i
|
|
64
|
+
cron_key = parts[(dispatch_index + 1)...-1].join(':')
|
|
65
|
+
validate_lock_key!(!cron_key.empty?, invalid_message)
|
|
66
|
+
|
|
67
|
+
fire_time = Time.at(fire_time_unix).utc
|
|
74
68
|
|
|
75
69
|
[cron_key, fire_time]
|
|
76
70
|
end
|
|
71
|
+
|
|
72
|
+
def self.validate_lock_key!(valid, message)
|
|
73
|
+
invalid_dispatch_lock_key!(message) unless valid
|
|
74
|
+
end
|
|
75
|
+
private_class_method :validate_lock_key!
|
|
76
|
+
|
|
77
|
+
def self.invalid_dispatch_lock_key!(message)
|
|
78
|
+
raise ArgumentError, message
|
|
79
|
+
end
|
|
80
|
+
private_class_method :invalid_dispatch_lock_key!
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def dispatch_attempt_logger
|
|
85
|
+
@dispatch_attempt_logger ||= DispatchAttemptLogger.new(
|
|
86
|
+
configuration: Kaal.configuration,
|
|
87
|
+
dispatch_registry_provider: -> { dispatch_registry }
|
|
88
|
+
)
|
|
89
|
+
end
|
|
77
90
|
end
|
|
78
91
|
end
|
|
79
92
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
module Kaal
|
|
8
|
+
module Backend
|
|
9
|
+
# Reads dispatch registry state through the configured backend adapter.
|
|
10
|
+
class DispatchRegistryAccessor
|
|
11
|
+
def initialize(configuration:)
|
|
12
|
+
@configuration = configuration
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def dispatched?(key, fire_time)
|
|
16
|
+
registry = fetch_registry
|
|
17
|
+
return false unless registry
|
|
18
|
+
|
|
19
|
+
registry.dispatched?(key, fire_time)
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
@configuration.logger&.warn("Error checking dispatch status for #{key}: #{e.message}")
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def registry
|
|
26
|
+
fetch_registry
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
@configuration.logger&.warn("Error accessing dispatch registry: #{e.message}")
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def fetch_registry
|
|
35
|
+
adapter = @configuration.backend
|
|
36
|
+
return nil unless adapter
|
|
37
|
+
return nil unless adapter.respond_to?(:dispatch_registry)
|
|
38
|
+
|
|
39
|
+
adapter.dispatch_registry
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|