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,90 +4,116 @@
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 'kaal/dispatch/registry'
8
+ require 'kaal/persistence/database'
9
9
 
10
10
  module Kaal
11
11
  module Dispatch
12
- ##
13
- # Database-backed dispatch registry using ActiveRecord.
14
- #
15
- # Stores dispatch records in the database using the CronDispatch model.
16
- # Provides persistent, queryable audit logs across all nodes.
17
- #
18
- # @example Usage
19
- # registry = Kaal::Dispatch::DatabaseEngine.new
20
- # registry.log_dispatch('daily_report', Time.current, 'node-1')
21
- # registry.dispatched?('daily_report', Time.current) # => true
12
+ # Sequel-backed dispatch registry stored in kaal_dispatches.
22
13
  class DatabaseEngine < Registry
23
- ##
24
- # Log a dispatch attempt in the database.
25
- #
26
- # @param key [String] the cron job key
27
- # @param fire_time [Time] when the job was scheduled to fire
28
- # @param node_id [String] identifier for the dispatching node
29
- # @param status [String] dispatch status ('dispatched', 'failed', etc.)
30
- # @return [Kaal::CronDispatch] the created dispatch record
31
- # @raise [ActiveRecord::RecordInvalid] if the record is invalid
14
+ def initialize(database:, namespace: nil)
15
+ super()
16
+ @database = Kaal::Persistence::Database.new(database)
17
+ @namespace = namespace
18
+ end
19
+
32
20
  def log_dispatch(key, fire_time, node_id, status = 'dispatched')
33
- ::Kaal::CronDispatch.create!(
34
- key: key,
21
+ now = Time.now.utc
22
+ storage_key = namespaced_key(key)
23
+ attributes = {
24
+ key: storage_key,
35
25
  fire_time: fire_time,
36
- dispatched_at: Time.current,
26
+ dispatched_at: now,
37
27
  node_id: node_id,
38
28
  status: status
39
- )
29
+ }
30
+ dispatches_dataset = dataset
31
+ update_values = { dispatched_at: now, node_id: node_id, status: status }
32
+ begin
33
+ dispatches_dataset.insert_conflict(
34
+ target: %i[key fire_time],
35
+ update: update_values
36
+ ).insert(attributes)
37
+ rescue NoMethodError => e
38
+ raise unless e.name == :insert_conflict
39
+
40
+ begin
41
+ dispatches_dataset.insert(attributes)
42
+ rescue ::Sequel::UniqueConstraintViolation
43
+ dispatches_dataset.where(key: storage_key, fire_time: fire_time).update(update_values)
44
+ end
45
+ end
46
+
47
+ find_dispatch(key, fire_time)
40
48
  end
41
49
 
42
- ##
43
- # Find a dispatch record for a specific job and fire time.
44
- #
45
- # @param key [String] the cron job key
46
- # @param fire_time [Time] when the job was scheduled to fire
47
- # @return [Kaal::CronDispatch, nil] dispatch record or nil if not found
48
50
  def find_dispatch(key, fire_time)
49
- ::Kaal::CronDispatch.find_by(key: key, fire_time: fire_time)
51
+ self.class.normalize_row(dataset.where(key: namespaced_key(key), fire_time: fire_time).first, namespace: @namespace)
50
52
  end
51
53
 
52
- ##
53
- # Find all dispatch records for a specific job key.
54
- #
55
- # @param key [String] the cron job key
56
- # @return [ActiveRecord::Relation] collection of dispatch records
57
54
  def find_by_key(key)
58
- ::Kaal::CronDispatch.where(key: key).order(fire_time: :desc)
55
+ query(key: namespaced_key(key))
59
56
  end
60
57
 
61
- ##
62
- # Find all dispatch records by node ID.
63
- #
64
- # @param node_id [String] the node identifier
65
- # @return [ActiveRecord::Relation] collection of dispatch records
66
58
  def find_by_node(node_id)
67
- ::Kaal::CronDispatch.where(node_id: node_id).order(fire_time: :desc)
59
+ query(node_id: node_id)
68
60
  end
69
61
 
70
- ##
71
- # Find all dispatch records with a specific status.
72
- #
73
- # @param status [String] the dispatch status
74
- # @return [ActiveRecord::Relation] collection of dispatch records
75
62
  def find_by_status(status)
76
- ::Kaal::CronDispatch.where(status: status).order(fire_time: :desc)
63
+ query(status: status)
77
64
  end
78
65
 
79
- ##
80
- # Delete old dispatch records older than the specified time.
81
- #
82
- # This cleanup prevents unbounded database growth by removing records
83
- # that are older than the recovery window, making them irrelevant for
84
- # future recovery operations.
85
- #
86
- # @param recovery_window [Integer] seconds to keep records for (e.g., 86400 for 24h)
87
- # @return [Integer] number of records deleted
88
66
  def cleanup(recovery_window: 86_400)
89
- cutoff_time = Time.current - recovery_window
90
- ::Kaal::CronDispatch.where('fire_time < ?', cutoff_time).delete_all
67
+ cutoff_time = Time.now.utc - recovery_window
68
+ cleanup_dataset.where { fire_time < cutoff_time }.delete
69
+ end
70
+
71
+ def self.normalize_row(row, namespace: nil)
72
+ return nil unless row
73
+
74
+ {
75
+ key: strip_namespace(row[:key], namespace:),
76
+ fire_time: row[:fire_time],
77
+ dispatched_at: row[:dispatched_at],
78
+ node_id: row[:node_id],
79
+ status: row[:status]
80
+ }
81
+ end
82
+
83
+ def self.strip_namespace(key, namespace:)
84
+ return key if namespace.to_s.empty?
85
+
86
+ prefix = "#{namespace}:"
87
+ key.start_with?(prefix) ? key.delete_prefix(prefix) : key
88
+ end
89
+
90
+ private
91
+
92
+ def dataset
93
+ @database.dispatches_dataset
94
+ end
95
+
96
+ def namespaced_key(key)
97
+ return key if @namespace.to_s.empty?
98
+
99
+ "#{@namespace}:#{key}"
100
+ end
101
+
102
+ def query(filters)
103
+ query_dataset(filters).reverse_order(:fire_time).all.map { |row| self.class.normalize_row(row, namespace: @namespace) }
104
+ end
105
+
106
+ def query_dataset(filters)
107
+ relation = dataset.where(filters)
108
+ return relation if @namespace.to_s.empty? || filters.key?(:key)
109
+
110
+ relation.where(::Sequel.lit('key LIKE ?', "#{@namespace}:%"))
111
+ end
112
+
113
+ def cleanup_dataset
114
+ return dataset if @namespace.to_s.empty?
115
+
116
+ dataset.where(::Sequel.lit('key LIKE ?', "#{@namespace}:%"))
91
117
  end
92
118
  end
93
119
  end
@@ -4,7 +4,6 @@
4
4
  #
5
5
  # This source code is licensed under the MIT license found in the
6
6
  # LICENSE file in the root directory of this source tree.
7
-
8
7
  require_relative 'registry'
9
8
 
10
9
  module Kaal
@@ -17,8 +16,8 @@ module Kaal
17
16
  #
18
17
  # @example Usage
19
18
  # registry = Kaal::Dispatch::MemoryEngine.new
20
- # registry.log_dispatch('daily_report', Time.current, 'node-1')
21
- # registry.dispatched?('daily_report', Time.current) # => true
19
+ # registry.log_dispatch('daily_report', Time.now.utc, 'node-1')
20
+ # registry.dispatched?('daily_report', Time.now.utc) # => true
22
21
  class MemoryEngine < Registry
23
22
  ##
24
23
  # Initialize a new in-memory registry.
@@ -42,7 +41,7 @@ module Kaal
42
41
  @dispatches[storage_key] = {
43
42
  key: key,
44
43
  fire_time: fire_time,
45
- dispatched_at: Time.current,
44
+ dispatched_at: Time.now.utc,
46
45
  node_id: node_id,
47
46
  status: status
48
47
  }
@@ -4,7 +4,6 @@
4
4
  #
5
5
  # This source code is licensed under the MIT license found in the
6
6
  # LICENSE file in the root directory of this source tree.
7
-
8
7
  require 'json'
9
8
  require_relative 'registry'
10
9
 
@@ -19,7 +18,7 @@ module Kaal
19
18
  # @example Usage
20
19
  # redis = Redis.new(url: ENV['REDIS_URL'])
21
20
  # registry = Kaal::Dispatch::RedisEngine.new(redis, namespace: 'myapp')
22
- # registry.log_dispatch('daily_report', Time.current, 'node-1')
21
+ # registry.log_dispatch('daily_report', Time.now.utc, 'node-1')
23
22
  class RedisEngine < Registry
24
23
  # Default TTL for dispatch records (7 days in seconds)
25
24
  DEFAULT_TTL = 7 * 24 * 60 * 60
@@ -50,7 +49,7 @@ module Kaal
50
49
  record = {
51
50
  key: key,
52
51
  fire_time: fire_time.to_i,
53
- dispatched_at: Time.current.to_i,
52
+ dispatched_at: Time.now.utc.to_i,
54
53
  node_id: node_id,
55
54
  status: status
56
55
  }
@@ -4,7 +4,6 @@
4
4
  #
5
5
  # This source code is licensed under the MIT license found in the
6
6
  # LICENSE file in the root directory of this source tree.
7
-
8
7
  module Kaal
9
8
  module Dispatch
10
9
  ##
@@ -0,0 +1,16 @@
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 Internal
9
+ module ActiveRecord
10
+ # Shared abstract Active Record base class for Kaal tables.
11
+ class BaseRecord < ::ActiveRecord::Base
12
+ self.abstract_class = true
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,96 @@
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 Internal
9
+ module ActiveRecord
10
+ # Establishes and reuses the Active Record connection for adapter models.
11
+ module ConnectionSupport
12
+ CONFIGURE_MUTEX = Mutex.new
13
+
14
+ module_function
15
+
16
+ def configure!(connection = nil)
17
+ return BaseRecord unless connection
18
+
19
+ CONFIGURE_MUTEX.synchronize do
20
+ current_config = current_connection_config
21
+ target_config = normalize_connection_config(connection)
22
+ return BaseRecord if configs_match?(current_config, target_config) && connection_active?
23
+
24
+ BaseRecord.establish_connection(connection)
25
+ end
26
+ BaseRecord
27
+ end
28
+
29
+ def normalize_connection_config(connection)
30
+ config = extract_connection_config(connection)
31
+ return connection unless config
32
+
33
+ config.each_with_object({}) do |(key, value), normalized|
34
+ normalized_key = key.to_sym
35
+ normalized[normalized_key] = normalize_connection_value(normalized_key, value)
36
+ end
37
+ end
38
+
39
+ def current_connection_config
40
+ db_config = BaseRecord.connection_db_config
41
+ normalize_connection_config(extract_connection_config(db_config))
42
+ rescue ::ActiveRecord::ConnectionNotEstablished
43
+ nil
44
+ end
45
+
46
+ def extract_connection_config(connection)
47
+ case connection
48
+ when Hash
49
+ connection
50
+ when String
51
+ { url: connection }
52
+ else
53
+ config = connection.configuration_hash
54
+ url = begin
55
+ connection.url
56
+ rescue NoMethodError
57
+ nil
58
+ end
59
+ url ? config.merge(url: url) : config
60
+ end
61
+ rescue NoMethodError
62
+ nil
63
+ end
64
+
65
+ def normalize_connection_value(key, value)
66
+ case key
67
+ when :adapter
68
+ value.to_s.downcase
69
+ when :port
70
+ integer_like?(value) ? value.to_i : value
71
+ else
72
+ value
73
+ end
74
+ end
75
+
76
+ def integer_like?(value)
77
+ value.is_a?(Integer) || value.to_s.match?(/\A\d+\z/)
78
+ end
79
+
80
+ def configs_match?(current_config, target_config)
81
+ return true if current_config == target_config
82
+
83
+ current_url = current_config.is_a?(Hash) ? current_config[:url] : nil
84
+ target_url = target_config.is_a?(Hash) ? target_config[:url] : nil
85
+ !!(current_url && target_url && current_url == target_url)
86
+ end
87
+
88
+ def connection_active?
89
+ BaseRecord.connection.active?
90
+ rescue ::ActiveRecord::ConnectionNotEstablished, StandardError
91
+ false
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,73 @@
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/backend/adapter'
8
+ require 'kaal/backend/dispatch_logging'
9
+
10
+ module Kaal
11
+ module Internal
12
+ module ActiveRecord
13
+ # Table-backed lock engine used for SQLite-style Active Record storage.
14
+ class DatabaseBackend < Kaal::Backend::Adapter
15
+ include Kaal::Backend::DispatchLogging
16
+
17
+ def initialize(connection = nil, lock_model: LockRecord, dispatch_registry: nil, definition_registry: nil, namespace: nil)
18
+ super()
19
+ ConnectionSupport.configure!(connection)
20
+ @lock_model = lock_model
21
+ @dispatch_registry = dispatch_registry
22
+ @definition_registry = definition_registry
23
+ @namespace = namespace
24
+ end
25
+
26
+ def dispatch_registry
27
+ @dispatch_registry ||= DispatchRegistry.new(namespace: resolved_namespace)
28
+ end
29
+
30
+ def definition_registry
31
+ @definition_registry ||= DefinitionRegistry.new
32
+ end
33
+
34
+ def acquire(key, ttl)
35
+ now = Time.now.utc
36
+ expires_at = now + ttl
37
+
38
+ 2.times do |attempt|
39
+ cleanup_expired_locks if attempt.positive?
40
+
41
+ begin
42
+ @lock_model.create!(key: key, acquired_at: now, expires_at: expires_at)
43
+ log_dispatch_attempt(key)
44
+ return true
45
+ rescue ::ActiveRecord::RecordNotUnique
46
+ next
47
+ end
48
+ end
49
+
50
+ false
51
+ rescue StandardError => e
52
+ raise Kaal::Backend::LockAdapterError, "Database acquire failed for #{key}: #{e.message}"
53
+ end
54
+
55
+ def release(key)
56
+ @lock_model.where(key: key).delete_all.positive?
57
+ rescue StandardError => e
58
+ raise Kaal::Backend::LockAdapterError, "Database release failed for #{key}: #{e.message}"
59
+ end
60
+
61
+ def cleanup_expired_locks
62
+ @lock_model.where(expires_at: ...Time.now.utc).delete_all
63
+ end
64
+
65
+ private
66
+
67
+ def resolved_namespace
68
+ @namespace || Kaal.configuration.namespace
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,16 @@
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 Internal
9
+ module ActiveRecord
10
+ # Active Record model for persisted scheduler definitions.
11
+ class DefinitionRecord < BaseRecord
12
+ self.table_name = 'kaal_definitions'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,81 @@
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
+ require 'kaal/definition/registry'
9
+ require 'kaal/definition/persistence_helpers'
10
+
11
+ module Kaal
12
+ module Internal
13
+ module ActiveRecord
14
+ # Active Record-backed registry for scheduler definitions.
15
+ class DefinitionRegistry < Kaal::Definition::Registry
16
+ def initialize(connection: nil, model: DefinitionRecord)
17
+ super()
18
+ ConnectionSupport.configure!(connection)
19
+ @model = model
20
+ end
21
+
22
+ def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
23
+ record = @model.find_or_initialize_by(key: key)
24
+ existing = record.persisted? ? { enabled: record.enabled, disabled_at: record.disabled_at } : nil
25
+ now = Time.now.utc
26
+ record.cron = cron
27
+ record.enabled = enabled
28
+ record.source = source
29
+ record.metadata = JSON.generate(metadata || {})
30
+ record.created_at ||= now
31
+ record.updated_at = now
32
+ record.disabled_at = Kaal::Definition::PersistenceHelpers.disabled_at_for(existing, enabled, now)
33
+ record.save!
34
+ normalize(record)
35
+ end
36
+
37
+ def remove_definition(key)
38
+ record = @model.find_by(key: key)
39
+ return nil unless record
40
+
41
+ normalized = normalize(record)
42
+ record.destroy!
43
+ normalized
44
+ end
45
+
46
+ def find_definition(key)
47
+ normalize(@model.find_by(key: key))
48
+ end
49
+
50
+ def all_definitions
51
+ @model.order(:key).map { |record| normalize(record) }
52
+ end
53
+
54
+ def enabled_definitions
55
+ @model.where(enabled: true).order(:key).map { |record| normalize(record) }
56
+ end
57
+
58
+ private
59
+
60
+ def normalize(record)
61
+ return nil unless record
62
+
63
+ normalize_definition_record(record)
64
+ end
65
+
66
+ def normalize_definition_record(record)
67
+ {
68
+ key: record.key,
69
+ cron: record.cron,
70
+ enabled: record.enabled ? true : false,
71
+ source: record.source,
72
+ metadata: Kaal::Definition::PersistenceHelpers.parse_metadata(record.metadata),
73
+ created_at: record.created_at,
74
+ updated_at: record.updated_at,
75
+ disabled_at: record.disabled_at
76
+ }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,16 @@
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 Internal
9
+ module ActiveRecord
10
+ # Active Record model for persisted dispatch audit entries.
11
+ class DispatchRecord < BaseRecord
12
+ self.table_name = 'kaal_dispatches'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,100 @@
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/dispatch/registry'
8
+
9
+ module Kaal
10
+ module Internal
11
+ module ActiveRecord
12
+ # Active Record-backed registry for dispatch audit records.
13
+ class DispatchRegistry < Kaal::Dispatch::Registry
14
+ def initialize(connection: nil, model: DispatchRecord, namespace: nil)
15
+ super()
16
+ ConnectionSupport.configure!(connection)
17
+ @model = model
18
+ @namespace = namespace
19
+ end
20
+
21
+ def log_dispatch(key, fire_time, node_id, status = 'dispatched')
22
+ record = @model.find_or_initialize_by(key: namespaced_key(key), fire_time: fire_time)
23
+ record.dispatched_at = Time.now.utc
24
+ record.node_id = node_id
25
+ record.status = status
26
+ record.save!
27
+ normalize(record)
28
+ end
29
+
30
+ def find_dispatch(key, fire_time)
31
+ normalize(@model.find_by(key: namespaced_key(key), fire_time: fire_time))
32
+ end
33
+
34
+ def find_by_key(key)
35
+ query(key: namespaced_key(key))
36
+ end
37
+
38
+ def find_by_node(node_id)
39
+ query(node_id: node_id)
40
+ end
41
+
42
+ def find_by_status(status)
43
+ query(status: status)
44
+ end
45
+
46
+ def cleanup(recovery_window: 86_400)
47
+ cutoff_time = Time.now.utc - recovery_window
48
+ cleanup_scope.where(fire_time: ...cutoff_time).delete_all
49
+ end
50
+
51
+ private
52
+
53
+ def query(filters)
54
+ query_scope(filters).order(fire_time: :desc).map { |record| normalize(record) }
55
+ end
56
+
57
+ def namespaced_key(key)
58
+ "#{namespace_prefix}#{key}"
59
+ end
60
+
61
+ def normalize(record)
62
+ return nil unless record
63
+
64
+ {
65
+ key: strip_namespace(record.key),
66
+ fire_time: record.fire_time,
67
+ dispatched_at: record.dispatched_at,
68
+ node_id: record.node_id,
69
+ status: record.status
70
+ }
71
+ end
72
+
73
+ def strip_namespace(key)
74
+ key.delete_prefix(namespace_prefix)
75
+ end
76
+
77
+ def query_scope(filters)
78
+ relation = @model.where(filters)
79
+ return relation if filters.key?(:key)
80
+
81
+ namespace_scope(relation)
82
+ end
83
+
84
+ def cleanup_scope
85
+ namespace_scope(@model)
86
+ end
87
+
88
+ def namespace_scope(relation)
89
+ return relation if namespace_prefix.empty?
90
+
91
+ relation.where('key LIKE ?', "#{namespace_prefix}%")
92
+ end
93
+
94
+ def namespace_prefix
95
+ @namespace.to_s.empty? ? '' : "#{@namespace}:"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,16 @@
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 Internal
9
+ module ActiveRecord
10
+ # Active Record model for table-backed scheduler locks.
11
+ class LockRecord < BaseRecord
12
+ self.table_name = 'kaal_locks'
13
+ end
14
+ end
15
+ end
16
+ end