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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +79 -287
  3. data/Rakefile +4 -2
  4. data/config/kaal.rb +15 -0
  5. data/config/scheduler.yml +12 -0
  6. data/{lib/tasks/kaal_tasks.rake → exe/kaal} +5 -3
  7. data/lib/kaal/active_record_support.rb +82 -0
  8. data/lib/kaal/backend/adapter.rb +0 -1
  9. data/lib/kaal/backend/dispatch_attempt_logger.rb +33 -0
  10. data/lib/kaal/backend/dispatch_logging.rb +36 -23
  11. data/lib/kaal/backend/dispatch_registry_accessor.rb +43 -0
  12. data/lib/kaal/backend/memory_adapter.rb +7 -5
  13. data/lib/kaal/backend/mysql.rb +41 -0
  14. data/lib/kaal/backend/postgres.rb +41 -0
  15. data/lib/kaal/backend/redis_adapter.rb +6 -6
  16. data/lib/kaal/backend/sqlite.rb +41 -0
  17. data/lib/kaal/cli.rb +230 -0
  18. data/lib/kaal/{configuration.rb → config/configuration.rb} +0 -1
  19. data/lib/kaal/{scheduler_config_error.rb → config/scheduler_config_error.rb} +0 -1
  20. data/lib/kaal/config/scheduler_time_zone_resolver.rb +50 -0
  21. data/lib/kaal/config.rb +19 -0
  22. data/lib/kaal/{coordinator.rb → core/coordinator.rb} +42 -62
  23. data/lib/kaal/core/enabled_entry_enumerator.rb +51 -0
  24. data/lib/kaal/core/occurrence_finder.rb +38 -0
  25. data/lib/kaal/core.rb +18 -0
  26. data/lib/kaal/definition/database_engine.rb +54 -16
  27. data/lib/kaal/definition/memory_engine.rb +11 -18
  28. data/lib/kaal/definition/persistence_helpers.rb +31 -0
  29. data/lib/kaal/definition/redis_engine.rb +9 -6
  30. data/lib/kaal/definition/registry.rb +24 -2
  31. data/lib/kaal/definitions/registration_service.rb +62 -0
  32. data/lib/kaal/definitions/registry_accessor.rb +33 -0
  33. data/lib/kaal/dispatch/database_engine.rb +87 -61
  34. data/lib/kaal/dispatch/memory_engine.rb +3 -4
  35. data/lib/kaal/dispatch/redis_engine.rb +2 -3
  36. data/lib/kaal/dispatch/registry.rb +0 -1
  37. data/lib/kaal/internal/active_record/base_record.rb +16 -0
  38. data/lib/kaal/internal/active_record/connection_support.rb +96 -0
  39. data/lib/kaal/internal/active_record/database_backend.rb +73 -0
  40. data/lib/kaal/internal/active_record/definition_record.rb +16 -0
  41. data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
  42. data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
  43. data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
  44. data/lib/kaal/internal/active_record/lock_record.rb +16 -0
  45. data/lib/kaal/internal/active_record/migration_templates.rb +108 -0
  46. data/lib/kaal/internal/active_record/mysql_backend.rb +71 -0
  47. data/lib/kaal/internal/active_record/postgres_backend.rb +69 -0
  48. data/lib/kaal/internal/active_record.rb +17 -0
  49. data/lib/kaal/internal/sequel/database_backend.rb +74 -0
  50. data/lib/kaal/internal/sequel/mysql_backend.rb +69 -0
  51. data/lib/kaal/internal/sequel/postgres_backend.rb +67 -0
  52. data/lib/kaal/internal/sequel.rb +12 -0
  53. data/lib/kaal/persistence/database.rb +35 -0
  54. data/lib/kaal/persistence/migration_templates.rb +97 -0
  55. data/lib/kaal/register_conflict_support.rb +0 -1
  56. data/lib/kaal/registry.rb +0 -3
  57. data/lib/kaal/runtime/runtime_context.rb +41 -0
  58. data/lib/kaal/runtime/scheduler_boot_loader.rb +52 -0
  59. data/lib/kaal/runtime/signal_handler_chain.rb +42 -0
  60. data/lib/kaal/runtime/signal_handler_installer.rb +39 -0
  61. data/lib/kaal/runtime.rb +20 -0
  62. data/lib/kaal/scheduler_file/hash_transform.rb +22 -0
  63. data/lib/kaal/scheduler_file/helper_bundle.rb +28 -0
  64. data/lib/kaal/scheduler_file/job_applier.rb +242 -0
  65. data/lib/kaal/scheduler_file/job_normalizer.rb +90 -0
  66. data/lib/kaal/scheduler_file/loader.rb +152 -0
  67. data/lib/kaal/scheduler_file/payload_loader.rb +95 -0
  68. data/lib/kaal/{scheduler_placeholder_support.rb → scheduler_file/placeholder_support.rb} +0 -1
  69. data/lib/kaal/scheduler_file.rb +18 -0
  70. data/lib/kaal/sequel_support.rb +82 -0
  71. data/lib/kaal/support/hash_tools.rb +93 -0
  72. data/lib/kaal/{cron_humanizer.rb → utils/cron_humanizer.rb} +19 -1
  73. data/lib/kaal/{cron_utils.rb → utils/cron_utils.rb} +0 -1
  74. data/lib/kaal/{idempotency_key_generator.rb → utils/idempotency_key_generator.rb} +3 -3
  75. data/lib/kaal/utils.rb +18 -0
  76. data/lib/kaal/version.rb +1 -2
  77. data/lib/kaal.rb +83 -397
  78. metadata +87 -42
  79. data/app/models/kaal/cron_definition.rb +0 -76
  80. data/app/models/kaal/cron_dispatch.rb +0 -50
  81. data/app/models/kaal/cron_lock.rb +0 -38
  82. data/lib/generators/kaal/install/install_generator.rb +0 -72
  83. data/lib/generators/kaal/install/templates/create_kaal_definitions.rb.tt +0 -21
  84. data/lib/generators/kaal/install/templates/create_kaal_dispatches.rb.tt +0 -20
  85. data/lib/generators/kaal/install/templates/create_kaal_locks.rb.tt +0 -17
  86. data/lib/generators/kaal/install/templates/kaal.rb.tt +0 -31
  87. data/lib/generators/kaal/install/templates/scheduler.yml.tt +0 -22
  88. data/lib/kaal/backend/mysql_adapter.rb +0 -170
  89. data/lib/kaal/backend/postgres_adapter.rb +0 -134
  90. data/lib/kaal/backend/sqlite_adapter.rb +0 -116
  91. data/lib/kaal/railtie.rb +0 -183
  92. data/lib/kaal/rake_tasks.rb +0 -184
  93. data/lib/kaal/scheduler_file_loader.rb +0 -321
  94. data/lib/kaal/scheduler_hash_transform.rb +0 -45
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3171d908e9cb61c59b265540ca3f8fbfece4ac67081b4fab19b45c5b371994e8
4
- data.tar.gz: 19a7540e6594434fe227532fcd4a92435c57cb436a945c7b1f8a05d77e3c94bd
3
+ metadata.gz: f078489fc2106826b98a1890f15dd5aad38a627ef9db230bd11db3785dc66f86
4
+ data.tar.gz: 10ec39fb62e2082d7a8191153af335950fb14d584c0ed33737298baaa2b2f270
5
5
  SHA512:
6
- metadata.gz: 2b28d2b723591834f9425b0ae9044a73e83fc8aa61c9b186a283c18e7282d91d461b8601b59c62e536c2fdc9d27c4a64b518fa438802577556166b8a529e996a
7
- data.tar.gz: 5a1ea96d68f3e8b482b0e9d28639e390e2af5764a8a82138a8a5cd5e7c60c1d8ef7acf0fe808471066f5f3f462dbaab4c3cb96e12fca33d2ba13ffd46cbb797b
6
+ metadata.gz: 5be3591420c49e0149f58e6d76abdb3bfc7a7c10f2249754fb427f89e9ce414bad044d44fc17ed8016b4766182558a2a513598dcc212a0d899872f31007da19b
7
+ data.tar.gz: 60857c850fe9ca808fd198bcd8480ee63962bc1df5de69bab5953168aa7641f1a739c74d970c67b20a7f71d1858abe2a043ac004191b473a94aee7380cf8ce50
data/README.md CHANGED
@@ -1,340 +1,132 @@
1
- # Kaal
1
+ # Kaal
2
2
 
3
- > Kaal is a distributed cron scheduler for Ruby that safely executes scheduled tasks across multiple nodes.
3
+ Distributed cron scheduling for plain Ruby.
4
4
 
5
- [![Gem](https://img.shields.io/gem/v/kaal.svg?style=flat-square)](https://rubygems.org/gems/kaal)
6
- [![CI](https://github.com/Code-Vedas/kaal/actions/workflows/ci.yml/badge.svg)](https://github.com/Code-Vedas/kaal/actions/workflows/ci.yml)
7
- [![Maintainability](https://qlty.sh/gh/Code-Vedas/projects/kaal/maintainability.svg)](https://qlty.sh/gh/Code-Vedas/projects/kaal)
8
- [![Code Coverage](https://qlty.sh/gh/Code-Vedas/projects/kaal/coverage.svg)](https://qlty.sh/gh/Code-Vedas/projects/kaal)
9
- ![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)
10
- ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-12%2B-336791?style=flat-square&logo=postgresql&logoColor=white)
11
- ![Redis](https://img.shields.io/badge/Redis-6%2B-d92b2b?style=flat-square&logo=redis&logoColor=white)
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
- ## Overview
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 "kaal"
12
+ gem 'kaal'
45
13
  ```
46
14
 
47
- Then run:
15
+ Then install and initialize:
48
16
 
49
17
  ```bash
50
18
  bundle install
51
- bin/rails g kaal:install --backend=sqlite
19
+ bundle exec kaal init --backend=memory
52
20
  ```
53
21
 
54
- The install generator creates `config/initializers/kaal.rb` and, for database-backed backends, generates only the migrations you need:
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
- production:
106
- jobs:
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
- `scheduler.yml` supports ERB and environment sections (`defaults`, `development`, `test`, `production`, etc.).
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
- 👉 [See full installation guide →](https://kaal.codevedas.com/install)
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
- ## 🚀 Quick Start
34
+ ## Configuration
120
35
 
121
- Register a scheduled job anywhere during boot (e.g., `config/initializers/kaal_jobs.rb`):
36
+ Generated `config/kaal.rb` is the primary entrypoint:
122
37
 
123
38
  ```ruby
124
- Kaal.register(
125
- key: "reports:weekly_summary",
126
- cron: "0 9 * * 1", # every Monday at 9 AM
127
- enqueue: ->(fire_time:, idempotency_key:) {
128
- WeeklySummaryJob.perform_later(fire_time: fire_time, key: idempotency_key)
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
- Kaal.valid?("0 * * * *") # => true
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
- > 🈶 All tokens are i18n-based — override translations under `kaal.*` keys.
55
+ redis = Redis.new(url: ENV.fetch('REDIS_URL'))
182
56
 
183
- ---
184
-
185
- ## 🧱 Running the Scheduler
186
-
187
- Run the scheduler as a dedicated process in production.
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
- **Procfile (process manager):**
63
+ Time zone behavior is explicit:
191
64
 
192
- ```procfile
193
- web: bundle exec puma -C config/puma.rb
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
- **systemd unit:**
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
- **Kubernetes Deployment:**
70
+ Default scheduler definitions live at `config/scheduler.yml`:
220
71
 
221
72
  ```yaml
222
- apiVersion: apps/v1
223
- kind: Deployment
224
- metadata:
225
- name: kaal-scheduler
226
- spec:
227
- replicas: 1
228
- selector:
229
- matchLabels:
230
- app: kaal-scheduler
231
- template:
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
- For Kubernetes, the scheduler process is the container's main process; if it exits, Kubernetes restarts it.
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
- | 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 |
87
+ - `.perform(*args, **kwargs)`
88
+ - `.perform_later(*args, **kwargs)`
89
+ - `.set(queue: ...).perform_later(*args, **kwargs)`
260
90
 
261
- 📖 [See FAQ →](https://kaal.codevedas.com/faq)
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
- bin/rspec-unit
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
- Run end-to-end adapter coverage with the same entrypoint used in CI:
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
- `pg` and `mysql` require `DATABASE_URL`. `redis` requires `REDIS_URL`. `memory` and `sqlite` use local test defaults.
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
- ## 🤝 Contributing
318
-
319
- 1. Fork the repository
320
- 2. Create a feature branch (`git checkout -b feature/my-feature`)
321
- 3. Run tests (`bundle exec rspec`)
322
- 4. Open a PR using the [Feature Request template](.github/ISSUE_TEMPLATE/feature_request.md)
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
- - [Overview & Motivation](https://kaal.codevedas.com)
331
- - [Installation](https://kaal.codevedas.com/install)
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
- ## 📄 License
127
+ Use the explicit SQL backends when you want persisted registries:
338
128
 
339
- Released under the [MIT License](LICENSE).
340
- © 2025 **Codevedas Inc.** — All rights reserved.
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
@@ -0,0 +1,12 @@
1
+ defaults:
2
+ jobs:
3
+ - key: "example:heartbeat"
4
+ cron: "*/5 * * * *"
5
+ job_class: "ExampleHeartbeatJob"
6
+ enabled: true
7
+ args:
8
+ - "{{fire_time.iso8601}}"
9
+ kwargs:
10
+ idempotency_key: "{{idempotency_key}}"
11
+ metadata:
12
+ owner: "ops"
@@ -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
- require 'kaal/rake_tasks'
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
@@ -4,7 +4,6 @@
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
7
  module Kaal
9
8
  module Backend
10
9
  ##
@@ -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