kaal 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +79 -287
- data/Rakefile +4 -2
- data/config/kaal.rb +15 -0
- data/config/scheduler.yml +12 -0
- data/{lib/tasks/kaal_tasks.rake → exe/kaal} +5 -3
- data/lib/kaal/active_record_support.rb +82 -0
- data/lib/kaal/backend/adapter.rb +0 -1
- data/lib/kaal/backend/dispatch_attempt_logger.rb +33 -0
- data/lib/kaal/backend/dispatch_logging.rb +36 -23
- data/lib/kaal/backend/dispatch_registry_accessor.rb +43 -0
- data/lib/kaal/backend/memory_adapter.rb +7 -5
- data/lib/kaal/backend/mysql.rb +41 -0
- data/lib/kaal/backend/postgres.rb +41 -0
- data/lib/kaal/backend/redis_adapter.rb +6 -6
- data/lib/kaal/backend/sqlite.rb +41 -0
- data/lib/kaal/cli.rb +230 -0
- data/lib/kaal/{configuration.rb → config/configuration.rb} +0 -1
- data/lib/kaal/{scheduler_config_error.rb → config/scheduler_config_error.rb} +0 -1
- data/lib/kaal/config/scheduler_time_zone_resolver.rb +50 -0
- data/lib/kaal/config.rb +19 -0
- data/lib/kaal/{coordinator.rb → core/coordinator.rb} +42 -62
- data/lib/kaal/core/enabled_entry_enumerator.rb +51 -0
- data/lib/kaal/core/occurrence_finder.rb +38 -0
- data/lib/kaal/core.rb +18 -0
- data/lib/kaal/definition/database_engine.rb +54 -16
- data/lib/kaal/definition/memory_engine.rb +11 -18
- data/lib/kaal/definition/persistence_helpers.rb +31 -0
- data/lib/kaal/definition/redis_engine.rb +9 -6
- data/lib/kaal/definition/registry.rb +24 -2
- data/lib/kaal/definitions/registration_service.rb +62 -0
- data/lib/kaal/definitions/registry_accessor.rb +33 -0
- data/lib/kaal/dispatch/database_engine.rb +87 -61
- data/lib/kaal/dispatch/memory_engine.rb +3 -4
- data/lib/kaal/dispatch/redis_engine.rb +2 -3
- data/lib/kaal/dispatch/registry.rb +0 -1
- data/lib/kaal/internal/active_record/base_record.rb +16 -0
- data/lib/kaal/internal/active_record/connection_support.rb +96 -0
- data/lib/kaal/internal/active_record/database_backend.rb +73 -0
- data/lib/kaal/internal/active_record/definition_record.rb +16 -0
- data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
- data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
- data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
- data/lib/kaal/internal/active_record/lock_record.rb +16 -0
- data/lib/kaal/internal/active_record/migration_templates.rb +108 -0
- data/lib/kaal/internal/active_record/mysql_backend.rb +71 -0
- data/lib/kaal/internal/active_record/postgres_backend.rb +69 -0
- data/lib/kaal/internal/active_record.rb +17 -0
- data/lib/kaal/internal/sequel/database_backend.rb +74 -0
- data/lib/kaal/internal/sequel/mysql_backend.rb +69 -0
- data/lib/kaal/internal/sequel/postgres_backend.rb +67 -0
- data/lib/kaal/internal/sequel.rb +12 -0
- data/lib/kaal/persistence/database.rb +35 -0
- data/lib/kaal/persistence/migration_templates.rb +97 -0
- data/lib/kaal/register_conflict_support.rb +0 -1
- data/lib/kaal/registry.rb +0 -3
- data/lib/kaal/runtime/runtime_context.rb +41 -0
- data/lib/kaal/runtime/scheduler_boot_loader.rb +52 -0
- data/lib/kaal/runtime/signal_handler_chain.rb +42 -0
- data/lib/kaal/runtime/signal_handler_installer.rb +39 -0
- data/lib/kaal/runtime.rb +20 -0
- data/lib/kaal/scheduler_file/hash_transform.rb +22 -0
- data/lib/kaal/scheduler_file/helper_bundle.rb +28 -0
- data/lib/kaal/scheduler_file/job_applier.rb +242 -0
- data/lib/kaal/scheduler_file/job_normalizer.rb +90 -0
- data/lib/kaal/scheduler_file/loader.rb +152 -0
- data/lib/kaal/scheduler_file/payload_loader.rb +95 -0
- data/lib/kaal/{scheduler_placeholder_support.rb → scheduler_file/placeholder_support.rb} +0 -1
- data/lib/kaal/scheduler_file.rb +18 -0
- data/lib/kaal/sequel_support.rb +82 -0
- data/lib/kaal/support/hash_tools.rb +93 -0
- data/lib/kaal/{cron_humanizer.rb → utils/cron_humanizer.rb} +19 -1
- data/lib/kaal/{cron_utils.rb → utils/cron_utils.rb} +0 -1
- data/lib/kaal/{idempotency_key_generator.rb → utils/idempotency_key_generator.rb} +3 -3
- data/lib/kaal/utils.rb +18 -0
- data/lib/kaal/version.rb +1 -2
- data/lib/kaal.rb +83 -397
- metadata +87 -42
- data/app/models/kaal/cron_definition.rb +0 -76
- data/app/models/kaal/cron_dispatch.rb +0 -50
- data/app/models/kaal/cron_lock.rb +0 -38
- data/lib/generators/kaal/install/install_generator.rb +0 -72
- data/lib/generators/kaal/install/templates/create_kaal_definitions.rb.tt +0 -21
- data/lib/generators/kaal/install/templates/create_kaal_dispatches.rb.tt +0 -20
- data/lib/generators/kaal/install/templates/create_kaal_locks.rb.tt +0 -17
- data/lib/generators/kaal/install/templates/kaal.rb.tt +0 -31
- data/lib/generators/kaal/install/templates/scheduler.yml.tt +0 -22
- data/lib/kaal/backend/mysql_adapter.rb +0 -170
- data/lib/kaal/backend/postgres_adapter.rb +0 -134
- data/lib/kaal/backend/sqlite_adapter.rb +0 -116
- data/lib/kaal/railtie.rb +0 -183
- data/lib/kaal/rake_tasks.rb +0 -184
- data/lib/kaal/scheduler_file_loader.rb +0 -321
- data/lib/kaal/scheduler_hash_transform.rb +0 -45
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
logger
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
434
|
-
@
|
|
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
|
-
|
|
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
|
-
#
|
|
14
|
+
# Sequel-backed definition registry persisted in kaal_definitions.
|
|
13
15
|
class DatabaseEngine < Registry
|
|
14
|
-
def initialize
|
|
15
|
-
super
|
|
16
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
46
|
+
rows = dataset.where(key: key)
|
|
47
|
+
row = rows.first
|
|
48
|
+
return nil unless row
|
|
32
49
|
|
|
33
|
-
|
|
50
|
+
rows.delete
|
|
51
|
+
self.class.normalize_row(row)
|
|
34
52
|
end
|
|
35
53
|
|
|
36
54
|
def find_definition(key)
|
|
37
|
-
|
|
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
|
-
|
|
59
|
+
dataset.order(:key).all.map { |row| self.class.normalize_row(row) }
|
|
43
60
|
end
|
|
44
61
|
|
|
45
62
|
def enabled_definitions
|
|
46
|
-
|
|
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.
|
|
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:
|
|
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)
|
|
43
|
+
@mutex.synchronize { deep_dup(@definitions.delete(key)) }
|
|
51
44
|
end
|
|
52
45
|
|
|
53
46
|
def find_definition(key)
|
|
54
|
-
@mutex.synchronize { @definitions[key]
|
|
47
|
+
@mutex.synchronize { deep_dup(@definitions[key]) }
|
|
55
48
|
end
|
|
56
49
|
|
|
57
50
|
def all_definitions
|
|
58
|
-
@mutex.synchronize { @definitions.values.map(
|
|
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.
|
|
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
|
|
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 =
|
|
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
|