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
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
# Rails migration templates for Active Record-backed Kaal tables.
|
|
11
|
+
module MigrationTemplates
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def for_backend(backend)
|
|
15
|
+
case backend.to_s
|
|
16
|
+
when 'sqlite'
|
|
17
|
+
{
|
|
18
|
+
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
19
|
+
'002_create_kaal_locks.rb' => locks_template,
|
|
20
|
+
'003_create_kaal_definitions.rb' => definitions_template('sqlite')
|
|
21
|
+
}
|
|
22
|
+
when 'postgres'
|
|
23
|
+
{
|
|
24
|
+
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
25
|
+
'002_create_kaal_definitions.rb' => definitions_template('postgres')
|
|
26
|
+
}
|
|
27
|
+
when 'mysql'
|
|
28
|
+
{
|
|
29
|
+
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
30
|
+
'002_create_kaal_definitions.rb' => definitions_template('mysql')
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
{}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def dispatches_template
|
|
38
|
+
<<~RUBY
|
|
39
|
+
class CreateKaalDispatches < ActiveRecord::Migration[7.1]
|
|
40
|
+
def change
|
|
41
|
+
create_table :kaal_dispatches do |t|
|
|
42
|
+
t.string :key, null: false
|
|
43
|
+
t.datetime :fire_time, null: false
|
|
44
|
+
t.datetime :dispatched_at, null: false
|
|
45
|
+
t.string :node_id, null: false
|
|
46
|
+
t.string :status, null: false, default: 'dispatched', limit: 50
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
add_index :kaal_dispatches, [:key, :fire_time], unique: true
|
|
50
|
+
add_index :kaal_dispatches, :key
|
|
51
|
+
add_index :kaal_dispatches, :node_id
|
|
52
|
+
add_index :kaal_dispatches, :status
|
|
53
|
+
add_index :kaal_dispatches, :fire_time
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
RUBY
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def locks_template
|
|
60
|
+
<<~RUBY
|
|
61
|
+
class CreateKaalLocks < ActiveRecord::Migration[7.1]
|
|
62
|
+
def change
|
|
63
|
+
create_table :kaal_locks do |t|
|
|
64
|
+
t.string :key, null: false
|
|
65
|
+
t.datetime :acquired_at, null: false
|
|
66
|
+
t.datetime :expires_at, null: false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
add_index :kaal_locks, :key, unique: true
|
|
70
|
+
add_index :kaal_locks, :expires_at
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
RUBY
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def definitions_template(backend)
|
|
77
|
+
metadata_definition =
|
|
78
|
+
if backend == 'mysql'
|
|
79
|
+
't.text :metadata, null: false'
|
|
80
|
+
else
|
|
81
|
+
"t.text :metadata, null: false, default: '{}'"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
<<~RUBY
|
|
85
|
+
class CreateKaalDefinitions < ActiveRecord::Migration[7.1]
|
|
86
|
+
def change
|
|
87
|
+
create_table :kaal_definitions do |t|
|
|
88
|
+
t.string :key, null: false
|
|
89
|
+
t.string :cron, null: false
|
|
90
|
+
t.boolean :enabled, null: false, default: true
|
|
91
|
+
t.string :source, null: false
|
|
92
|
+
#{metadata_definition}
|
|
93
|
+
t.datetime :disabled_at
|
|
94
|
+
t.datetime :created_at, null: false
|
|
95
|
+
t.datetime :updated_at, null: false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
add_index :kaal_definitions, :key, unique: true
|
|
99
|
+
add_index :kaal_definitions, :enabled
|
|
100
|
+
add_index :kaal_definitions, :source
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
RUBY
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
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 'digest'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
module Internal
|
|
11
|
+
module ActiveRecord
|
|
12
|
+
# MySQL named-lock engine paired with Active Record registries.
|
|
13
|
+
class MySQLBackend < Kaal::Backend::Adapter
|
|
14
|
+
include Kaal::Backend::DispatchLogging
|
|
15
|
+
|
|
16
|
+
MAX_LOCK_NAME_LENGTH = 64
|
|
17
|
+
|
|
18
|
+
def initialize(connection = nil, dispatch_registry: nil, definition_registry: nil, namespace: nil)
|
|
19
|
+
super()
|
|
20
|
+
ConnectionSupport.configure!(connection)
|
|
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
|
+
acquired = scalar('SELECT GET_LOCK(?, 0) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
36
|
+
log_dispatch_attempt(key) if acquired
|
|
37
|
+
acquired
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
raise Kaal::Backend::LockAdapterError, "MySQL acquire failed for #{key}: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def release(key)
|
|
43
|
+
scalar('SELECT RELEASE_LOCK(?) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
raise Kaal::Backend::LockAdapterError, "MySQL release failed for #{key}: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.normalize_lock_name(key)
|
|
49
|
+
return key if key.length <= MAX_LOCK_NAME_LENGTH
|
|
50
|
+
|
|
51
|
+
digest = Digest::SHA256.hexdigest(key)
|
|
52
|
+
prefix_length = MAX_LOCK_NAME_LENGTH - 17
|
|
53
|
+
"#{key[0...prefix_length]}:#{digest[0...16]}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def scalar(sql, value)
|
|
59
|
+
result = BaseRecord.connection.exec_query(
|
|
60
|
+
BaseRecord.send(:sanitize_sql_array, [sql, value])
|
|
61
|
+
)
|
|
62
|
+
result.first.values.first
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def resolved_namespace
|
|
66
|
+
@namespace || Kaal.configuration.namespace
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
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 'digest'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
module Internal
|
|
11
|
+
module ActiveRecord
|
|
12
|
+
# PostgreSQL advisory-lock engine paired with Active Record registries.
|
|
13
|
+
class PostgresBackend < Kaal::Backend::Adapter
|
|
14
|
+
include Kaal::Backend::DispatchLogging
|
|
15
|
+
|
|
16
|
+
SIGNED_64_MAX = 9_223_372_036_854_775_807
|
|
17
|
+
UNSIGNED_64_RANGE = 18_446_744_073_709_551_616
|
|
18
|
+
|
|
19
|
+
def initialize(connection = nil, dispatch_registry: nil, definition_registry: nil, namespace: nil)
|
|
20
|
+
super()
|
|
21
|
+
ConnectionSupport.configure!(connection)
|
|
22
|
+
@dispatch_registry = dispatch_registry
|
|
23
|
+
@definition_registry = definition_registry
|
|
24
|
+
@namespace = namespace
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dispatch_registry
|
|
28
|
+
@dispatch_registry ||= DispatchRegistry.new(namespace: resolved_namespace)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def definition_registry
|
|
32
|
+
@definition_registry ||= DefinitionRegistry.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def acquire(key, _ttl)
|
|
36
|
+
acquired = scalar('SELECT pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
|
|
37
|
+
log_dispatch_attempt(key) if acquired
|
|
38
|
+
acquired
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL acquire failed for #{key}: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def release(key)
|
|
44
|
+
scalar('SELECT pg_advisory_unlock(?) AS released', self.class.calculate_lock_id(key)) == true
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL release failed for #{key}: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.calculate_lock_id(key)
|
|
50
|
+
hash = Digest::MD5.digest(key).unpack1('Q>')
|
|
51
|
+
hash > SIGNED_64_MAX ? hash - UNSIGNED_64_RANGE : hash
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def scalar(sql, value)
|
|
57
|
+
result = BaseRecord.connection.exec_query(
|
|
58
|
+
BaseRecord.send(:sanitize_sql_array, [sql, value])
|
|
59
|
+
)
|
|
60
|
+
result.first.values.first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolved_namespace
|
|
64
|
+
@namespace || Kaal.configuration.namespace
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
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/internal/active_record/base_record'
|
|
8
|
+
require 'kaal/internal/active_record/connection_support'
|
|
9
|
+
require 'kaal/internal/active_record/definition_record'
|
|
10
|
+
require 'kaal/internal/active_record/dispatch_record'
|
|
11
|
+
require 'kaal/internal/active_record/lock_record'
|
|
12
|
+
require 'kaal/internal/active_record/definition_registry'
|
|
13
|
+
require 'kaal/internal/active_record/dispatch_registry'
|
|
14
|
+
require 'kaal/internal/active_record/database_backend'
|
|
15
|
+
require 'kaal/internal/active_record/postgres_backend'
|
|
16
|
+
require 'kaal/internal/active_record/mysql_backend'
|
|
17
|
+
require 'kaal/internal/active_record/migration_templates'
|
|
@@ -0,0 +1,74 @@
|
|
|
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/dispatch_logging'
|
|
8
|
+
require 'kaal/persistence/database'
|
|
9
|
+
|
|
10
|
+
module Kaal
|
|
11
|
+
module Internal
|
|
12
|
+
module Sequel
|
|
13
|
+
# Shared table-backed Sequel SQL engine used by the public SQLite backend.
|
|
14
|
+
class DatabaseBackend < Kaal::Backend::Adapter
|
|
15
|
+
include Kaal::Backend::DispatchLogging
|
|
16
|
+
|
|
17
|
+
def initialize(database, namespace: nil)
|
|
18
|
+
super()
|
|
19
|
+
@database = Kaal::Persistence::Database.new(database)
|
|
20
|
+
@namespace = namespace
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def dispatch_registry
|
|
24
|
+
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new(database: @database.connection, namespace: resolved_namespace)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def definition_registry
|
|
28
|
+
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def acquire(key, ttl)
|
|
32
|
+
now = Time.now.utc
|
|
33
|
+
expires_at = now + ttl
|
|
34
|
+
|
|
35
|
+
2.times do |attempt|
|
|
36
|
+
cleanup_expired_locks if attempt.positive?
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
dataset.insert(key: key, acquired_at: now, expires_at: expires_at)
|
|
40
|
+
log_dispatch_attempt(key)
|
|
41
|
+
return true
|
|
42
|
+
rescue ::Sequel::UniqueConstraintViolation
|
|
43
|
+
next
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
false
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
raise Kaal::Backend::LockAdapterError, "Database acquire failed for #{key}: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def release(key)
|
|
53
|
+
dataset.where(key: key).delete.positive?
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
raise Kaal::Backend::LockAdapterError, "Database release failed for #{key}: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cleanup_expired_locks
|
|
59
|
+
dataset.where { expires_at < Time.now.utc }.delete
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def dataset
|
|
65
|
+
@database.locks_dataset
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def resolved_namespace
|
|
69
|
+
@namespace || Kaal.configuration.namespace
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
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 'digest'
|
|
8
|
+
require 'kaal/backend/dispatch_logging'
|
|
9
|
+
require 'kaal/persistence/database'
|
|
10
|
+
|
|
11
|
+
module Kaal
|
|
12
|
+
module Internal
|
|
13
|
+
module Sequel
|
|
14
|
+
# MySQL named-lock engine backed by Sequel.
|
|
15
|
+
class MySQLBackend < Kaal::Backend::Adapter
|
|
16
|
+
include Kaal::Backend::DispatchLogging
|
|
17
|
+
|
|
18
|
+
MAX_LOCK_NAME_LENGTH = 64
|
|
19
|
+
|
|
20
|
+
def initialize(database, namespace: nil)
|
|
21
|
+
super()
|
|
22
|
+
@database = Kaal::Persistence::Database.new(database)
|
|
23
|
+
@namespace = namespace
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def dispatch_registry
|
|
27
|
+
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new(database: @database.connection, namespace: resolved_namespace)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def definition_registry
|
|
31
|
+
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def acquire(key, _ttl)
|
|
35
|
+
acquired = scalar('SELECT GET_LOCK(?, 0) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
36
|
+
log_dispatch_attempt(key) if acquired
|
|
37
|
+
acquired
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
raise Kaal::Backend::LockAdapterError, "MySQL acquire failed for #{key}: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def release(key)
|
|
43
|
+
scalar('SELECT RELEASE_LOCK(?) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
raise Kaal::Backend::LockAdapterError, "MySQL release failed for #{key}: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.normalize_lock_name(key)
|
|
49
|
+
return key if key.length <= MAX_LOCK_NAME_LENGTH
|
|
50
|
+
|
|
51
|
+
digest = Digest::SHA256.hexdigest(key)
|
|
52
|
+
prefix_length = MAX_LOCK_NAME_LENGTH - 17
|
|
53
|
+
"#{key[0...prefix_length]}:#{digest[0...16]}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def scalar(sql, *binds)
|
|
59
|
+
row = @database.connection.fetch(sql, *binds).first
|
|
60
|
+
row.values.first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolved_namespace
|
|
64
|
+
@namespace || Kaal.configuration.namespace
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
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 'digest'
|
|
8
|
+
require 'kaal/backend/dispatch_logging'
|
|
9
|
+
require 'kaal/persistence/database'
|
|
10
|
+
|
|
11
|
+
module Kaal
|
|
12
|
+
module Internal
|
|
13
|
+
module Sequel
|
|
14
|
+
# PostgreSQL advisory-lock engine backed by Sequel.
|
|
15
|
+
class PostgresBackend < Kaal::Backend::Adapter
|
|
16
|
+
include Kaal::Backend::DispatchLogging
|
|
17
|
+
|
|
18
|
+
SIGNED_64_MAX = 9_223_372_036_854_775_807
|
|
19
|
+
UNSIGNED_64_RANGE = 18_446_744_073_709_551_616
|
|
20
|
+
|
|
21
|
+
def initialize(database, namespace: nil)
|
|
22
|
+
super()
|
|
23
|
+
@database = Kaal::Persistence::Database.new(database)
|
|
24
|
+
@namespace = namespace
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dispatch_registry
|
|
28
|
+
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new(database: @database.connection, namespace: resolved_namespace)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def definition_registry
|
|
32
|
+
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def acquire(key, _ttl)
|
|
36
|
+
acquired = scalar('SELECT pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
|
|
37
|
+
log_dispatch_attempt(key) if acquired
|
|
38
|
+
acquired
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL acquire failed for #{key}: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def release(key)
|
|
44
|
+
scalar('SELECT pg_advisory_unlock(?) AS released', self.class.calculate_lock_id(key)) == true
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL release failed for #{key}: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.calculate_lock_id(key)
|
|
50
|
+
hash = Digest::MD5.digest(key).unpack1('Q>')
|
|
51
|
+
hash > SIGNED_64_MAX ? hash - UNSIGNED_64_RANGE : hash
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def scalar(sql, *binds)
|
|
57
|
+
row = @database.connection.fetch(sql, *binds).first
|
|
58
|
+
row.values.first
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resolved_namespace
|
|
62
|
+
@namespace || Kaal.configuration.namespace
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
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/internal/sequel/database_backend'
|
|
8
|
+
require 'kaal/internal/sequel/postgres_backend'
|
|
9
|
+
require 'kaal/internal/sequel/mysql_backend'
|
|
10
|
+
require 'kaal/definition/database_engine'
|
|
11
|
+
require 'kaal/dispatch/database_engine'
|
|
12
|
+
require 'kaal/persistence/database'
|
|
@@ -0,0 +1,35 @@
|
|
|
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 Persistence
|
|
9
|
+
# Thin wrapper around a Sequel connection to keep table access consistent.
|
|
10
|
+
class Database
|
|
11
|
+
attr_reader :connection
|
|
12
|
+
|
|
13
|
+
def initialize(connection)
|
|
14
|
+
Kaal::Sequel.require_sequel!
|
|
15
|
+
@connection = if connection.is_a?(::Sequel::Database)
|
|
16
|
+
connection
|
|
17
|
+
else
|
|
18
|
+
::Sequel.connect(connection)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def definitions_dataset
|
|
23
|
+
connection[:kaal_definitions]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def dispatches_dataset
|
|
27
|
+
connection[:kaal_dispatches]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def locks_dataset
|
|
31
|
+
connection[:kaal_locks]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
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 Persistence
|
|
9
|
+
# Sequel migration templates emitted by `kaal init`.
|
|
10
|
+
module MigrationTemplates
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def for_backend(backend)
|
|
14
|
+
case backend.to_s
|
|
15
|
+
when 'sqlite'
|
|
16
|
+
{
|
|
17
|
+
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
18
|
+
'002_create_kaal_locks.rb' => locks_template,
|
|
19
|
+
'003_create_kaal_definitions.rb' => definitions_template
|
|
20
|
+
}
|
|
21
|
+
when 'postgres', 'mysql'
|
|
22
|
+
{
|
|
23
|
+
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
24
|
+
'002_create_kaal_definitions.rb' => definitions_template
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def dispatches_template
|
|
32
|
+
<<~RUBY
|
|
33
|
+
Sequel.migration do
|
|
34
|
+
change do
|
|
35
|
+
create_table?(:kaal_dispatches) do
|
|
36
|
+
primary_key :id
|
|
37
|
+
String :key, null: false
|
|
38
|
+
Time :fire_time, null: false
|
|
39
|
+
Time :dispatched_at, null: false
|
|
40
|
+
String :node_id, null: false
|
|
41
|
+
String :status, null: false, default: 'dispatched', size: 50
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
add_index :kaal_dispatches, [:key, :fire_time], unique: true
|
|
45
|
+
add_index :kaal_dispatches, :key
|
|
46
|
+
add_index :kaal_dispatches, :node_id
|
|
47
|
+
add_index :kaal_dispatches, :status
|
|
48
|
+
add_index :kaal_dispatches, :fire_time
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
RUBY
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def locks_template
|
|
55
|
+
<<~RUBY
|
|
56
|
+
Sequel.migration do
|
|
57
|
+
change do
|
|
58
|
+
create_table?(:kaal_locks) do
|
|
59
|
+
primary_key :id
|
|
60
|
+
String :key, null: false
|
|
61
|
+
Time :acquired_at, null: false
|
|
62
|
+
Time :expires_at, null: false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
add_index :kaal_locks, :key, unique: true
|
|
66
|
+
add_index :kaal_locks, :expires_at
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
RUBY
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def definitions_template
|
|
73
|
+
<<~RUBY
|
|
74
|
+
Sequel.migration do
|
|
75
|
+
change do
|
|
76
|
+
create_table?(:kaal_definitions) do
|
|
77
|
+
primary_key :id
|
|
78
|
+
String :key, null: false
|
|
79
|
+
String :cron, null: false
|
|
80
|
+
TrueClass :enabled, null: false, default: true
|
|
81
|
+
String :source, null: false
|
|
82
|
+
String :metadata, text: true, null: false, default: '{}'
|
|
83
|
+
Time :disabled_at
|
|
84
|
+
Time :created_at, null: false
|
|
85
|
+
Time :updated_at, null: false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
add_index :kaal_definitions, :key, unique: true
|
|
89
|
+
add_index :kaal_definitions, :enabled
|
|
90
|
+
add_index :kaal_definitions, :source
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
RUBY
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/kaal/registry.rb
CHANGED
|
@@ -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
|
##
|
|
10
9
|
# Thread-safe registry for storing and managing registered cron jobs.
|
|
@@ -21,9 +20,7 @@ module Kaal
|
|
|
21
20
|
|
|
22
21
|
##
|
|
23
22
|
# Entry class representing a single registered cron job
|
|
24
|
-
# rubocop:disable Style/RedundantStructKeywordInit
|
|
25
23
|
Entry = Struct.new(:key, :cron, :enqueue, keyword_init: true)
|
|
26
|
-
# rubocop:enable Style/RedundantStructKeywordInit
|
|
27
24
|
|
|
28
25
|
##
|
|
29
26
|
# Initialize a new Registry instance.
|
|
@@ -0,0 +1,41 @@
|
|
|
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 'pathname'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
# Resolves environment and path information for plain-Ruby runtime loading.
|
|
11
|
+
class RuntimeContext
|
|
12
|
+
DEFAULT_ENVIRONMENT_NAME = 'development'
|
|
13
|
+
ENVIRONMENT_KEYS = %w[KAAL_ENV RAILS_ENV APP_ENV RACK_ENV].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :environment_name, :root_path
|
|
16
|
+
|
|
17
|
+
def self.default(env: ENV, root_path: Dir.pwd)
|
|
18
|
+
new(root_path: root_path, environment_name: environment_name_from(env))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.environment_name_from(env)
|
|
22
|
+
ENVIRONMENT_KEYS.each do |key|
|
|
23
|
+
value = env[key].to_s.strip
|
|
24
|
+
return value unless value.empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
DEFAULT_ENVIRONMENT_NAME
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(root_path:, environment_name:)
|
|
31
|
+
@root_path = Pathname.new(root_path)
|
|
32
|
+
@environment_name = environment_name.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolve_path(path)
|
|
36
|
+
return path.to_s if Pathname.new(path).absolute?
|
|
37
|
+
|
|
38
|
+
root_path.join(path).to_s
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|