kaal 0.4.0 → 0.6.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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -25
  3. data/config/kaal.yml +12 -0
  4. data/lib/kaal/active_record_support.rb +2 -2
  5. data/lib/kaal/backend/adapter.rb +8 -0
  6. data/lib/kaal/backend/memory_adapter.rb +5 -0
  7. data/lib/kaal/backend/mysql.rb +25 -3
  8. data/lib/kaal/backend/postgres.rb +6 -2
  9. data/lib/kaal/backend/redis_adapter.rb +5 -0
  10. data/lib/kaal/backend/sqlite.rb +4 -0
  11. data/lib/kaal/cli.rb +38 -33
  12. data/lib/kaal/config/backend_factory.rb +178 -0
  13. data/lib/kaal/config/configuration.rb +98 -9
  14. data/lib/kaal/config/delayed_job_security_policy.rb +60 -0
  15. data/lib/kaal/config/file_loader.rb +187 -0
  16. data/lib/kaal/config.rb +3 -0
  17. data/lib/kaal/core/coordinator.rb +68 -19
  18. data/lib/kaal/delayed_job/database_engine.rb +116 -0
  19. data/lib/kaal/delayed_job/dispatch_failure_logger.rb +31 -0
  20. data/lib/kaal/delayed_job/memory_engine.rb +79 -0
  21. data/lib/kaal/delayed_job/mysql_version_support.rb +43 -0
  22. data/lib/kaal/delayed_job/redis_engine.rb +119 -0
  23. data/lib/kaal/delayed_job/registry.rb +39 -0
  24. data/lib/kaal/internal/active_record/database_backend.rb +5 -0
  25. data/lib/kaal/internal/active_record/delayed_job_record.rb +16 -0
  26. data/lib/kaal/internal/active_record/delayed_job_registry.rb +119 -0
  27. data/lib/kaal/internal/active_record/migration_templates.rb +33 -3
  28. data/lib/kaal/internal/active_record/mysql_backend.rb +23 -5
  29. data/lib/kaal/internal/active_record/postgres_backend.rb +4 -0
  30. data/lib/kaal/internal/active_record.rb +2 -0
  31. data/lib/kaal/internal/sequel/database_backend.rb +5 -0
  32. data/lib/kaal/internal/sequel/mysql_backend.rb +15 -1
  33. data/lib/kaal/internal/sequel/postgres_backend.rb +4 -0
  34. data/lib/kaal/internal/sequel.rb +1 -0
  35. data/lib/kaal/job_dispatcher.rb +108 -0
  36. data/lib/kaal/persistence/database.rb +4 -0
  37. data/lib/kaal/persistence/migration_templates.rb +35 -3
  38. data/lib/kaal/runtime/scheduler_boot_loader.rb +3 -1
  39. data/lib/kaal/scheduler_file/job_applier.rb +28 -53
  40. data/lib/kaal/scheduler_file/loader.rb +1 -1
  41. data/lib/kaal/sequel_support.rb +2 -2
  42. data/lib/kaal/version.rb +1 -1
  43. data/lib/kaal.rb +118 -0
  44. data/sig/00_types.rbs +12 -0
  45. data/sig/dependencies.rbs +49 -0
  46. data/sig/kaal/active_record_support.rbs +23 -0
  47. data/sig/kaal/backend/adapter.rbs +26 -0
  48. data/sig/kaal/backend/dispatch_attempt_logger.rbs +17 -0
  49. data/sig/kaal/backend/dispatch_logging.rbs +23 -0
  50. data/sig/kaal/backend/dispatch_registry_accessor.rbs +17 -0
  51. data/sig/kaal/backend/memory_adapter.rbs +33 -0
  52. data/sig/kaal/backend/mysql.rbs +25 -0
  53. data/sig/kaal/backend/postgres.rbs +19 -0
  54. data/sig/kaal/backend/redis_adapter.rbs +41 -0
  55. data/sig/kaal/backend/sqlite.rbs +19 -0
  56. data/sig/kaal/cli.rbs +41 -0
  57. data/sig/kaal/config/backend_factory.rbs +41 -0
  58. data/sig/kaal/config/configuration.rbs +70 -0
  59. data/sig/kaal/config/delayed_job_security_policy.rbs +19 -0
  60. data/sig/kaal/config/file_loader.rbs +35 -0
  61. data/sig/kaal/config/scheduler_config_error.rbs +4 -0
  62. data/sig/kaal/config/scheduler_time_zone_resolver.rbs +19 -0
  63. data/sig/kaal/config.rbs +11 -0
  64. data/sig/kaal/core/coordinator.rbs +103 -0
  65. data/sig/kaal/core/enabled_entry_enumerator.rbs +21 -0
  66. data/sig/kaal/core/occurrence_finder.rbs +9 -0
  67. data/sig/kaal/core.rbs +9 -0
  68. data/sig/kaal/definition/database_engine.rbs +25 -0
  69. data/sig/kaal/definition/memory_engine.rbs +23 -0
  70. data/sig/kaal/definition/persistence_helpers.rbs +9 -0
  71. data/sig/kaal/definition/redis_engine.rbs +33 -0
  72. data/sig/kaal/definition/registry.rbs +29 -0
  73. data/sig/kaal/definitions/registration_service.rbs +27 -0
  74. data/sig/kaal/definitions/registry_accessor.rbs +17 -0
  75. data/sig/kaal/delayed_job/database_engine.rbs +37 -0
  76. data/sig/kaal/delayed_job/dispatch_failure_logger.rbs +7 -0
  77. data/sig/kaal/delayed_job/memory_engine.rbs +29 -0
  78. data/sig/kaal/delayed_job/mysql_version_support.rbs +15 -0
  79. data/sig/kaal/delayed_job/redis_engine.rbs +31 -0
  80. data/sig/kaal/delayed_job/registry.rbs +20 -0
  81. data/sig/kaal/dispatch/database_engine.rbs +39 -0
  82. data/sig/kaal/dispatch/memory_engine.rbs +23 -0
  83. data/sig/kaal/dispatch/redis_engine.rbs +25 -0
  84. data/sig/kaal/dispatch/registry.rbs +11 -0
  85. data/sig/kaal/internal/active_record/base_record.rbs +8 -0
  86. data/sig/kaal/internal/active_record/connection_support.rbs +25 -0
  87. data/sig/kaal/internal/active_record/database_backend.rbs +37 -0
  88. data/sig/kaal/internal/active_record/definition_record.rbs +8 -0
  89. data/sig/kaal/internal/active_record/definition_registry.rbs +27 -0
  90. data/sig/kaal/internal/active_record/delayed_job_record.rbs +8 -0
  91. data/sig/kaal/internal/active_record/delayed_job_registry.rbs +39 -0
  92. data/sig/kaal/internal/active_record/dispatch_record.rbs +8 -0
  93. data/sig/kaal/internal/active_record/dispatch_registry.rbs +43 -0
  94. data/sig/kaal/internal/active_record/lock_record.rbs +8 -0
  95. data/sig/kaal/internal/active_record/migration_templates.rbs +17 -0
  96. data/sig/kaal/internal/active_record/mysql_backend.rbs +45 -0
  97. data/sig/kaal/internal/active_record/postgres_backend.rbs +41 -0
  98. data/sig/kaal/internal/active_record.rbs +0 -0
  99. data/sig/kaal/internal/sequel/database_backend.rbs +39 -0
  100. data/sig/kaal/internal/sequel/mysql_backend.rbs +47 -0
  101. data/sig/kaal/internal/sequel/postgres_backend.rbs +43 -0
  102. data/sig/kaal/internal/sequel.rbs +0 -0
  103. data/sig/kaal/job_dispatcher.rbs +19 -0
  104. data/sig/kaal/persistence/database.rbs +19 -0
  105. data/sig/kaal/persistence/migration_templates.rbs +15 -0
  106. data/sig/kaal/register_conflict_support.rbs +11 -0
  107. data/sig/kaal/registry.rbs +44 -0
  108. data/sig/kaal/runtime/runtime_context.rbs +23 -0
  109. data/sig/kaal/runtime/scheduler_boot_loader.rbs +23 -0
  110. data/sig/kaal/runtime/signal_handler_chain.rbs +19 -0
  111. data/sig/kaal/runtime/signal_handler_installer.rbs +19 -0
  112. data/sig/kaal/runtime.rbs +11 -0
  113. data/sig/kaal/scheduler_file/hash_transform.rbs +9 -0
  114. data/sig/kaal/scheduler_file/helper_bundle.rbs +15 -0
  115. data/sig/kaal/scheduler_file/job_applier.rbs +43 -0
  116. data/sig/kaal/scheduler_file/job_normalizer.rbs +27 -0
  117. data/sig/kaal/scheduler_file/loader.rbs +69 -0
  118. data/sig/kaal/scheduler_file/payload_loader.rbs +33 -0
  119. data/sig/kaal/scheduler_file/placeholder_support.rbs +19 -0
  120. data/sig/kaal/scheduler_file.rbs +9 -0
  121. data/sig/kaal/sequel_support.rbs +25 -0
  122. data/sig/kaal/support/hash_tools.rbs +27 -0
  123. data/sig/kaal/utils/cron_humanizer.rbs +39 -0
  124. data/sig/kaal/utils/cron_utils.rbs +43 -0
  125. data/sig/kaal/utils/idempotency_key_generator.rbs +5 -0
  126. data/sig/kaal/utils.rbs +9 -0
  127. data/sig/kaal/version.rbs +3 -0
  128. data/sig/kaal.rbs +145 -0
  129. metadata +100 -3
  130. data/config/kaal.rb +0 -15
  131. /data/config/{scheduler.yml → kaal-scheduler.yml} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f078489fc2106826b98a1890f15dd5aad38a627ef9db230bd11db3785dc66f86
4
- data.tar.gz: 10ec39fb62e2082d7a8191153af335950fb14d584c0ed33737298baaa2b2f270
3
+ metadata.gz: 0d8c12714ca483db0abe733287ae543ecd206e58c02b228983aa0fb506671542
4
+ data.tar.gz: 3236ad6f7721d80424a3b725542c66f0633a3a21ed42d52514a5294b75013461
5
5
  SHA512:
6
- metadata.gz: 5be3591420c49e0149f58e6d76abdb3bfc7a7c10f2249754fb427f89e9ce414bad044d44fc17ed8016b4766182558a2a513598dcc212a0d899872f31007da19b
7
- data.tar.gz: 60857c850fe9ca808fd198bcd8480ee63962bc1df5de69bab5953168aa7641f1a739c74d970c67b20a7f71d1858abe2a043ac004191b473a94aee7380cf8ce50
6
+ metadata.gz: a7213b5670154c87e4115761d62ee9e5955bd84e520ac3d3dc44dc5118c7840aeb6b6d82ad7f71aa8fe1b7fb32430abd59f636c2d14a0450054af6de7c695105
7
+ data.tar.gz: 790385b5f77017e8001e2c9c3e38484acdab8e733dc5cd15d1bf0a39c6c2074fa9756c7226328446bc69b9044895b0644bdb521725b6992780265d5d0a296fb0
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Distributed cron scheduling for plain Ruby.
4
4
 
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.
5
+ `kaal` is the core engine gem. It owns scheduler/runtime behavior, the registry APIs, the plain Ruby CLI, delayed-job dispatch, and the optional SQL backend surfaces.
6
6
 
7
7
  ## Installation
8
8
 
@@ -21,43 +21,43 @@ bundle exec kaal init --backend=memory
21
21
 
22
22
  `kaal init` creates:
23
23
 
24
- - `config/kaal.rb`
25
- - `config/scheduler.yml`
24
+ - `config/kaal.yml`
25
+ - `config/kaal-scheduler.yml`
26
26
 
27
27
  Supported backends:
28
28
 
29
29
  - `memory`
30
30
  - `redis`
31
31
 
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.
32
+ If you want SQL persistence instead, add the runtime libraries your app uses, such as `sequel`, `activerecord`, `sqlite3`, `pg`, or `mysql2`, then set `backend: sqlite/postgres/mysql` plus `backend_config` in `config/kaal.yml`.
33
33
 
34
34
  ## Configuration
35
35
 
36
- Generated `config/kaal.rb` is the primary entrypoint:
36
+ Generated `config/kaal.yml` is the primary entrypoint:
37
37
 
38
- ```ruby
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
38
+ ```yaml
39
+ defaults:
40
+ backend: memory
41
+ namespace: kaal
42
+ tick_interval: 5
43
+ window_lookback: 120
44
+ window_lookahead: 0
45
+ lease_ttl: 125
46
+ scheduler_config_path: config/kaal-scheduler.yml
47
+ enable_dispatch_recovery: true
48
+ enable_log_dispatch_registry: false
49
+ delayed_job_allowed_class_prefixes: []
50
+ backend_config: {}
48
51
  ```
49
52
 
50
53
  Redis path:
51
54
 
52
- ```ruby
53
- require 'redis'
54
-
55
- redis = Redis.new(url: ENV.fetch('REDIS_URL'))
56
-
57
- Kaal.configure do |config|
58
- config.backend = Kaal::Backend::RedisAdapter.new(redis)
59
- config.scheduler_config_path = 'config/scheduler.yml'
60
- end
55
+ ```yaml
56
+ defaults:
57
+ backend: redis
58
+ scheduler_config_path: config/kaal-scheduler.yml
59
+ backend_config:
60
+ url: redis://127.0.0.1:6379/0
61
61
  ```
62
62
 
63
63
  Time zone behavior is explicit:
@@ -67,7 +67,7 @@ Time zone behavior is explicit:
67
67
 
68
68
  ## Scheduler File
69
69
 
70
- Default scheduler definitions live at `config/scheduler.yml`:
70
+ Default scheduler definitions live at `config/kaal-scheduler.yml`:
71
71
 
72
72
  ```yaml
73
73
  defaults:
@@ -110,6 +110,8 @@ REDIS_URL=redis://127.0.0.1:6379/0 bin/rspec-e2e redis
110
110
 
111
111
  ## Runtime API
112
112
 
113
+ Recurring jobs:
114
+
113
115
  ```ruby
114
116
  Kaal.register(
115
117
  key: 'reports:daily',
@@ -122,6 +124,35 @@ Kaal.register(
122
124
  Kaal.start!
123
125
  ```
124
126
 
127
+ Delayed jobs:
128
+
129
+ ```ruby
130
+ Kaal.enqueue_at(
131
+ at: Time.now.utc + 300,
132
+ job_class: "InvoiceReminderJob",
133
+ args: [123],
134
+ queue: "mailers",
135
+ job_id: "invoice-reminder:123"
136
+ )
137
+ ```
138
+
139
+ Rules shared by the runtime surface:
140
+
141
+ - delayed jobs use `job_id` as their identity and require it to be unique while pending
142
+ - delayed-job `args` are positional only
143
+ - recurring and delayed jobs share the same job-class dispatch rules
144
+ - string job classes are constantized and class or module values are used directly
145
+
146
+ To restrict delayed-job class names:
147
+
148
+ ```ruby
149
+ Kaal.configure do |config|
150
+ config.delayed_job_allowed_class_prefixes = ["Reports::", "Billing::"]
151
+ end
152
+ ```
153
+
154
+ An empty `delayed_job_allowed_class_prefixes` list leaves delayed-job class resolution unrestricted. That is reasonable for local or trusted deployments. On shared Redis or SQL backends in production, set a restrictive prefix list.
155
+
125
156
  ## SQL Backends
126
157
 
127
158
  Use the explicit SQL backends when you want persisted registries:
@@ -130,3 +161,7 @@ Use the explicit SQL backends when you want persisted registries:
130
161
  - `Kaal::Backend::Postgres`
131
162
  - `Kaal::Backend::MySQL`
132
163
  - `kaal-rails` for Rails-native install and auto-wiring
164
+
165
+ For SQL-backed deployments, run the generated migrations so `kaal_delayed_jobs` exists alongside the recurring scheduler tables.
166
+
167
+ Postgres and supported MySQL versions claim due delayed jobs with `SKIP LOCKED`. Older SQL paths still preserve correctness with delete confirmation, and Kaal adds a small pre-claim jitter there to reduce multi-node contention.
data/config/kaal.yml ADDED
@@ -0,0 +1,12 @@
1
+ defaults:
2
+ backend: memory
3
+ namespace: kaal
4
+ tick_interval: 5
5
+ window_lookback: 120
6
+ window_lookahead: 0
7
+ lease_ttl: 125
8
+ scheduler_config_path: config/kaal-scheduler.yml
9
+ enable_dispatch_recovery: true
10
+ enable_log_dispatch_registry: false
11
+ delayed_job_allowed_class_prefixes: []
12
+ backend_config: {}
@@ -68,9 +68,9 @@ module Kaal
68
68
  end
69
69
 
70
70
  def migration_suffixes_for(backend)
71
- return %w[dispatches locks definitions] if backend.to_s == 'sqlite'
71
+ return %w[dispatches locks definitions delayed_jobs] if backend.to_s == 'sqlite'
72
72
 
73
- %w[dispatches definitions]
73
+ %w[dispatches definitions delayed_jobs]
74
74
  end
75
75
 
76
76
  def alphanumeric?(char)
@@ -94,6 +94,14 @@ module Kaal
94
94
  def definition_registry
95
95
  nil
96
96
  end
97
+
98
+ def delayed_store
99
+ nil
100
+ end
101
+
102
+ def disconnect_for_fork
103
+ nil
104
+ end
97
105
  end
98
106
 
99
107
  ##
@@ -6,6 +6,7 @@
6
6
  # LICENSE file in the root directory of this source tree.
7
7
  require_relative 'dispatch_logging'
8
8
  require_relative '../definition/memory_engine'
9
+ require_relative '../delayed_job/memory_engine'
9
10
 
10
11
  module Kaal
11
12
  module Backend
@@ -50,6 +51,10 @@ module Kaal
50
51
  @definition_registry ||= Kaal::Definition::MemoryEngine.new
51
52
  end
52
53
 
54
+ def delayed_store
55
+ @delayed_store ||= Kaal::DelayedJob::MemoryEngine.new
56
+ end
57
+
53
58
  ##
54
59
  # Attempt to acquire a lock in memory.
55
60
  #
@@ -8,16 +8,20 @@ module Kaal
8
8
  module Backend
9
9
  # MySQL-backed backend for either Sequel or Active Record persistence.
10
10
  class MySQL < Adapter
11
- def initialize(database: nil, connection: nil, namespace: nil, **)
11
+ UNSET_SKIP_LOCKED_SUPPORT = Object.new.freeze
12
+
13
+ def initialize(database: nil, connection: nil, namespace: nil,
14
+ use_skip_locked: UNSET_SKIP_LOCKED_SUPPORT)
12
15
  super()
16
+ backend_class = self.class
13
17
  @engine = if database
14
18
  Kaal::Sequel.require_sequel!
15
19
  require 'kaal/internal/sequel'
16
- Kaal::Internal::Sequel::MySQLBackend.new(database, namespace:)
20
+ backend_class.send(:build_sequel_backend, database, namespace, use_skip_locked)
17
21
  else
18
22
  Kaal::ActiveRecord.require_activerecord!
19
23
  require 'kaal/internal/active_record'
20
- Kaal::Internal::ActiveRecord::MySQLBackend.new(connection, namespace:, **)
24
+ backend_class.send(:build_active_record_backend, connection, namespace, use_skip_locked)
21
25
  end
22
26
  end
23
27
 
@@ -29,6 +33,10 @@ module Kaal
29
33
  @engine.definition_registry
30
34
  end
31
35
 
36
+ def delayed_store
37
+ @engine.delayed_store
38
+ end
39
+
32
40
  def acquire(key, ttl)
33
41
  @engine.acquire(key, ttl)
34
42
  end
@@ -36,6 +44,20 @@ module Kaal
36
44
  def release(key)
37
45
  @engine.release(key)
38
46
  end
47
+
48
+ def self.build_sequel_backend(database, namespace, use_skip_locked)
49
+ return Kaal::Internal::Sequel::MySQLBackend.new(database, namespace:) if use_skip_locked.equal?(UNSET_SKIP_LOCKED_SUPPORT)
50
+
51
+ Kaal::Internal::Sequel::MySQLBackend.new(database, namespace:, use_skip_locked:)
52
+ end
53
+ private_class_method :build_sequel_backend
54
+
55
+ def self.build_active_record_backend(connection, namespace, use_skip_locked)
56
+ return Kaal::Internal::ActiveRecord::MySQLBackend.new(connection, namespace:) if use_skip_locked.equal?(UNSET_SKIP_LOCKED_SUPPORT)
57
+
58
+ Kaal::Internal::ActiveRecord::MySQLBackend.new(connection, namespace:, use_skip_locked:)
59
+ end
60
+ private_class_method :build_active_record_backend
39
61
  end
40
62
  end
41
63
  end
@@ -8,7 +8,7 @@ module Kaal
8
8
  module Backend
9
9
  # PostgreSQL-backed backend for either Sequel or Active Record persistence.
10
10
  class Postgres < Adapter
11
- def initialize(database: nil, connection: nil, namespace: nil, **)
11
+ def initialize(database: nil, connection: nil, namespace: nil)
12
12
  super()
13
13
  @engine = if database
14
14
  Kaal::Sequel.require_sequel!
@@ -17,7 +17,7 @@ module Kaal
17
17
  else
18
18
  Kaal::ActiveRecord.require_activerecord!
19
19
  require 'kaal/internal/active_record'
20
- Kaal::Internal::ActiveRecord::PostgresBackend.new(connection, namespace:, **)
20
+ Kaal::Internal::ActiveRecord::PostgresBackend.new(connection, namespace:)
21
21
  end
22
22
  end
23
23
 
@@ -29,6 +29,10 @@ module Kaal
29
29
  @engine.definition_registry
30
30
  end
31
31
 
32
+ def delayed_store
33
+ @engine.delayed_store
34
+ end
35
+
32
36
  def acquire(key, ttl)
33
37
  @engine.acquire(key, ttl)
34
38
  end
@@ -7,6 +7,7 @@
7
7
  require 'securerandom'
8
8
  require_relative 'dispatch_logging'
9
9
  require_relative '../definition/redis_engine'
10
+ require_relative '../delayed_job/redis_engine'
10
11
 
11
12
  module Kaal
12
13
  module Backend
@@ -65,6 +66,10 @@ module Kaal
65
66
  @definition_registry ||= Kaal::Definition::RedisEngine.new(@redis, namespace: @namespace)
66
67
  end
67
68
 
69
+ def delayed_store
70
+ @delayed_store ||= Kaal::DelayedJob::RedisEngine.new(@redis, namespace: @namespace)
71
+ end
72
+
68
73
  ##
69
74
  # Attempt to acquire a distributed lock in Redis.
70
75
  #
@@ -29,6 +29,10 @@ module Kaal
29
29
  @engine.definition_registry
30
30
  end
31
31
 
32
+ def delayed_store
33
+ @engine.delayed_store
34
+ end
35
+
32
36
  def acquire(key, ttl)
33
37
  @engine.acquire(key, ttl)
34
38
  end
data/lib/kaal/cli.rb CHANGED
@@ -19,9 +19,10 @@ module Kaal
19
19
  def load_project!
20
20
  Kaal.reset_configuration!
21
21
  Kaal.reset_registry!
22
- load config_path
23
22
  runtime_context = RuntimeContext.default(root_path: root_path)
24
- Kaal.load_scheduler_file!(runtime_context: runtime_context) if File.exist?(scheduler_path)
23
+ Kaal.load_config_file!(path: config_path, runtime_context:)
24
+ Kaal.warn_on_risky_configuration!
25
+ Kaal.load_scheduler_file!(runtime_context: runtime_context)
25
26
  end
26
27
 
27
28
  def root_path
@@ -32,42 +33,46 @@ module Kaal
32
33
  config = options[:config]
33
34
  return File.expand_path(config) if config
34
35
 
35
- File.join(root_path, 'config', 'kaal.rb')
36
+ File.join(root_path, 'config', 'kaal.yml')
36
37
  end
37
38
 
38
39
  def scheduler_path
39
- File.join(root_path, 'config', 'scheduler.yml')
40
+ File.join(root_path, 'config', 'kaal-scheduler.yml')
40
41
  end
41
42
 
42
43
  def render_config_template(backend)
43
44
  case backend
44
45
  when 'memory'
45
- <<~RUBY
46
- require 'kaal'
47
-
48
- Kaal.configure do |config|
49
- config.backend = Kaal::Backend::MemoryAdapter.new
50
- config.tick_interval = 5
51
- config.window_lookback = 120
52
- config.lease_ttl = 125
53
- config.scheduler_config_path = 'config/scheduler.yml'
54
- end
55
- RUBY
46
+ <<~YAML
47
+ defaults:
48
+ backend: memory
49
+ namespace: kaal
50
+ tick_interval: 5
51
+ window_lookback: 120
52
+ window_lookahead: 0
53
+ lease_ttl: 125
54
+ scheduler_config_path: config/kaal-scheduler.yml
55
+ enable_dispatch_recovery: true
56
+ enable_log_dispatch_registry: false
57
+ delayed_job_allowed_class_prefixes: []
58
+ backend_config: {}
59
+ YAML
56
60
  when 'redis'
57
- <<~RUBY
58
- require 'kaal'
59
- require 'redis'
60
-
61
- redis = Redis.new(url: ENV.fetch('REDIS_URL'))
62
-
63
- Kaal.configure do |config|
64
- config.backend = Kaal::Backend::RedisAdapter.new(redis, namespace: 'kaal')
65
- config.tick_interval = 5
66
- config.window_lookback = 120
67
- config.lease_ttl = 125
68
- config.scheduler_config_path = 'config/scheduler.yml'
69
- end
70
- RUBY
61
+ <<~YAML
62
+ defaults:
63
+ backend: redis
64
+ namespace: kaal
65
+ tick_interval: 5
66
+ window_lookback: 120
67
+ window_lookahead: 0
68
+ lease_ttl: 125
69
+ scheduler_config_path: config/kaal-scheduler.yml
70
+ enable_dispatch_recovery: true
71
+ enable_log_dispatch_registry: false
72
+ delayed_job_allowed_class_prefixes: []
73
+ backend_config:
74
+ url: redis://127.0.0.1:6379/0
75
+ YAML
71
76
  else
72
77
  raise Thor::Error, "Unsupported backend '#{backend}'"
73
78
  end
@@ -94,9 +99,9 @@ module Kaal
94
99
  package_name 'kaal'
95
100
 
96
101
  class_option :root, type: :string, default: Dir.pwd, desc: 'Project root'
97
- class_option :config, type: :string, desc: 'Path to config/kaal.rb'
102
+ class_option :config, type: :string, desc: 'Path to config/kaal.yml'
98
103
 
99
- desc 'init', 'Generate config/kaal.rb and config/scheduler.yml'
104
+ desc 'init', 'Generate config/kaal.yml and config/kaal-scheduler.yml'
100
105
  option :backend, type: :string, default: 'memory', enum: %w[memory redis]
101
106
  def init
102
107
  root = File.expand_path(options[:root])
@@ -104,8 +109,8 @@ module Kaal
104
109
  writer = self.class
105
110
  FileUtils.mkdir_p(File.join(root, 'config'))
106
111
 
107
- writer.write_file(File.join(root, 'config', 'kaal.rb'), render_config_template(backend))
108
- writer.write_file(File.join(root, 'config', 'scheduler.yml'), scheduler_template)
112
+ writer.write_file(File.join(root, 'config', 'kaal.yml'), render_config_template(backend))
113
+ writer.write_file(File.join(root, 'config', 'kaal-scheduler.yml'), scheduler_template)
109
114
 
110
115
  say("Initialized Kaal project for #{backend} backend")
111
116
  end
@@ -0,0 +1,178 @@
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
+ require 'pathname'
9
+ require 'uri'
10
+
11
+ module Kaal
12
+ module Config
13
+ # Builds backend adapter instances from symbolic runtime configuration.
14
+ module BackendFactory
15
+ module_function
16
+
17
+ SUPPORTED_BACKENDS = %w[memory redis sqlite postgres mysql].freeze
18
+
19
+ def normalize_name(name)
20
+ normalized = name.to_s.strip.downcase
21
+ return nil if normalized.empty?
22
+
23
+ normalized = 'postgres' if normalized == 'postgresql'
24
+ normalized = 'mysql' if normalized == 'trilogy'
25
+ return normalized if SUPPORTED_BACKENDS.include?(normalized)
26
+
27
+ raise Kaal::ConfigurationError, "Unsupported backend #{name.inspect}; use memory, redis, sqlite, postgres, or mysql"
28
+ end
29
+
30
+ def build(name, backend_config:, namespace:, runtime_context: nil)
31
+ backend_name = normalize_name(name)
32
+ config = normalize_backend_config(backend_config)
33
+
34
+ case backend_name
35
+ when 'memory'
36
+ Kaal::Backend::MemoryAdapter.new
37
+ when 'redis'
38
+ build_redis_backend(config, namespace)
39
+ when 'sqlite'
40
+ build_sqlite_backend(config, namespace, runtime_context)
41
+ when 'postgres'
42
+ build_postgres_backend(config, namespace)
43
+ when 'mysql'
44
+ build_mysql_backend(config, namespace)
45
+ end
46
+ end
47
+
48
+ def normalize_backend_config(backend_config)
49
+ hash = backend_config.is_a?(Hash) ? backend_config : {}
50
+ Kaal::Support::HashTools.symbolize_keys(Kaal::Support::HashTools.deep_dup(hash))
51
+ end
52
+
53
+ def build_redis_backend(config, namespace)
54
+ require_redis!
55
+
56
+ url = string_value(config[:url])
57
+ raise Kaal::ConfigurationError, 'redis backend requires backend_config.url or KAAL_BACKEND_URL' if url.empty?
58
+
59
+ Kaal::Backend::RedisAdapter.new(::Redis.new(url: url), namespace:)
60
+ end
61
+
62
+ def build_sqlite_backend(config, namespace, runtime_context)
63
+ return Kaal::Backend::SQLite.new(connection: build_sqlite_connection(config[:connection], runtime_context), namespace:) if config.key?(:connection)
64
+
65
+ url = string_value(config[:url])
66
+ database = string_value(config[:database])
67
+ target = url.empty? ? database : url
68
+ raise Kaal::ConfigurationError, 'sqlite backend requires backend_config.url, backend_config.database, or KAAL_BACKEND_URL' if target.empty?
69
+
70
+ require_sequel!
71
+ Kaal::Backend::SQLite.new(
72
+ database: sequel_sqlite_database(target, runtime_context),
73
+ namespace:
74
+ )
75
+ end
76
+
77
+ def build_postgres_backend(config, namespace)
78
+ if config.key?(:connection)
79
+ return Kaal::Backend::Postgres.new(connection: normalize_connection_hash(config[:connection], 'postgresql', nil),
80
+ namespace:)
81
+ end
82
+
83
+ url = string_value(config[:url])
84
+ raise Kaal::ConfigurationError, 'postgres backend requires backend_config.url or KAAL_BACKEND_URL' if url.empty?
85
+
86
+ require_sequel!
87
+ Kaal::Backend::Postgres.new(database: ::Sequel.connect(url), namespace:)
88
+ end
89
+
90
+ def build_mysql_backend(config, namespace)
91
+ use_skip_locked = config[:use_skip_locked]
92
+ skip_locked_configured = config.key?(:use_skip_locked)
93
+
94
+ if config.key?(:connection)
95
+ connection = normalize_connection_hash(config[:connection], 'mysql2', nil)
96
+ return Kaal::Backend::MySQL.new(connection:, namespace:) unless skip_locked_configured
97
+
98
+ return Kaal::Backend::MySQL.new(connection:, namespace:, use_skip_locked:)
99
+ end
100
+
101
+ url = string_value(config[:url])
102
+ raise Kaal::ConfigurationError, 'mysql backend requires backend_config.url or KAAL_BACKEND_URL' if url.empty?
103
+
104
+ require_sequel!
105
+ database = ::Sequel.connect(url)
106
+ return Kaal::Backend::MySQL.new(database:, namespace:) unless skip_locked_configured
107
+
108
+ Kaal::Backend::MySQL.new(database:, namespace:, use_skip_locked:)
109
+ end
110
+
111
+ def build_sqlite_connection(connection, runtime_context)
112
+ normalize_connection_hash(connection, 'sqlite3', runtime_context)
113
+ end
114
+
115
+ def normalize_connection_hash(connection, default_adapter, runtime_context)
116
+ case connection
117
+ when String
118
+ connection
119
+ when Hash
120
+ normalized = Kaal::Support::HashTools.symbolize_keys(Kaal::Support::HashTools.deep_dup(connection))
121
+ adapter = string_value(normalized[:adapter])
122
+ normalized_adapter = adapter.empty? ? default_adapter : adapter
123
+ normalized[:adapter] = normalized_adapter
124
+ normalized[:database] = resolve_sqlite_database_path(normalized[:database], runtime_context) if normalized_adapter == 'sqlite3'
125
+ normalized
126
+ else
127
+ raise Kaal::ConfigurationError, 'backend_config.connection must be a URL string or hash'
128
+ end
129
+ end
130
+
131
+ def resolve_sqlite_database_path(database, runtime_context)
132
+ value = string_value(database)
133
+ return value if value.empty?
134
+ return value if sqlite_uri?(value)
135
+ return ensure_sqlite_directory!(value) if Pathname.new(value).absolute?
136
+
137
+ resolved = runtime_context ? runtime_context.resolve_path(value) : value
138
+ ensure_sqlite_directory!(resolved)
139
+ end
140
+
141
+ def sqlite_uri?(value)
142
+ value.start_with?('sqlite:', 'file:')
143
+ end
144
+
145
+ def sequel_sqlite_database(target, runtime_context)
146
+ return ::Sequel.connect(target) if sqlite_uri?(target)
147
+
148
+ ::Sequel.connect(adapter: 'sqlite', database: resolve_sqlite_database_path(target, runtime_context))
149
+ end
150
+
151
+ def ensure_sqlite_directory!(database_path)
152
+ directory = File.dirname(database_path)
153
+ FileUtils.mkdir_p(directory) unless directory == '.' || directory.empty?
154
+ database_path
155
+ end
156
+
157
+ def adapter_name_for_error(adapter)
158
+ adapter == 'postgresql' ? 'postgres' : 'mysql'
159
+ end
160
+
161
+ def string_value(value)
162
+ value.to_s.strip
163
+ end
164
+
165
+ def require_redis!
166
+ require 'redis'
167
+ rescue LoadError => e
168
+ raise LoadError, "#{e.message}. Add `gem 'redis'` to your Gemfile to use the Redis-backed Kaal adapter.", cause: e
169
+ end
170
+
171
+ def require_sequel!
172
+ require 'sequel'
173
+ rescue LoadError => e
174
+ raise LoadError, "#{e.message}. Add `gem 'sequel'` to your Gemfile to use Sequel-backed Kaal SQL support.", cause: e
175
+ end
176
+ end
177
+ end
178
+ end