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,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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
90
|
-
|
|
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.
|
|
21
|
-
# registry.dispatched?('daily_report', Time.
|
|
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.
|
|
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.
|
|
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.
|
|
52
|
+
dispatched_at: Time.now.utc.to_i,
|
|
54
53
|
node_id: node_id,
|
|
55
54
|
status: status
|
|
56
55
|
}
|
|
@@ -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
|