kaal 0.2.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +340 -0
  4. data/Rakefile +6 -0
  5. data/app/models/kaal/cron_definition.rb +71 -0
  6. data/app/models/kaal/cron_dispatch.rb +50 -0
  7. data/app/models/kaal/cron_lock.rb +38 -0
  8. data/config/locales/en.yml +46 -0
  9. data/lib/generators/kaal/install/install_generator.rb +67 -0
  10. data/lib/generators/kaal/install/templates/create_kaal_definitions.rb.tt +21 -0
  11. data/lib/generators/kaal/install/templates/create_kaal_dispatches.rb.tt +20 -0
  12. data/lib/generators/kaal/install/templates/create_kaal_locks.rb.tt +17 -0
  13. data/lib/generators/kaal/install/templates/kaal.rb.tt +31 -0
  14. data/lib/generators/kaal/install/templates/scheduler.yml.tt +22 -0
  15. data/lib/kaal/backend/adapter.rb +147 -0
  16. data/lib/kaal/backend/dispatch_logging.rb +79 -0
  17. data/lib/kaal/backend/memory_adapter.rb +99 -0
  18. data/lib/kaal/backend/mysql_adapter.rb +170 -0
  19. data/lib/kaal/backend/postgres_adapter.rb +134 -0
  20. data/lib/kaal/backend/redis_adapter.rb +145 -0
  21. data/lib/kaal/backend/sqlite_adapter.rb +116 -0
  22. data/lib/kaal/configuration.rb +231 -0
  23. data/lib/kaal/coordinator.rb +437 -0
  24. data/lib/kaal/cron_humanizer.rb +182 -0
  25. data/lib/kaal/cron_utils.rb +233 -0
  26. data/lib/kaal/definition/database_engine.rb +45 -0
  27. data/lib/kaal/definition/memory_engine.rb +61 -0
  28. data/lib/kaal/definition/redis_engine.rb +93 -0
  29. data/lib/kaal/definition/registry.rb +46 -0
  30. data/lib/kaal/dispatch/database_engine.rb +94 -0
  31. data/lib/kaal/dispatch/memory_engine.rb +99 -0
  32. data/lib/kaal/dispatch/redis_engine.rb +103 -0
  33. data/lib/kaal/dispatch/registry.rb +62 -0
  34. data/lib/kaal/idempotency_key_generator.rb +26 -0
  35. data/lib/kaal/railtie.rb +183 -0
  36. data/lib/kaal/rake_tasks.rb +184 -0
  37. data/lib/kaal/register_conflict_support.rb +54 -0
  38. data/lib/kaal/registry.rb +242 -0
  39. data/lib/kaal/scheduler_config_error.rb +6 -0
  40. data/lib/kaal/scheduler_file_loader.rb +316 -0
  41. data/lib/kaal/scheduler_hash_transform.rb +40 -0
  42. data/lib/kaal/scheduler_placeholder_support.rb +80 -0
  43. data/lib/kaal/version.rb +10 -0
  44. data/lib/kaal.rb +571 -0
  45. data/lib/tasks/kaal_tasks.rake +10 -0
  46. metadata +142 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d9c81a663455de1a4bc16743a0ec58df8e42ac5d49ec6fa2bf291f131af6772b
4
+ data.tar.gz: d803aba6a93b004c5b97da505a0c7021e0a50b0243c31bd537c2a5d811d04f55
5
+ SHA512:
6
+ metadata.gz: 5f4142e84f230f1d82b6601162124ee9e846df08322486c613d6a35c049fc4d40054c7d1266c41929d9ac63a873609fb00d6b9ba50a9cf92c9d4e003bc6cc2a1
7
+ data.tar.gz: 0caaa0787fa7fb8e13aeb917221be83b657568fda4a6238df2e523fe0945a9a87a9a163ac638d4040cd9ef277c5a88c3c37612643105c0852d12693d04aff44e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 The kaal Authors
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 ADDED
@@ -0,0 +1,340 @@
1
+ # ⏰ Kaal
2
+
3
+ > Kaal is a distributed cron scheduler for Ruby that safely executes scheduled tasks across multiple nodes.
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)
12
+
13
+ ---
14
+
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:
42
+
43
+ ```ruby
44
+ gem "kaal"
45
+ ```
46
+
47
+ Then run:
48
+
49
+ ```bash
50
+ bundle install
51
+ bin/rails g kaal:install --backend=sqlite
52
+ ```
53
+
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"
104
+
105
+ production:
106
+ jobs:
107
+ - key: "reports:daily_digest"
108
+ cron: "<%= ENV.fetch('DAILY_DIGEST_CRON', '0 7 * * *') %>"
109
+ job_class: "DailyDigestJob"
110
+ ```
111
+
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}}`.
114
+
115
+ 👉 [See full installation guide →](https://kaal.codevedas.com/install)
116
+
117
+ ---
118
+
119
+ ## 🚀 Quick Start
120
+
121
+ Register a scheduled job anywhere during boot (e.g., `config/initializers/kaal_jobs.rb`):
122
+
123
+ ```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
166
+ ```
167
+
168
+ ---
169
+
170
+ ## 🧠 Cron Utilities
171
+
172
+ ```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
+ ```
180
+
181
+ > 🈶 All tokens are i18n-based — override translations under `kaal.*` keys.
182
+
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.
189
+
190
+ **Procfile (process manager):**
191
+
192
+ ```procfile
193
+ web: bundle exec puma -C config/puma.rb
194
+ scheduler: bundle exec rails kaal:start
195
+ ```
196
+
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
+ ```
218
+
219
+ **Kubernetes Deployment:**
220
+
221
+ ```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"]
240
+ ```
241
+
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
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 |
260
+
261
+ 📖 [See FAQ →](https://kaal.codevedas.com/faq)
262
+
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:
285
+
286
+ ```bash
287
+ bin/rspec-unit
288
+ ```
289
+
290
+ Run end-to-end adapter coverage with the same entrypoint used in CI:
291
+
292
+ ```bash
293
+ 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
+ REDIS_URL=redis://127.0.0.1:6379/0 bin/rspec-e2e redis
298
+ ```
299
+
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` |
314
+
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
329
+
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)
334
+
335
+ ---
336
+
337
+ ## 📄 License
338
+
339
+ Released under the [MIT License](LICENSE).
340
+ © 2025 **Codevedas Inc.** — All rights reserved.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
5
+ load 'rails/tasks/engine.rake'
6
+ require 'bundler/gem_tasks'
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaal
4
+ # Persistent scheduler definition model.
5
+ class CronDefinition < ApplicationRecord
6
+ self.table_name = 'kaal_definitions'
7
+
8
+ before_validation :ensure_metadata
9
+
10
+ validates :key, presence: true, uniqueness: true
11
+ validates :cron, presence: true
12
+ validates :source, presence: true
13
+ validates :enabled, inclusion: { in: [true, false] }
14
+
15
+ scope :enabled, -> { where(enabled: true) }
16
+ scope :disabled, -> { where(enabled: false) }
17
+ scope :by_source, ->(source) { where(source: source) }
18
+
19
+ def self.upsert_definition!(key:, cron:, enabled:, source:, metadata:)
20
+ persist_definition(find_or_initialize_by(key: key), cron:, enabled:, source:, metadata:)
21
+ rescue ActiveRecord::RecordNotUnique
22
+ persist_definition(find_by!(key: key), cron:, enabled:, source:, metadata:)
23
+ end
24
+
25
+ def to_definition_hash
26
+ {
27
+ key: key,
28
+ cron: cron,
29
+ enabled: enabled,
30
+ source: source,
31
+ metadata: metadata || {},
32
+ created_at: created_at,
33
+ updated_at: updated_at,
34
+ disabled_at: disabled_at
35
+ }
36
+ end
37
+
38
+ def destroy_and_return_definition_hash
39
+ definition_hash = to_definition_hash
40
+ destroy!
41
+ definition_hash
42
+ end
43
+
44
+ def self.persist_definition(record, cron:, enabled:, source:, metadata:)
45
+ disabled_at = if enabled
46
+ nil
47
+ elsif record.new_record? || record.enabled != false
48
+ Time.current
49
+ else
50
+ record.disabled_at
51
+ end
52
+
53
+ record.assign_attributes(
54
+ cron: cron,
55
+ enabled: enabled,
56
+ source: source,
57
+ metadata: metadata,
58
+ disabled_at:
59
+ )
60
+ record.save!
61
+ record
62
+ end
63
+ private_class_method :persist_definition
64
+
65
+ private
66
+
67
+ def ensure_metadata
68
+ self.metadata ||= {}
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
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
+
8
+ module Kaal
9
+ ##
10
+ # Audit model for tracking cron job dispatch attempts across the cluster.
11
+ #
12
+ # CronDispatch records information about each cron job that was dispatched,
13
+ # including the cron key, scheduled fire time, actual dispatch time,
14
+ # the node that dispatched it, and the dispatch status.
15
+ #
16
+ # This model is used by the PostgreSQL lock adapter for observability and
17
+ # debugging in distributed deployments. You can use this table to:
18
+ # - Verify that jobs are only dispatched once per fire_time
19
+ # - Track which node dispatched each job
20
+ # - Monitor dispatch latency
21
+ # - Audit job execution history
22
+ #
23
+ # @example Query dispatch history
24
+ # Kaal::CronDispatch.where(
25
+ # key: 'send_emails',
26
+ # status: 'dispatched'
27
+ # ).order(fire_time: :desc).limit(10)
28
+ #
29
+ # @example Find all dispatches from a specific node
30
+ # Kaal::CronDispatch.where(node_id: 'worker1').order(dispatched_at: :desc)
31
+ class CronDispatch < ApplicationRecord
32
+ self.table_name = 'kaal_dispatches'
33
+
34
+ ##
35
+ # Validations
36
+ validates :key, presence: true
37
+ validates :fire_time, presence: true
38
+ validates :dispatched_at, presence: true
39
+ validates :node_id, presence: true
40
+ validates :status, presence: true, inclusion: { in: %w[dispatched failed] }
41
+
42
+ ##
43
+ # Scopes for common queries
44
+ scope :recent, ->(limit = 100) { order(dispatched_at: :desc).limit(limit) }
45
+ scope :by_key, ->(key) { where(key: key) }
46
+ scope :by_node, ->(node_id) { where(node_id: node_id) }
47
+ scope :by_status, ->(status) { where(status: status) }
48
+ scope :since, ->(timestamp) { where('dispatched_at >= ?', timestamp) }
49
+ end
50
+ end
@@ -0,0 +1,38 @@
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
+
8
+ module Kaal
9
+ ##
10
+ # Model for distributed lock records (database-backed).
11
+ #
12
+ # Used by the database-backed adapter (SQLiteAdapter) to store lock state in a database table.
13
+ # Works with any ActiveRecord-supported SQL database (SQLite, PostgreSQL, MySQL, etc.).
14
+ # Each row represents a held lock with an expiration time.
15
+ class CronLock < ApplicationRecord
16
+ self.table_name = 'kaal_locks'
17
+
18
+ validates :key, presence: true, uniqueness: true
19
+ validates :acquired_at, presence: true
20
+ validates :expires_at, presence: true
21
+
22
+ ##
23
+ # Find and delete any expired locks (cleanup).
24
+ #
25
+ # @return [Integer] number of locks deleted
26
+ def self.cleanup_expired
27
+ where('expires_at < ?', Time.current).delete_all
28
+ end
29
+
30
+ ##
31
+ # Check if this lock is still valid (not expired).
32
+ #
33
+ # @return [Boolean] true if not expired
34
+ def not_expired?
35
+ expires_at > Time.current
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ en:
2
+ kaal:
3
+ every: "every"
4
+ at: "at"
5
+ and: "and"
6
+ time:
7
+ minute: "minute"
8
+ minutes: "minutes"
9
+ hour: "hour"
10
+ hours: "hours"
11
+ day: "day"
12
+ days: "days"
13
+ week: "week"
14
+ weeks: "weeks"
15
+ month: "month"
16
+ months: "months"
17
+ weekdays:
18
+ "0": "Sunday"
19
+ "1": "Monday"
20
+ "2": "Tuesday"
21
+ "3": "Wednesday"
22
+ "4": "Thursday"
23
+ "5": "Friday"
24
+ "6": "Saturday"
25
+ months:
26
+ "1": "January"
27
+ "2": "February"
28
+ "3": "March"
29
+ "4": "April"
30
+ "5": "May"
31
+ "6": "June"
32
+ "7": "July"
33
+ "8": "August"
34
+ "9": "September"
35
+ "10": "October"
36
+ "11": "November"
37
+ "12": "December"
38
+ phrases:
39
+ daily: "Daily"
40
+ weekly: "Weekly"
41
+ monthly: "Monthly"
42
+ hourly: "Hourly"
43
+ yearly: "Yearly"
44
+ at_time: "At %{time}"
45
+ every_interval: "Every %{count} %{unit}"
46
+ cron_expression: "Cron: %{expression}"
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module Kaal
7
+ module Generators
8
+ # Installs the database migrations needed for the selected backend.
9
+ class InstallGenerator < Rails::Generators::Base
10
+ include Rails::Generators::Migration
11
+
12
+ source_root File.expand_path('templates', __dir__)
13
+
14
+ class_option :backend,
15
+ type: :string,
16
+ default: 'sqlite',
17
+ desc: 'Backend to install migrations for: sqlite, postgres, mysql, redis, memory'
18
+
19
+ def create_initializer
20
+ template 'kaal.rb.tt', 'config/initializers/kaal.rb'
21
+ end
22
+
23
+ def create_scheduler_config
24
+ template 'scheduler.yml.tt', 'config/scheduler.yml'
25
+ end
26
+
27
+ def install_migrations
28
+ templates = migration_templates
29
+ return say_status(:skip, "No database migrations required for #{normalized_backend} backend", :yellow) if templates.empty?
30
+
31
+ templates.each do |template_name|
32
+ migration_template "#{template_name}.rb.tt", "db/migrate/#{template_name}.rb"
33
+ end
34
+ end
35
+
36
+ def self.next_migration_number(dirname)
37
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
38
+ end
39
+
40
+ private
41
+
42
+ def migration_templates
43
+ case normalized_backend
44
+ when 'sqlite', 'database'
45
+ %w[
46
+ create_kaal_dispatches
47
+ create_kaal_locks
48
+ create_kaal_definitions
49
+ ]
50
+ when 'postgres', 'mysql'
51
+ %w[
52
+ create_kaal_dispatches
53
+ create_kaal_definitions
54
+ ]
55
+ when 'memory', 'redis'
56
+ []
57
+ else
58
+ raise Thor::Error, "Unsupported backend '#{options['backend']}'. Use sqlite, postgres, mysql, redis, or memory."
59
+ end
60
+ end
61
+
62
+ def normalized_backend
63
+ options['backend'].to_s.downcase
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates the persisted cron definitions table for database-backed definitions.
4
+ class CreateKaalDefinitions < ActiveRecord::Migration[7.0]
5
+ def change
6
+ create_table :kaal_definitions do |t|
7
+ t.string :key, null: false, limit: 255
8
+ t.string :cron, null: false, limit: 255
9
+ t.boolean :enabled, null: false, default: true
10
+ t.string :source, null: false, limit: 50, default: 'code'
11
+ t.json :metadata, null: false
12
+ t.datetime :disabled_at
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :kaal_definitions, :key, unique: true
18
+ add_index :kaal_definitions, :enabled
19
+ add_index :kaal_definitions, :source
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates the dispatch records table for database-backed dispatch logging.
4
+ class CreateKaalDispatches < ActiveRecord::Migration[7.0]
5
+ def change
6
+ create_table :kaal_dispatches do |t|
7
+ t.string :key, null: false, limit: 255
8
+ t.datetime :fire_time, null: false
9
+ t.datetime :dispatched_at, null: false
10
+ t.string :node_id, null: false, limit: 255
11
+ t.string :status, null: false, limit: 50, default: 'dispatched'
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :kaal_dispatches, %i[key fire_time], unique: true
17
+ add_index :kaal_dispatches, :dispatched_at
18
+ add_index :kaal_dispatches, :status
19
+ end
20
+ end