kaal 0.2.1 → 0.4.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 +79 -287
- 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/active_record_support.rb +82 -0
- 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/mysql.rb +41 -0
- data/lib/kaal/backend/postgres.rb +41 -0
- data/lib/kaal/backend/redis_adapter.rb +6 -6
- data/lib/kaal/backend/sqlite.rb +41 -0
- 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/database_engine.rb +54 -16
- 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/database_engine.rb +87 -61
- 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/internal/active_record/base_record.rb +16 -0
- data/lib/kaal/internal/active_record/connection_support.rb +96 -0
- data/lib/kaal/internal/active_record/database_backend.rb +73 -0
- data/lib/kaal/internal/active_record/definition_record.rb +16 -0
- data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
- data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
- data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
- data/lib/kaal/internal/active_record/lock_record.rb +16 -0
- data/lib/kaal/internal/active_record/migration_templates.rb +108 -0
- data/lib/kaal/internal/active_record/mysql_backend.rb +71 -0
- data/lib/kaal/internal/active_record/postgres_backend.rb +69 -0
- data/lib/kaal/internal/active_record.rb +17 -0
- data/lib/kaal/internal/sequel/database_backend.rb +74 -0
- data/lib/kaal/internal/sequel/mysql_backend.rb +69 -0
- data/lib/kaal/internal/sequel/postgres_backend.rb +67 -0
- data/lib/kaal/internal/sequel.rb +12 -0
- data/lib/kaal/persistence/database.rb +35 -0
- data/lib/kaal/persistence/migration_templates.rb +97 -0
- data/lib/kaal/register_conflict_support.rb +0 -1
- data/lib/kaal/registry.rb +0 -3
- 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/sequel_support.rb +82 -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 +83 -397
- metadata +87 -42
- 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/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: f078489fc2106826b98a1890f15dd5aad38a627ef9db230bd11db3785dc66f86
|
|
4
|
+
data.tar.gz: 10ec39fb62e2082d7a8191153af335950fb14d584c0ed33737298baaa2b2f270
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5be3591420c49e0149f58e6d76abdb3bfc7a7c10f2249754fb427f89e9ce414bad044d44fc17ed8016b4766182558a2a513598dcc212a0d899872f31007da19b
|
|
7
|
+
data.tar.gz: 60857c850fe9ca808fd198bcd8480ee63962bc1df5de69bab5953168aa7641f1a739c74d970c67b20a7f71d1858abe2a043ac004191b473a94aee7380cf8ce50
|
data/README.md
CHANGED
|
@@ -1,340 +1,132 @@
|
|
|
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, the plain Ruby CLI, and the optional SQL backend surfaces.
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
- `--backend=sqlite`: dispatches, locks, and definitions tables
|
|
57
|
-
- `--backend=postgres` or `--backend=mysql`: dispatches and definitions tables
|
|
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"
|
|
22
|
+
`kaal init` creates:
|
|
104
23
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
- key: "reports:daily_digest"
|
|
108
|
-
cron: "<%= ENV.fetch('DAILY_DIGEST_CRON', '0 7 * * *') %>"
|
|
109
|
-
job_class: "DailyDigestJob"
|
|
110
|
-
```
|
|
24
|
+
- `config/kaal.rb`
|
|
25
|
+
- `config/scheduler.yml`
|
|
111
26
|
|
|
112
|
-
|
|
113
|
-
Allowed runtime placeholders in `args` and `kwargs` values (not keys): `{{fire_time.iso8601}}`, `{{fire_time.unix}}`, `{{idempotency_key}}`, `{{key}}`.
|
|
27
|
+
Supported backends:
|
|
114
28
|
|
|
115
|
-
|
|
29
|
+
- `memory`
|
|
30
|
+
- `redis`
|
|
116
31
|
|
|
117
|
-
|
|
32
|
+
If you want SQL persistence instead, add the runtime libraries your app uses, such as `sequel`, `activerecord`, `sqlite3`, `pg`, or `mysql2`, then configure one of the explicit `Kaal::Backend::*` SQL backends.
|
|
118
33
|
|
|
119
|
-
##
|
|
34
|
+
## Configuration
|
|
120
35
|
|
|
121
|
-
|
|
36
|
+
Generated `config/kaal.rb` is the primary entrypoint:
|
|
122
37
|
|
|
123
38
|
```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
|
|
39
|
+
require 'kaal'
|
|
40
|
+
|
|
41
|
+
Kaal.configure do |config|
|
|
42
|
+
config.backend = Kaal::Backend::MemoryAdapter.new
|
|
43
|
+
config.tick_interval = 5
|
|
44
|
+
config.window_lookback = 120
|
|
45
|
+
config.lease_ttl = 125
|
|
46
|
+
config.scheduler_config_path = 'config/scheduler.yml'
|
|
47
|
+
end
|
|
166
48
|
```
|
|
167
49
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
## 🧠 Cron Utilities
|
|
50
|
+
Redis path:
|
|
171
51
|
|
|
172
52
|
```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
|
-
```
|
|
53
|
+
require 'redis'
|
|
180
54
|
|
|
181
|
-
|
|
55
|
+
redis = Redis.new(url: ENV.fetch('REDIS_URL'))
|
|
182
56
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
Do not run it inside web server processes by default.
|
|
57
|
+
Kaal.configure do |config|
|
|
58
|
+
config.backend = Kaal::Backend::RedisAdapter.new(redis)
|
|
59
|
+
config.scheduler_config_path = 'config/scheduler.yml'
|
|
60
|
+
end
|
|
61
|
+
```
|
|
189
62
|
|
|
190
|
-
|
|
63
|
+
Time zone behavior is explicit:
|
|
191
64
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
scheduler: bundle exec rails kaal:start
|
|
195
|
-
```
|
|
65
|
+
- use `config.time_zone = 'America/Toronto'` when needed
|
|
66
|
+
- otherwise scheduling runs in `UTC`
|
|
196
67
|
|
|
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
|
-
```
|
|
68
|
+
## Scheduler File
|
|
218
69
|
|
|
219
|
-
|
|
70
|
+
Default scheduler definitions live at `config/scheduler.yml`:
|
|
220
71
|
|
|
221
72
|
```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"]
|
|
73
|
+
defaults:
|
|
74
|
+
jobs:
|
|
75
|
+
- key: "example:heartbeat"
|
|
76
|
+
cron: "*/5 * * * *"
|
|
77
|
+
job_class: "ExampleHeartbeatJob"
|
|
78
|
+
enabled: true
|
|
79
|
+
args:
|
|
80
|
+
- "{{fire_time.iso8601}}"
|
|
81
|
+
kwargs:
|
|
82
|
+
idempotency_key: "{{idempotency_key}}"
|
|
240
83
|
```
|
|
241
84
|
|
|
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
|
|
85
|
+
`job_class` must resolve to a Ruby constant that responds to one of:
|
|
253
86
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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 |
|
|
87
|
+
- `.perform(*args, **kwargs)`
|
|
88
|
+
- `.perform_later(*args, **kwargs)`
|
|
89
|
+
- `.set(queue: ...).perform_later(*args, **kwargs)`
|
|
260
90
|
|
|
261
|
-
|
|
91
|
+
For plain Ruby `.perform` jobs, Kaal treats the dispatch as successful unless the job raises an exception. Return values are ignored.
|
|
262
92
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
## 🧪 Testing Example
|
|
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:
|
|
93
|
+
## CLI
|
|
285
94
|
|
|
286
95
|
```bash
|
|
287
|
-
|
|
96
|
+
bundle exec kaal init --backend=memory
|
|
97
|
+
bundle exec kaal start
|
|
98
|
+
bundle exec kaal status
|
|
99
|
+
bundle exec kaal tick
|
|
100
|
+
bundle exec kaal explain "*/15 * * * *"
|
|
101
|
+
bundle exec kaal next "0 9 * * 1" --count 3
|
|
288
102
|
```
|
|
289
103
|
|
|
290
|
-
|
|
104
|
+
## E2E Verification
|
|
291
105
|
|
|
292
106
|
```bash
|
|
293
107
|
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
108
|
REDIS_URL=redis://127.0.0.1:6379/0 bin/rspec-e2e redis
|
|
298
109
|
```
|
|
299
110
|
|
|
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` |
|
|
111
|
+
## Runtime API
|
|
314
112
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
Labels: `feature`, `build`, `ci`, `lang`
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
|
-
## 📚 Documentation
|
|
113
|
+
```ruby
|
|
114
|
+
Kaal.register(
|
|
115
|
+
key: 'reports:daily',
|
|
116
|
+
cron: '0 9 * * *',
|
|
117
|
+
enqueue: ->(fire_time:, idempotency_key:) {
|
|
118
|
+
ReportsJob.perform(fire_time: fire_time, idempotency_key: idempotency_key)
|
|
119
|
+
}
|
|
120
|
+
)
|
|
329
121
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
- [Usage](https://kaal.codevedas.com/usage)
|
|
333
|
-
- [FAQ / Troubleshooting](https://kaal.codevedas.com/faq)
|
|
122
|
+
Kaal.start!
|
|
123
|
+
```
|
|
334
124
|
|
|
335
|
-
|
|
125
|
+
## SQL Backends
|
|
336
126
|
|
|
337
|
-
|
|
127
|
+
Use the explicit SQL backends when you want persisted registries:
|
|
338
128
|
|
|
339
|
-
|
|
340
|
-
|
|
129
|
+
- `Kaal::Backend::SQLite`
|
|
130
|
+
- `Kaal::Backend::Postgres`
|
|
131
|
+
- `Kaal::Backend::MySQL`
|
|
132
|
+
- `kaal-rails` for Rails-native install and auto-wiring
|
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)
|
|
@@ -0,0 +1,82 @@
|
|
|
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 'fileutils'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
# Active Record migration/install support for SQL-backed Kaal backends.
|
|
11
|
+
module ActiveRecord
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def install_postgres_migration(target_dir:, migration_name: 'Create Kaal Postgres Backend')
|
|
15
|
+
install_migrations(target_dir:, backend: 'postgres', migration_name:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def install_mysql_migration(target_dir:, migration_name: 'Create Kaal MySQL Backend')
|
|
19
|
+
install_migrations(target_dir:, backend: 'mysql', migration_name:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def install_sqlite_migration(target_dir:, migration_name: 'Create Kaal SQLite Backend')
|
|
23
|
+
install_migrations(target_dir:, backend: 'sqlite', migration_name:)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def install_migrations(target_dir:, backend:, migration_name: nil, time_source: -> { Time.now.utc })
|
|
27
|
+
class_name = normalize_migration_name(migration_name, fallback: default_migration_class_for(backend))
|
|
28
|
+
base_path = File.expand_path(target_dir)
|
|
29
|
+
FileUtils.mkdir_p(base_path)
|
|
30
|
+
templates = Kaal::Internal::ActiveRecord::MigrationTemplates.for_backend(backend)
|
|
31
|
+
|
|
32
|
+
templates.map.with_index do |(_name, contents), index|
|
|
33
|
+
suffix = underscore(class_name)
|
|
34
|
+
suffix = "#{suffix}_#{migration_suffixes_for(backend).fetch(index)}" if templates.length > 1
|
|
35
|
+
path = File.expand_path("#{(time_source.call + index).strftime('%Y%m%d%H%M%S')}_#{suffix}.rb", base_path)
|
|
36
|
+
File.write(path, contents)
|
|
37
|
+
path
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def require_activerecord!
|
|
42
|
+
require 'active_record'
|
|
43
|
+
require 'active_support/inflector'
|
|
44
|
+
rescue LoadError => e
|
|
45
|
+
raise LoadError,
|
|
46
|
+
"#{e.message}. Add `gem 'activerecord'` to your Gemfile to use Active Record-backed Kaal SQL support.",
|
|
47
|
+
cause: e
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def normalize_migration_name(name, fallback:)
|
|
51
|
+
normalized = name.to_s.each_char.with_object(+'') do |char, buffer|
|
|
52
|
+
if alphanumeric?(char)
|
|
53
|
+
buffer << char
|
|
54
|
+
elsif !buffer.empty? && !buffer.end_with?(' ')
|
|
55
|
+
buffer << ' '
|
|
56
|
+
end
|
|
57
|
+
end.split.map!(&:capitalize).join
|
|
58
|
+
normalized.empty? ? fallback : normalized
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def underscore(value)
|
|
62
|
+
require_activerecord!
|
|
63
|
+
::ActiveSupport::Inflector.underscore(value)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def default_migration_class_for(backend)
|
|
67
|
+
"CreateKaal#{backend.capitalize}Backend"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def migration_suffixes_for(backend)
|
|
71
|
+
return %w[dispatches locks definitions] if backend.to_s == 'sqlite'
|
|
72
|
+
|
|
73
|
+
%w[dispatches definitions]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def alphanumeric?(char)
|
|
77
|
+
char.between?('a', 'z') ||
|
|
78
|
+
char.between?('A', 'Z') ||
|
|
79
|
+
char.between?('0', '9')
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
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
|