kaal 0.2.1 → 0.3.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 +81 -286
- 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/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/redis_adapter.rb +6 -6
- 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/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/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/register_conflict_support.rb +0 -1
- data/lib/kaal/registry.rb +0 -1
- 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/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 +77 -397
- metadata +64 -44
- 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/definition/database_engine.rb +0 -50
- data/lib/kaal/dispatch/database_engine.rb +0 -94
- 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
|
@@ -1,170 +0,0 @@
|
|
|
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
|
-
|
|
8
|
-
require 'socket'
|
|
9
|
-
require 'digest'
|
|
10
|
-
require_relative 'dispatch_logging'
|
|
11
|
-
require_relative '../definition/database_engine'
|
|
12
|
-
|
|
13
|
-
module Kaal
|
|
14
|
-
module Backend
|
|
15
|
-
##
|
|
16
|
-
# Distributed backend adapter using MySQL named locks (GET_LOCK/RELEASE_LOCK).
|
|
17
|
-
#
|
|
18
|
-
# This adapter uses MySQL's GET_LOCK and RELEASE_LOCK functions for
|
|
19
|
-
# distributed locking across multiple nodes. Locks are connection-based
|
|
20
|
-
# and automatically released when the database connection is closed.
|
|
21
|
-
#
|
|
22
|
-
# **IMPORTANT LIMITATIONS:**
|
|
23
|
-
# - Locks are connection-scoped: if a process crashes, the lock persists until
|
|
24
|
-
# the database connection timeout occurs (typically 28,800 seconds or 8 hours).
|
|
25
|
-
# For critical systems, consider monitoring stale locks or using a time-based
|
|
26
|
-
# fallback mechanism.
|
|
27
|
-
# - MySQL named locks have a maximum length of 64 characters. Lock keys longer than
|
|
28
|
-
# 64 characters use a deterministic hash-based shortening scheme (prefix + SHA256
|
|
29
|
-
# digest) to avoid collisions while respecting the limit.
|
|
30
|
-
# - Uses non-blocking acquisition: GET_LOCK is called with timeout=0 for immediate
|
|
31
|
-
# return (does not block waiting for the lock).
|
|
32
|
-
# - Ensure connection pooling is properly configured to release connections
|
|
33
|
-
# promptly when processes terminate.
|
|
34
|
-
#
|
|
35
|
-
# Optionally logs all dispatch attempts to the database when
|
|
36
|
-
# enable_log_dispatch_registry is enabled in configuration.
|
|
37
|
-
#
|
|
38
|
-
# @example Using the MySQL adapter
|
|
39
|
-
# Kaal.configure do |config|
|
|
40
|
-
# config.backend = Kaal::Backend::MySQLAdapter.new
|
|
41
|
-
# config.enable_log_dispatch_registry = true # Enable dispatch logging
|
|
42
|
-
# end
|
|
43
|
-
class MySQLAdapter < Adapter
|
|
44
|
-
include DispatchLogging
|
|
45
|
-
|
|
46
|
-
# MySQL named locks have a maximum length of 64 characters
|
|
47
|
-
MAX_LOCK_NAME_LENGTH = 64
|
|
48
|
-
|
|
49
|
-
def initialize
|
|
50
|
-
super
|
|
51
|
-
@lock_name_length_limit = MAX_LOCK_NAME_LENGTH
|
|
52
|
-
@false_value_pattern = /\A(0|f|false|)\z/i
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
##
|
|
56
|
-
# Initialize a new MySQL adapter.
|
|
57
|
-
|
|
58
|
-
##
|
|
59
|
-
# Get the dispatch registry for database logging.
|
|
60
|
-
#
|
|
61
|
-
# @return [Kaal::Dispatch::DatabaseEngine] database engine instance
|
|
62
|
-
def dispatch_registry
|
|
63
|
-
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
##
|
|
67
|
-
# Get the definition registry for database-backed definition persistence.
|
|
68
|
-
#
|
|
69
|
-
# @return [Kaal::Definition::DatabaseEngine] database definition engine instance
|
|
70
|
-
def definition_registry
|
|
71
|
-
@definition_registry ||= Kaal::Definition::DatabaseEngine.new
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
##
|
|
75
|
-
# Attempt to acquire a distributed lock using MySQL GET_LOCK.
|
|
76
|
-
#
|
|
77
|
-
# Uses MySQL's GET_LOCK(name, timeout) function with a timeout of 0 seconds
|
|
78
|
-
# to perform non-blocking acquisition. If successful, logs the dispatch
|
|
79
|
-
# attempt when enable_log_dispatch_registry is enabled.
|
|
80
|
-
#
|
|
81
|
-
# **Note:** The +ttl+ parameter is ignored. MySQL named locks are connection-based
|
|
82
|
-
# and do not have automatic expiration. The lock will be held until explicitly
|
|
83
|
-
# released or the database connection is closed. See class documentation for
|
|
84
|
-
# limitations.
|
|
85
|
-
#
|
|
86
|
-
# @param key [String] the lock key (format: "namespace:dispatch:cron_key:fire_time")
|
|
87
|
-
# @param ttl [Integer] time-to-live in seconds (ignored; see class docs)
|
|
88
|
-
# @return [Boolean] true if acquired, false if held by another process
|
|
89
|
-
def acquire(key, _ttl)
|
|
90
|
-
lock_name = normalize_lock_name(key)
|
|
91
|
-
|
|
92
|
-
# GET_LOCK returns 1 on success, 0 on timeout, NULL on error
|
|
93
|
-
sql = ActiveRecord::Base.sanitize_sql_array(['SELECT GET_LOCK(?, 0) as lock_result', lock_name])
|
|
94
|
-
result_set = ActiveRecord::Base.connection.execute(sql)
|
|
95
|
-
# Convert result to array and get first row, then first column value
|
|
96
|
-
result_row = result_set.to_a.first
|
|
97
|
-
result_value = result_row.is_a?(Hash) ? result_row['lock_result'] : result_row&.first
|
|
98
|
-
acquired = cast_to_boolean(result_value)
|
|
99
|
-
|
|
100
|
-
log_dispatch_attempt(key) if acquired
|
|
101
|
-
|
|
102
|
-
acquired
|
|
103
|
-
rescue StandardError => e
|
|
104
|
-
raise LockAdapterError, "MySQL acquire failed for #{key}: #{e.message}"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
##
|
|
108
|
-
# Release a distributed lock held by MySQL GET_LOCK.
|
|
109
|
-
#
|
|
110
|
-
# @param key [String] the lock key
|
|
111
|
-
# @return [Boolean] true if released, false if not held
|
|
112
|
-
def release(key)
|
|
113
|
-
lock_name = normalize_lock_name(key)
|
|
114
|
-
|
|
115
|
-
# RELEASE_LOCK returns 1 if held and released, 0 if not held, NULL on error
|
|
116
|
-
sql = ActiveRecord::Base.sanitize_sql_array(['SELECT RELEASE_LOCK(?) as lock_result', lock_name])
|
|
117
|
-
result_set = ActiveRecord::Base.connection.execute(sql)
|
|
118
|
-
# Convert result to array and get first row, then first column value
|
|
119
|
-
result_row = result_set.to_a.first
|
|
120
|
-
result_value = result_row.is_a?(Hash) ? result_row['lock_result'] : result_row&.first
|
|
121
|
-
cast_to_boolean(result_value)
|
|
122
|
-
rescue StandardError => e
|
|
123
|
-
raise LockAdapterError, "MySQL release failed for #{key}: #{e.message}"
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
private
|
|
127
|
-
|
|
128
|
-
def cast_to_boolean(value)
|
|
129
|
-
# MySQL GET_LOCK/RELEASE_LOCK returns 1 (success), 0 (failure), or NULL (error).
|
|
130
|
-
# Cast integer/nil to boolean: 1 => true, 0 or nil => false.
|
|
131
|
-
case value
|
|
132
|
-
when 1
|
|
133
|
-
true
|
|
134
|
-
when 0
|
|
135
|
-
false
|
|
136
|
-
when true, false
|
|
137
|
-
value
|
|
138
|
-
else
|
|
139
|
-
!value.to_s.match?(@false_value_pattern)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
##
|
|
144
|
-
# Normalize lock names to fit MySQL's 64-character limit.
|
|
145
|
-
#
|
|
146
|
-
# For keys exceeding the limit, uses a deterministic hash-based scheme
|
|
147
|
-
# (prefix + SHA256 digest) to avoid collisions.
|
|
148
|
-
#
|
|
149
|
-
# @param key [String] the lock key to normalize
|
|
150
|
-
# @return [String] normalized key (max 64 characters)
|
|
151
|
-
def normalize_lock_name(key)
|
|
152
|
-
return key if key.length <= @lock_name_length_limit
|
|
153
|
-
|
|
154
|
-
# Use SHA256 digest to ensure uniqueness while respecting the 64-char limit.
|
|
155
|
-
# Format: "prefix:hash" where hash is first 16 hex chars (~8 bytes entropy)
|
|
156
|
-
digest = Digest::SHA256.hexdigest(key)
|
|
157
|
-
# Reserve 17 chars for `:` + 16 hex chars, use remainder for prefix
|
|
158
|
-
prefix_length = @lock_name_length_limit - 17
|
|
159
|
-
normalized = "#{key[0...prefix_length]}:#{digest[0...16]}"
|
|
160
|
-
|
|
161
|
-
Kaal.logger&.warn(
|
|
162
|
-
"Lock key '#{key}' exceeds MySQL named lock limit of #{@lock_name_length_limit} characters. " \
|
|
163
|
-
"Using hash-based shortening to avoid collisions: '#{normalized}'."
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
normalized
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
@@ -1,134 +0,0 @@
|
|
|
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
|
-
|
|
8
|
-
require 'digest'
|
|
9
|
-
require 'socket'
|
|
10
|
-
require_relative 'dispatch_logging'
|
|
11
|
-
require_relative '../definition/database_engine'
|
|
12
|
-
|
|
13
|
-
module Kaal
|
|
14
|
-
module Backend
|
|
15
|
-
##
|
|
16
|
-
# Distributed backend adapter using PostgreSQL advisory locks.
|
|
17
|
-
#
|
|
18
|
-
# This adapter uses PostgreSQL's pg_try_advisory_lock function for
|
|
19
|
-
# distributed locking across multiple nodes. Locks are connection-based
|
|
20
|
-
# and automatically released when the database connection is closed.
|
|
21
|
-
#
|
|
22
|
-
# **IMPORTANT LIMITATIONS:**
|
|
23
|
-
# - The +ttl+ parameter is ignored. Locks do not auto-expire based on time;
|
|
24
|
-
# they persist until the database connection terminates.
|
|
25
|
-
# - If a process crashes while holding a lock, the lock will remain held until
|
|
26
|
-
# the connection timeout occurs (typically 30-60 minutes). For critical systems,
|
|
27
|
-
# consider monitoring stale locks or using a time-based fallback mechanism.
|
|
28
|
-
# - Ensure connection pooling is properly configured to release connections
|
|
29
|
-
# promptly when processes terminate.
|
|
30
|
-
#
|
|
31
|
-
# Optionally logs all dispatch attempts to the database when
|
|
32
|
-
# enable_log_dispatch_registry is enabled in configuration.
|
|
33
|
-
#
|
|
34
|
-
# @example Using the PostgreSQL adapter
|
|
35
|
-
# Kaal.configure do |config|
|
|
36
|
-
# config.backend = Kaal::Backend::PostgresAdapter.new
|
|
37
|
-
# config.enable_log_dispatch_registry = true # Enable dispatch logging
|
|
38
|
-
# end
|
|
39
|
-
class PostgresAdapter < Adapter
|
|
40
|
-
include DispatchLogging
|
|
41
|
-
|
|
42
|
-
##
|
|
43
|
-
# Initialize a new PostgreSQL adapter.
|
|
44
|
-
def initialize
|
|
45
|
-
super
|
|
46
|
-
@signed_64_max = 9_223_372_036_854_775_807
|
|
47
|
-
@unsigned_64_range = 18_446_744_073_709_551_616
|
|
48
|
-
@false_value_pattern = /\A(f|false|0|)\z/i
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
##
|
|
52
|
-
# Get the dispatch registry for database logging.
|
|
53
|
-
#
|
|
54
|
-
# @return [Kaal::Dispatch::DatabaseEngine] database engine instance
|
|
55
|
-
def dispatch_registry
|
|
56
|
-
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
##
|
|
60
|
-
# Get the definition registry for database-backed definition persistence.
|
|
61
|
-
#
|
|
62
|
-
# @return [Kaal::Definition::DatabaseEngine] database definition engine instance
|
|
63
|
-
def definition_registry
|
|
64
|
-
@definition_registry ||= Kaal::Definition::DatabaseEngine.new
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
##
|
|
68
|
-
# Attempt to acquire a distributed lock using PostgreSQL advisory lock.
|
|
69
|
-
#
|
|
70
|
-
# Converts the lock key to a deterministic 64-bit integer hash and attempts
|
|
71
|
-
# to acquire the advisory lock. If successful, logs the dispatch attempt
|
|
72
|
-
# when enable_log_dispatch_registry is enabled.
|
|
73
|
-
#
|
|
74
|
-
# **Note:** The +ttl+ parameter is ignored. PostgreSQL advisory locks are
|
|
75
|
-
# connection-based and do not auto-expire. The lock will be held until the
|
|
76
|
-
# database connection is closed. See class documentation for limitations.
|
|
77
|
-
#
|
|
78
|
-
# @param key [String] the lock key (format: "namespace:dispatch:cron_key:fire_time")
|
|
79
|
-
# @param ttl [Integer] time-to-live in seconds (ignored; see class docs)
|
|
80
|
-
# @return [Boolean] true if acquired, false if held by another process
|
|
81
|
-
def acquire(key, _ttl)
|
|
82
|
-
lock_id = calculate_lock_id(key)
|
|
83
|
-
|
|
84
|
-
sql = ActiveRecord::Base.sanitize_sql_array(['SELECT pg_try_advisory_lock(?)', lock_id])
|
|
85
|
-
acquired = cast_to_boolean(ActiveRecord::Base.connection.execute(sql).first['pg_try_advisory_lock'])
|
|
86
|
-
|
|
87
|
-
log_dispatch_attempt(key) if acquired
|
|
88
|
-
|
|
89
|
-
acquired
|
|
90
|
-
rescue StandardError => e
|
|
91
|
-
raise LockAdapterError, "PostgreSQL acquire failed for #{key}: #{e.message}"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
##
|
|
95
|
-
# Release a distributed lock held by PostgreSQL advisory lock.
|
|
96
|
-
#
|
|
97
|
-
# @param key [String] the lock key
|
|
98
|
-
# @return [Boolean] true if released, false if not held
|
|
99
|
-
def release(key)
|
|
100
|
-
lock_id = calculate_lock_id(key)
|
|
101
|
-
|
|
102
|
-
sql = ActiveRecord::Base.sanitize_sql_array(['SELECT pg_advisory_unlock(?)', lock_id])
|
|
103
|
-
cast_to_boolean(ActiveRecord::Base.connection.execute(sql).first['pg_advisory_unlock'])
|
|
104
|
-
rescue StandardError => e
|
|
105
|
-
raise LockAdapterError, "PostgreSQL release failed for #{key}: #{e.message}"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
private
|
|
109
|
-
|
|
110
|
-
def cast_to_boolean(value)
|
|
111
|
-
# PostgreSQL's `.execute` returns "t"/"f" strings for boolean columns,
|
|
112
|
-
# not Ruby true/false. Explicitly cast to boolean for proper semantics.
|
|
113
|
-
case value
|
|
114
|
-
when 't'
|
|
115
|
-
true
|
|
116
|
-
when 'f'
|
|
117
|
-
false
|
|
118
|
-
when true, false
|
|
119
|
-
value
|
|
120
|
-
else
|
|
121
|
-
!value.to_s.match?(@false_value_pattern)
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def calculate_lock_id(key)
|
|
126
|
-
# Use MD5 hash of the key and convert to 64-bit signed integer
|
|
127
|
-
# Ensure it's in the range of a signed 64-bit integer
|
|
128
|
-
hash = Digest::MD5.digest(key).unpack1('Q>')
|
|
129
|
-
# Convert to signed 64-bit integer
|
|
130
|
-
hash > @signed_64_max ? hash - @unsigned_64_range : hash
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
|
|
8
|
-
require_relative 'dispatch_logging'
|
|
9
|
-
require_relative '../definition/database_engine'
|
|
10
|
-
|
|
11
|
-
module Kaal
|
|
12
|
-
module Backend
|
|
13
|
-
##
|
|
14
|
-
# Distributed backend adapter using any ActiveRecord-backed SQL database.
|
|
15
|
-
#
|
|
16
|
-
# Despite the "SQLiteAdapter" name, this adapter works with any SQL database
|
|
17
|
-
# supported by Rails (SQLite, PostgreSQL, MySQL, etc.) via ActiveRecord. It stores
|
|
18
|
-
# locks in a database table with TTL-based expiration and uses a UNIQUE constraint
|
|
19
|
-
# on the key column to ensure atomicity.
|
|
20
|
-
#
|
|
21
|
-
# Suitable for single-server or development environments. For production
|
|
22
|
-
# multi-node deployments, use Redis or PostgreSQL adapters instead.
|
|
23
|
-
#
|
|
24
|
-
# @example Using the adapter with any SQL database
|
|
25
|
-
# Kaal.configure do |config|
|
|
26
|
-
# config.backend = Kaal::Backend::SQLiteAdapter.new
|
|
27
|
-
# config.enable_log_dispatch_registry = true # Enable dispatch logging
|
|
28
|
-
# end
|
|
29
|
-
class SQLiteAdapter < Adapter
|
|
30
|
-
include DispatchLogging
|
|
31
|
-
|
|
32
|
-
##
|
|
33
|
-
# Initialize a new database-backed adapter.
|
|
34
|
-
#
|
|
35
|
-
# @return [SQLiteAdapter] a new instance
|
|
36
|
-
# (Note: Despite the class name, this works with any ActiveRecord SQL database)
|
|
37
|
-
|
|
38
|
-
##
|
|
39
|
-
# Get the dispatch registry for database logging.
|
|
40
|
-
#
|
|
41
|
-
# @return [Kaal::Dispatch::DatabaseEngine] database engine instance
|
|
42
|
-
def dispatch_registry
|
|
43
|
-
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
##
|
|
47
|
-
# Get the definition registry for database-backed definition persistence.
|
|
48
|
-
#
|
|
49
|
-
# @return [Kaal::Definition::DatabaseEngine] database definition engine instance
|
|
50
|
-
def definition_registry
|
|
51
|
-
@definition_registry ||= Kaal::Definition::DatabaseEngine.new
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
##
|
|
55
|
-
# Attempt to acquire a distributed lock in the database.
|
|
56
|
-
#
|
|
57
|
-
# Attempts to insert a new lock record. If the key already exists, cleans up
|
|
58
|
-
# any expired locks and retries once. This avoids unnecessary cleanup in the
|
|
59
|
-
# common case and reduces the window for race conditions.
|
|
60
|
-
#
|
|
61
|
-
# @param key [String] the lock key
|
|
62
|
-
# @param ttl [Integer] time-to-live in seconds
|
|
63
|
-
# @return [Boolean] true if acquired, false if held by another process
|
|
64
|
-
def acquire(key, ttl)
|
|
65
|
-
now = Time.current
|
|
66
|
-
expires_at = now + ttl.seconds
|
|
67
|
-
acquired = false
|
|
68
|
-
attempt_cleanup = false
|
|
69
|
-
|
|
70
|
-
2.times do
|
|
71
|
-
Kaal::CronLock.cleanup_expired if attempt_cleanup
|
|
72
|
-
begin
|
|
73
|
-
Kaal::CronLock.create!(
|
|
74
|
-
key: key,
|
|
75
|
-
acquired_at: now,
|
|
76
|
-
expires_at: expires_at
|
|
77
|
-
)
|
|
78
|
-
acquired = true
|
|
79
|
-
break
|
|
80
|
-
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
|
81
|
-
attempt_cleanup = true
|
|
82
|
-
rescue ActiveRecord::StatementInvalid => e
|
|
83
|
-
raise unless wrapped_contention_error?(e)
|
|
84
|
-
|
|
85
|
-
attempt_cleanup = true
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
log_dispatch_attempt(key) if acquired
|
|
90
|
-
|
|
91
|
-
acquired
|
|
92
|
-
rescue StandardError => e
|
|
93
|
-
raise LockAdapterError, "SQLite acquire failed for #{key}: #{e.message}"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
##
|
|
97
|
-
# Release a previously acquired lock.
|
|
98
|
-
#
|
|
99
|
-
# @param key [String] the lock key to release
|
|
100
|
-
# @return [Boolean] true if released (key existed and was deleted), false if not held
|
|
101
|
-
def release(key)
|
|
102
|
-
deleted = Kaal::CronLock.where(key: key).delete_all
|
|
103
|
-
deleted.positive?
|
|
104
|
-
rescue StandardError => e
|
|
105
|
-
raise LockAdapterError, "SQLite release failed for #{key}: #{e.message}"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
private
|
|
109
|
-
|
|
110
|
-
def wrapped_contention_error?(error)
|
|
111
|
-
cause = error.cause
|
|
112
|
-
cause.is_a?(ActiveRecord::RecordNotUnique) || error.message.match?(/unique|duplicate/i)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
|
|
8
|
-
require_relative 'registry'
|
|
9
|
-
|
|
10
|
-
module Kaal
|
|
11
|
-
module Definition
|
|
12
|
-
# ActiveRecord-backed definition registry persisted in kaal_definitions.
|
|
13
|
-
class DatabaseEngine < Registry
|
|
14
|
-
def initialize
|
|
15
|
-
super
|
|
16
|
-
@definition_model = ::Kaal::CronDefinition
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
|
|
20
|
-
@definition_model.upsert_definition!(
|
|
21
|
-
key: key,
|
|
22
|
-
cron: cron,
|
|
23
|
-
enabled: enabled,
|
|
24
|
-
source: source,
|
|
25
|
-
metadata: metadata
|
|
26
|
-
).to_definition_hash
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def remove_definition(key)
|
|
30
|
-
record = @definition_model.find_by(key: key)
|
|
31
|
-
return nil unless record
|
|
32
|
-
|
|
33
|
-
record.destroy_and_return_definition_hash
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def find_definition(key)
|
|
37
|
-
record = @definition_model.find_by(key: key)
|
|
38
|
-
record&.to_definition_hash
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def all_definitions
|
|
42
|
-
@definition_model.order(:key).map(&:to_definition_hash)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def enabled_definitions
|
|
46
|
-
@definition_model.where(enabled: true).order(:key).map(&:to_definition_hash)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,94 +0,0 @@
|
|
|
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
|
-
|
|
8
|
-
require_relative 'registry'
|
|
9
|
-
|
|
10
|
-
module Kaal
|
|
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
|
|
22
|
-
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
|
|
32
|
-
def log_dispatch(key, fire_time, node_id, status = 'dispatched')
|
|
33
|
-
::Kaal::CronDispatch.create!(
|
|
34
|
-
key: key,
|
|
35
|
-
fire_time: fire_time,
|
|
36
|
-
dispatched_at: Time.current,
|
|
37
|
-
node_id: node_id,
|
|
38
|
-
status: status
|
|
39
|
-
)
|
|
40
|
-
end
|
|
41
|
-
|
|
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
|
-
def find_dispatch(key, fire_time)
|
|
49
|
-
::Kaal::CronDispatch.find_by(key: key, fire_time: fire_time)
|
|
50
|
-
end
|
|
51
|
-
|
|
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
|
-
def find_by_key(key)
|
|
58
|
-
::Kaal::CronDispatch.where(key: key).order(fire_time: :desc)
|
|
59
|
-
end
|
|
60
|
-
|
|
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
|
-
def find_by_node(node_id)
|
|
67
|
-
::Kaal::CronDispatch.where(node_id: node_id).order(fire_time: :desc)
|
|
68
|
-
end
|
|
69
|
-
|
|
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
|
-
def find_by_status(status)
|
|
76
|
-
::Kaal::CronDispatch.where(status: status).order(fire_time: :desc)
|
|
77
|
-
end
|
|
78
|
-
|
|
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
|
-
def cleanup(recovery_window: 86_400)
|
|
89
|
-
cutoff_time = Time.current - recovery_window
|
|
90
|
-
::Kaal::CronDispatch.where('fire_time < ?', cutoff_time).delete_all
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|