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
@@ -4,8 +4,10 @@
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
  require 'fugit'
8
+ require_relative 'enabled_entry_enumerator'
9
+ require_relative 'occurrence_finder'
10
+ require 'kaal/config/scheduler_time_zone_resolver'
9
11
 
10
12
  module Kaal
11
13
  ##
@@ -172,13 +174,15 @@ module Kaal
172
174
  each_enabled_entry do |entry|
173
175
  calculate_and_dispatch_due_times(entry)
174
176
  end
177
+ rescue ConfigurationError => e
178
+ log_configuration_error('Kaal coordinator tick failed', e)
179
+ raise
175
180
  rescue StandardError => e
176
- # Log error but continue the loop
177
- @configuration.logger&.error("Kaal coordinator tick failed: #{e.message}")
181
+ log_runtime_error('Kaal coordinator tick failed', e)
178
182
  end
179
183
 
180
184
  def calculate_and_dispatch_due_times(entry)
181
- now = Time.current
185
+ now = Time.now.utc
182
186
  window_start = now - @configuration.window_lookback
183
187
  window_end = now + @configuration.window_lookahead
184
188
 
@@ -197,7 +201,7 @@ module Kaal
197
201
  end
198
202
 
199
203
  def parse_cron(cron_expression)
200
- result = Fugit.parse_cron(cron_expression)
204
+ result = Fugit.parse_cron("#{cron_expression} #{scheduler_time_zone_resolver.time_zone_identifier}")
201
205
  raise ArgumentError, "Invalid cron expression: #{cron_expression}" unless result
202
206
 
203
207
  result
@@ -207,24 +211,7 @@ module Kaal
207
211
  end
208
212
 
209
213
  def find_occurrences(cron, start_time, end_time)
210
- # Use fugit to find all occurrences between start and end times
211
- occurrences = []
212
- current_time = start_time
213
-
214
- while current_time <= end_time
215
- next_occurrence = cron.next_time(current_time)
216
- break unless next_occurrence
217
-
218
- break if next_occurrence > end_time
219
-
220
- occurrences << next_occurrence
221
- current_time = next_occurrence + 1.second # Move past this occurrence to find the next
222
- end
223
-
224
- occurrences
225
- rescue StandardError => e
226
- @configuration.logger&.error("Failed to calculate occurrences: #{e.message}")
227
- []
214
+ occurrence_finder.call(cron:, start_time:, end_time:)
228
215
  end
229
216
 
230
217
  def dispatch_if_due(entry, fire_time, now)
@@ -233,6 +220,7 @@ module Kaal
233
220
 
234
221
  logger = @configuration.logger
235
222
  cron_key = entry.key
223
+ return if Kaal.configuration.enable_log_dispatch_registry && already_dispatched?(cron_key, fire_time)
236
224
 
237
225
  # Generate a unique backend coordination key for this fire time
238
226
  lock_key = generate_lock_key(cron_key, fire_time)
@@ -240,8 +228,8 @@ module Kaal
240
228
  # Try to acquire the coordination lease
241
229
  if acquire_lock(lock_key)
242
230
  dispatch_work(entry, fire_time)
243
- else
244
- logger&.debug("Failed to acquire lock for #{lock_key}")
231
+ elsif logger
232
+ logger.debug("Failed to acquire lock for #{lock_key}")
245
233
  end
246
234
  rescue StandardError => e
247
235
  cron_key ||= 'unknown'
@@ -263,7 +251,7 @@ module Kaal
263
251
  jitter = rand(0..@configuration.recovery_startup_jitter)
264
252
  sleep(jitter) if jitter.positive?
265
253
 
266
- current_time = Time.current
254
+ current_time = Time.now.utc
267
255
  recovery_window = @configuration.recovery_window
268
256
  recovery_start = current_time - recovery_window
269
257
  recovery_end = current_time
@@ -281,8 +269,11 @@ module Kaal
281
269
 
282
270
  # Clean up old dispatch records after recovery completes
283
271
  cleanup_old_dispatch_records(recovery_window)
272
+ rescue ConfigurationError => e
273
+ log_configuration_error('Kaal missed-run recovery failed', e, logger:)
274
+ raise
284
275
  rescue StandardError => e
285
- logger&.error("Error during missed-run recovery: #{e.message}")
276
+ log_runtime_error('Error during missed-run recovery', e, logger:)
286
277
  end
287
278
 
288
279
  ##
@@ -307,12 +298,15 @@ module Kaal
307
298
 
308
299
  # Attempt to dispatch each missed occurrence
309
300
  occurrences.each do |fire_time|
310
- dispatch_if_due(entry, fire_time, Time.current)
301
+ dispatch_if_due(entry, fire_time, Time.now.utc)
311
302
  end
312
303
 
313
304
  occurrences_size
305
+ rescue ConfigurationError => e
306
+ log_configuration_error("Error recovering entry #{entry_key}", e, logger:)
307
+ raise
314
308
  rescue StandardError => e
315
- logger&.error("Error recovering entry #{entry_key}: #{e.message}")
309
+ log_runtime_error("Error recovering entry #{entry_key}", e, logger:)
316
310
  0
317
311
  end
318
312
 
@@ -368,37 +362,7 @@ module Kaal
368
362
  end
369
363
 
370
364
  def each_enabled_entry(&)
371
- use_registry_entries = false
372
- logger = @configuration.logger
373
- warn_iteration_failure = ->(target, error) { logger&.warn("Failed to iterate #{target}: #{error.message}") }
374
-
375
- begin
376
- definition_registry = Kaal.definition_registry
377
- return each_registry_entry(&) unless definition_registry
378
-
379
- definitions = definition_registry.enabled_definitions || []
380
- use_registry_entries = definitions.empty? && definition_registry.all_definitions.to_a.empty?
381
-
382
- definitions
383
- .filter_map { |definition| build_entry_from_definition(definition) }
384
- .each(&)
385
- rescue StandardError => e
386
- warn_iteration_failure.call('enabled definitions', e)
387
- use_registry_entries = true
388
- end
389
-
390
- each_registry_entry(&) if use_registry_entries
391
- end
392
-
393
- def build_entry_from_definition(definition)
394
- key = definition[:key]
395
- callback_entry = @registry.find(key)
396
- unless callback_entry&.enqueue
397
- @configuration.logger&.warn("No enqueue callback registered for definition '#{key}', skipping")
398
- return nil
399
- end
400
-
401
- Registry::Entry.new(key: key, cron: definition[:cron], enqueue: callback_entry.enqueue).freeze
365
+ enabled_entry_enumerator.each(&)
402
366
  end
403
367
 
404
368
  def dispatch_work(entry, fire_time)
@@ -430,8 +394,24 @@ module Kaal
430
394
  @configuration.logger&.error("Sleep interrupted: #{e.message}")
431
395
  end
432
396
 
433
- def each_registry_entry(&)
434
- @registry.each(&)
397
+ def scheduler_time_zone_resolver
398
+ @scheduler_time_zone_resolver ||= SchedulerTimeZoneResolver.new(configuration: @configuration)
399
+ end
400
+
401
+ def occurrence_finder
402
+ @occurrence_finder ||= OccurrenceFinder.new(configuration: @configuration)
403
+ end
404
+
405
+ def enabled_entry_enumerator
406
+ @enabled_entry_enumerator ||= EnabledEntryEnumerator.new(configuration: @configuration, registry: @registry)
407
+ end
408
+
409
+ def log_configuration_error(prefix, error, logger: @configuration.logger)
410
+ logger&.error("#{prefix} due to configuration error: #{error.message}")
411
+ end
412
+
413
+ def log_runtime_error(prefix, error, logger: @configuration.logger)
414
+ logger&.error("#{prefix}: #{error.message}")
435
415
  end
436
416
  end
437
417
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+ module Kaal
8
+ # Enumerates scheduler entries from persisted definitions or the in-memory registry.
9
+ class EnabledEntryEnumerator
10
+ def initialize(configuration:, registry:, definition_registry_provider: -> { Kaal.definition_registry })
11
+ @configuration = configuration
12
+ @registry = registry
13
+ @definition_registry_provider = definition_registry_provider
14
+ end
15
+
16
+ def each(&)
17
+ resolve_entries.each(&)
18
+ rescue StandardError => e
19
+ @configuration.logger&.warn("Failed to iterate enabled definitions: #{e.message}")
20
+ yield_registry_entries(&)
21
+ end
22
+
23
+ private
24
+
25
+ def yield_registry_entries(&)
26
+ @registry.each(&)
27
+ end
28
+
29
+ def resolve_entries
30
+ registry_entries = @registry.to_enum
31
+ definition_registry = @definition_registry_provider.call
32
+ return registry_entries unless definition_registry
33
+
34
+ definitions = definition_registry.enabled_definitions || []
35
+ return registry_entries if definitions.empty? && definition_registry.all_definitions.to_a.empty?
36
+
37
+ definitions.filter_map { |definition| build_entry(definition) }
38
+ end
39
+
40
+ def build_entry(definition)
41
+ key = definition[:key]
42
+ callback_entry = @registry.find(key)
43
+ unless callback_entry&.enqueue
44
+ @configuration.logger&.warn("No enqueue callback registered for definition '#{key}', skipping")
45
+ return nil
46
+ end
47
+
48
+ Registry::Entry.new(key: key, cron: definition[:cron], enqueue: callback_entry.enqueue).freeze
49
+ end
50
+ end
51
+ 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
+ module Kaal
8
+ # Finds all absolute fire times for a parsed cron expression within a window.
9
+ class OccurrenceFinder
10
+ def initialize(configuration:)
11
+ @configuration = configuration
12
+ end
13
+
14
+ def call(cron:, start_time:, end_time:)
15
+ occurrences = []
16
+ current_time = start_time.getutc
17
+ normalized_end_time = end_time.getutc
18
+ normalized_end_time_unix = normalized_end_time.to_f
19
+
20
+ while current_time <= normalized_end_time
21
+ next_occurrence = cron.next_time(current_time)
22
+ break unless next_occurrence
23
+
24
+ next_occurrence_unix = next_occurrence.to_f
25
+ break if next_occurrence_unix > normalized_end_time_unix
26
+
27
+ fire_time = Time.at(next_occurrence_unix).utc
28
+ occurrences << fire_time
29
+ current_time = Time.at(next_occurrence_unix + 1).utc
30
+ end
31
+
32
+ occurrences
33
+ rescue StandardError => e
34
+ @configuration.logger&.error("Failed to calculate occurrences: #{e.message}")
35
+ []
36
+ end
37
+ end
38
+ end
data/lib/kaal/core.rb ADDED
@@ -0,0 +1,18 @@
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/core/coordinator'
8
+ require 'kaal/core/occurrence_finder'
9
+ require 'kaal/core/enabled_entry_enumerator'
10
+
11
+ module Kaal
12
+ # Core scheduling orchestration types.
13
+ module Core
14
+ Coordinator = ::Kaal::Coordinator
15
+ OccurrenceFinder = ::Kaal::OccurrenceFinder
16
+ EnabledEntryEnumerator = ::Kaal::EnabledEntryEnumerator
17
+ end
18
+ end
@@ -4,46 +4,84 @@
4
4
  #
5
5
  # This source code is licensed under the MIT license found in the
6
6
  # LICENSE file in the root directory of this source tree.
7
-
8
- require_relative 'registry'
7
+ require 'json'
8
+ require 'kaal/definition/registry'
9
+ require 'kaal/definition/persistence_helpers'
10
+ require 'kaal/persistence/database'
9
11
 
10
12
  module Kaal
11
13
  module Definition
12
- # ActiveRecord-backed definition registry persisted in kaal_definitions.
14
+ # Sequel-backed definition registry persisted in kaal_definitions.
13
15
  class DatabaseEngine < Registry
14
- def initialize
15
- super
16
- @definition_model = ::Kaal::CronDefinition
16
+ def initialize(database:)
17
+ super()
18
+ @database = Kaal::Persistence::Database.new(database)
17
19
  end
18
20
 
19
21
  def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
20
- @definition_model.upsert_definition!(
22
+ rows = dataset.where(key: key)
23
+ existing = rows.first
24
+ now = Time.now.utc
25
+ payload = {
21
26
  key: key,
22
27
  cron: cron,
23
28
  enabled: enabled,
24
29
  source: source,
25
- metadata: metadata
26
- ).to_definition_hash
30
+ metadata: JSON.generate(metadata || {}),
31
+ created_at: existing ? existing[:created_at] : now,
32
+ updated_at: now,
33
+ disabled_at: PersistenceHelpers.disabled_at_for(existing, enabled, now)
34
+ }
35
+
36
+ if existing
37
+ rows.update(payload)
38
+ else
39
+ dataset.insert(payload)
40
+ end
41
+
42
+ find_definition(key)
27
43
  end
28
44
 
29
45
  def remove_definition(key)
30
- record = @definition_model.find_by(key: key)
31
- return nil unless record
46
+ rows = dataset.where(key: key)
47
+ row = rows.first
48
+ return nil unless row
32
49
 
33
- record.destroy_and_return_definition_hash
50
+ rows.delete
51
+ self.class.normalize_row(row)
34
52
  end
35
53
 
36
54
  def find_definition(key)
37
- record = @definition_model.find_by(key: key)
38
- record&.to_definition_hash
55
+ self.class.normalize_row(dataset.where(key: key).first)
39
56
  end
40
57
 
41
58
  def all_definitions
42
- @definition_model.order(:key).map(&:to_definition_hash)
59
+ dataset.order(:key).all.map { |row| self.class.normalize_row(row) }
43
60
  end
44
61
 
45
62
  def enabled_definitions
46
- @definition_model.where(enabled: true).order(:key).map(&:to_definition_hash)
63
+ dataset.where(enabled: true).order(:key).all.map { |row| self.class.normalize_row(row) }
64
+ end
65
+
66
+ def self.normalize_row(row)
67
+ return nil unless row
68
+
69
+ {
70
+ key: row[:key],
71
+ cron: row[:cron],
72
+ enabled: row[:enabled] ? true : false,
73
+ source: row[:source],
74
+ metadata: PersistenceHelpers.parse_metadata(row[:metadata]),
75
+ created_at: row[:created_at],
76
+ updated_at: row[:updated_at],
77
+ disabled_at: row[:disabled_at]
78
+ }
79
+ end
80
+
81
+ private
82
+
83
+ def dataset
84
+ @database.definitions_dataset
47
85
  end
48
86
  end
49
87
  end
@@ -4,14 +4,16 @@
4
4
  #
5
5
  # This source code is licensed under the MIT license found in the
6
6
  # LICENSE file in the root directory of this source tree.
7
-
8
- require 'active_support/core_ext/object/deep_dup'
9
7
  require_relative 'registry'
8
+ require_relative 'persistence_helpers'
9
+ require 'kaal/support/hash_tools'
10
10
 
11
11
  module Kaal
12
12
  module Definition
13
13
  # In-memory definition registry used when no persistent backend is configured.
14
14
  class MemoryEngine < Registry
15
+ include Kaal::Support::HashTools
16
+
15
17
  def initialize
16
18
  super
17
19
  @definitions = {}
@@ -20,42 +22,33 @@ module Kaal
20
22
 
21
23
  def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
22
24
  @mutex.synchronize do
23
- now = Time.current
25
+ now = Time.now.utc
24
26
  existing = @definitions[key]
25
- stored_metadata = (metadata || {}).deep_dup
26
- disabled_at = if enabled
27
- nil
28
- elsif existing && existing[:enabled] == false
29
- existing[:disabled_at]
30
- else
31
- now
32
- end
33
27
  definition = {
34
28
  key: key,
35
29
  cron: cron,
36
30
  enabled: enabled,
37
31
  source: source,
38
- metadata: stored_metadata,
32
+ metadata: deep_dup(metadata || {}),
39
33
  created_at: existing ? existing[:created_at] : now,
40
34
  updated_at: now,
41
- disabled_at:
35
+ disabled_at: PersistenceHelpers.disabled_at_for(existing, enabled, now)
42
36
  }
43
37
  @definitions[key] = definition
44
-
45
- definition.deep_dup
38
+ deep_dup(definition)
46
39
  end
47
40
  end
48
41
 
49
42
  def remove_definition(key)
50
- @mutex.synchronize { @definitions.delete(key)&.deep_dup }
43
+ @mutex.synchronize { deep_dup(@definitions.delete(key)) }
51
44
  end
52
45
 
53
46
  def find_definition(key)
54
- @mutex.synchronize { @definitions[key]&.deep_dup }
47
+ @mutex.synchronize { deep_dup(@definitions[key]) }
55
48
  end
56
49
 
57
50
  def all_definitions
58
- @mutex.synchronize { @definitions.values.map(&:deep_dup) }
51
+ @mutex.synchronize { @definitions.values.sort_by { |definition| definition[:key] }.map { |definition| deep_dup(definition) } }
59
52
  end
60
53
 
61
54
  def clear
@@ -0,0 +1,31 @@
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 'json'
8
+
9
+ module Kaal
10
+ module Definition
11
+ # Shared pure helpers for persisted definition rows and metadata.
12
+ module PersistenceHelpers
13
+ module_function
14
+
15
+ def disabled_at_for(existing, enabled, now)
16
+ return nil if enabled
17
+ return existing[:disabled_at] if existing && existing[:enabled] == false
18
+
19
+ now
20
+ end
21
+
22
+ def parse_metadata(value)
23
+ return {} if value.to_s.empty?
24
+
25
+ JSON.parse(value, symbolize_names: true)
26
+ rescue JSON::ParserError
27
+ {}
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,15 +4,18 @@
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
  require 'json'
9
8
  require 'time'
10
9
  require_relative 'registry'
10
+ require 'kaal/support/hash_tools'
11
+ require 'kaal/definition/persistence_helpers'
11
12
 
12
13
  module Kaal
13
14
  module Definition
14
15
  # Redis-backed definition registry shared across processes.
15
16
  class RedisEngine < Registry
17
+ include Kaal::Support::HashTools
18
+
16
19
  def initialize(redis, namespace: 'kaal')
17
20
  super()
18
21
  @redis = redis
@@ -20,21 +23,21 @@ module Kaal
20
23
  end
21
24
 
22
25
  def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
23
- now = Time.current
26
+ now = Time.now.utc
24
27
  existing = find_definition(key)
25
28
  payload = {
26
29
  key: key,
27
30
  cron: cron,
28
31
  enabled: enabled,
29
32
  source: source,
30
- metadata: metadata,
33
+ metadata: deep_dup(metadata || {}),
31
34
  created_at: existing ? existing[:created_at] : now,
32
35
  updated_at: now,
33
- disabled_at: enabled ? nil : now
36
+ disabled_at: PersistenceHelpers.disabled_at_for(existing, enabled, now)
34
37
  }
35
38
 
36
39
  @redis.hset(storage_key, key, JSON.generate(self.class.serialize_payload(payload)))
37
- payload
40
+ deep_dup(payload)
38
41
  end
39
42
 
40
43
  def remove_definition(key)
@@ -49,7 +52,7 @@ module Kaal
49
52
  end
50
53
 
51
54
  def all_definitions
52
- @redis.hvals(storage_key).filter_map { |raw| self.class.deserialize_payload(raw) }
55
+ @redis.hvals(storage_key).filter_map { |raw| self.class.deserialize_payload(raw) }.sort_by { |definition| definition[:key] }
53
56
  end
54
57
 
55
58
  private
@@ -4,9 +4,31 @@
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 Definition
9
+ # Pure helpers for extracting persisted definition attributes without ActiveSupport.
10
+ module AttributeHelpers
11
+ module_function
12
+
13
+ def definition_attributes(definition)
14
+ {
15
+ key: definition[:key],
16
+ cron: definition[:cron],
17
+ source: definition[:source],
18
+ metadata: definition[:metadata]
19
+ }
20
+ end
21
+
22
+ def persisted_definition_attributes(definition)
23
+ return {} unless definition
24
+
25
+ {
26
+ enabled: definition[:enabled],
27
+ metadata: definition[:metadata]
28
+ }
29
+ end
30
+ end
31
+
10
32
  # Base abstraction for cron definition storage.
11
33
  class Registry
12
34
  def upsert_definition(**)
@@ -43,7 +65,7 @@ module Kaal
43
65
  definition = find_definition(key)
44
66
  return nil unless definition
45
67
 
46
- attributes = definition.slice(:key, :cron, :source, :metadata).merge(enabled: enabled)
68
+ attributes = AttributeHelpers.definition_attributes(definition).merge(enabled: enabled)
47
69
  upsert_definition(**attributes)
48
70
  end
49
71
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+ module Kaal
8
+ module Definitions
9
+ # Registers code-defined jobs while preserving persisted definition state.
10
+ class RegistrationService
11
+ include RegisterConflictSupport
12
+
13
+ attr_reader :configuration, :definition_registry, :registry
14
+
15
+ def initialize(configuration:, definition_registry:, registry:)
16
+ @configuration = configuration
17
+ @definition_registry = definition_registry
18
+ @registry = registry
19
+ end
20
+
21
+ def call(key:, cron:, enqueue:)
22
+ existing_definition = @definition_registry.find_definition(key)
23
+ existing_entry = @registry.find(key)
24
+ if existing_entry
25
+ conflict_result = resolve_register_conflict(
26
+ key: key,
27
+ cron: cron,
28
+ enqueue: enqueue,
29
+ existing_definition: existing_definition,
30
+ existing_entry: existing_entry
31
+ )
32
+
33
+ return conflict_result if conflict_result
34
+
35
+ raise RegistryError, "Key '#{key}' is already registered"
36
+ end
37
+
38
+ persisted_attributes = {
39
+ enabled: true,
40
+ source: 'code',
41
+ metadata: {}
42
+ }.merge(Definition::AttributeHelpers.persisted_definition_attributes(existing_definition))
43
+ @definition_registry.upsert_definition(key: key, cron: cron, **persisted_attributes)
44
+ with_registered_definition_rollback(key, existing_definition) do
45
+ @registry.add(key: key, cron: cron, enqueue: enqueue)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def rollback_registered_definition(key, existing_definition)
52
+ if existing_definition
53
+ @definition_registry.upsert_definition(
54
+ **Definition::AttributeHelpers.definition_attributes(existing_definition), enabled: existing_definition[:enabled]
55
+ )
56
+ elsif !@registry.registered?(key)
57
+ @definition_registry.remove_definition(key)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -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
+ module Kaal
8
+ module Definitions
9
+ # Resolves the active definition registry with an in-memory fallback.
10
+ class RegistryAccessor
11
+ def initialize(configuration:, fallback_registry_provider:)
12
+ @configuration = configuration
13
+ @fallback_registry_provider = fallback_registry_provider
14
+ end
15
+
16
+ def call
17
+ configured_backend = @configuration.backend
18
+ registry = configured_backend&.definition_registry
19
+ return registry if registry
20
+
21
+ fallback_registry
22
+ rescue NoMethodError
23
+ fallback_registry
24
+ end
25
+
26
+ private
27
+
28
+ def fallback_registry
29
+ @fallback_registry_provider.call
30
+ end
31
+ end
32
+ end
33
+ end