kaal 0.3.0 → 0.5.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 +43 -11
- data/lib/kaal/active_record_support.rb +82 -0
- data/lib/kaal/backend/adapter.rb +4 -0
- data/lib/kaal/backend/memory_adapter.rb +5 -0
- data/lib/kaal/backend/mysql.rb +63 -0
- data/lib/kaal/backend/postgres.rb +45 -0
- data/lib/kaal/backend/redis_adapter.rb +5 -0
- data/lib/kaal/backend/sqlite.rb +45 -0
- data/lib/kaal/cli.rb +1 -0
- data/lib/kaal/config/configuration.rb +33 -2
- data/lib/kaal/config/delayed_job_security_policy.rb +60 -0
- data/lib/kaal/config.rb +1 -0
- data/lib/kaal/core/coordinator.rb +68 -19
- data/lib/kaal/definition/database_engine.rb +88 -0
- data/lib/kaal/delayed_job/database_engine.rb +116 -0
- data/lib/kaal/delayed_job/dispatch_failure_logger.rb +31 -0
- data/lib/kaal/delayed_job/memory_engine.rb +79 -0
- data/lib/kaal/delayed_job/mysql_version_support.rb +43 -0
- data/lib/kaal/delayed_job/redis_engine.rb +119 -0
- data/lib/kaal/delayed_job/registry.rb +39 -0
- data/lib/kaal/dispatch/database_engine.rb +120 -0
- 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 +78 -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/delayed_job_record.rb +16 -0
- data/lib/kaal/internal/active_record/delayed_job_registry.rb +119 -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 +138 -0
- data/lib/kaal/internal/active_record/mysql_backend.rb +89 -0
- data/lib/kaal/internal/active_record/postgres_backend.rb +73 -0
- data/lib/kaal/internal/active_record.rb +19 -0
- data/lib/kaal/internal/sequel/database_backend.rb +79 -0
- data/lib/kaal/internal/sequel/mysql_backend.rb +83 -0
- data/lib/kaal/internal/sequel/postgres_backend.rb +71 -0
- data/lib/kaal/internal/sequel.rb +13 -0
- data/lib/kaal/job_dispatcher.rb +108 -0
- data/lib/kaal/persistence/database.rb +39 -0
- data/lib/kaal/persistence/migration_templates.rb +129 -0
- data/lib/kaal/registry.rb +0 -2
- data/lib/kaal/runtime/scheduler_boot_loader.rb +2 -0
- data/lib/kaal/scheduler_file/job_applier.rb +28 -53
- data/lib/kaal/sequel_support.rb +82 -0
- data/lib/kaal/version.rb +1 -1
- data/lib/kaal.rb +117 -0
- metadata +36 -1
|
@@ -0,0 +1,89 @@
|
|
|
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/delayed_job/mysql_version_support'
|
|
9
|
+
|
|
10
|
+
module Kaal
|
|
11
|
+
module Internal
|
|
12
|
+
module ActiveRecord
|
|
13
|
+
# MySQL named-lock engine paired with Active Record registries.
|
|
14
|
+
class MySQLBackend < Kaal::Backend::Adapter
|
|
15
|
+
include Kaal::Backend::DispatchLogging
|
|
16
|
+
|
|
17
|
+
MAX_LOCK_NAME_LENGTH = 64
|
|
18
|
+
UNSET_SKIP_LOCKED_SUPPORT = Object.new.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(connection = nil, dispatch_registry: nil, definition_registry: nil, namespace: nil,
|
|
21
|
+
use_skip_locked: UNSET_SKIP_LOCKED_SUPPORT)
|
|
22
|
+
super()
|
|
23
|
+
ConnectionSupport.configure!(connection)
|
|
24
|
+
@dispatch_registry = dispatch_registry
|
|
25
|
+
@definition_registry = definition_registry
|
|
26
|
+
@namespace = namespace
|
|
27
|
+
@use_skip_locked = use_skip_locked
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def dispatch_registry
|
|
31
|
+
@dispatch_registry ||= DispatchRegistry.new(namespace: resolved_namespace)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def definition_registry
|
|
35
|
+
@definition_registry ||= DefinitionRegistry.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def delayed_store
|
|
39
|
+
@delayed_store ||= DelayedJobRegistry.new(use_skip_locked: supports_skip_locked?)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def acquire(key, _ttl)
|
|
43
|
+
acquired = scalar('SELECT GET_LOCK(?, 0) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
44
|
+
log_dispatch_attempt(key) if acquired
|
|
45
|
+
acquired
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
raise Kaal::Backend::LockAdapterError, "MySQL acquire failed for #{key}: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def release(key)
|
|
51
|
+
scalar('SELECT RELEASE_LOCK(?) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
raise Kaal::Backend::LockAdapterError, "MySQL release failed for #{key}: #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.normalize_lock_name(key)
|
|
57
|
+
return key if key.length <= MAX_LOCK_NAME_LENGTH
|
|
58
|
+
|
|
59
|
+
digest = Digest::SHA256.hexdigest(key)
|
|
60
|
+
prefix_length = MAX_LOCK_NAME_LENGTH - 17
|
|
61
|
+
"#{key[0...prefix_length]}:#{digest[0...16]}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def scalar(sql, *binds)
|
|
67
|
+
sanitized_sql = if binds.empty?
|
|
68
|
+
sql
|
|
69
|
+
else
|
|
70
|
+
BaseRecord.send(:sanitize_sql_array, [sql, *binds])
|
|
71
|
+
end
|
|
72
|
+
result = BaseRecord.connection.exec_query(sanitized_sql)
|
|
73
|
+
result.first.values.first
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resolved_namespace
|
|
77
|
+
@namespace || Kaal.configuration.namespace
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def supports_skip_locked?
|
|
81
|
+
return @use_skip_locked unless @use_skip_locked.equal?(UNSET_SKIP_LOCKED_SUPPORT)
|
|
82
|
+
|
|
83
|
+
version_string = scalar('SELECT VERSION() AS version')
|
|
84
|
+
Kaal::DelayedJob::MySQLVersionSupport.skip_locked_supported?(version_string)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
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 '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 delayed_store
|
|
36
|
+
@delayed_store ||= DelayedJobRegistry.new(use_skip_locked: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def acquire(key, _ttl)
|
|
40
|
+
acquired = scalar('SELECT pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
|
|
41
|
+
log_dispatch_attempt(key) if acquired
|
|
42
|
+
acquired
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL acquire failed for #{key}: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def release(key)
|
|
48
|
+
scalar('SELECT pg_advisory_unlock(?) AS released', self.class.calculate_lock_id(key)) == true
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL release failed for #{key}: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.calculate_lock_id(key)
|
|
54
|
+
hash = Digest::MD5.digest(key).unpack1('Q>')
|
|
55
|
+
hash > SIGNED_64_MAX ? hash - UNSIGNED_64_RANGE : hash
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def scalar(sql, value)
|
|
61
|
+
result = BaseRecord.connection.exec_query(
|
|
62
|
+
BaseRecord.send(:sanitize_sql_array, [sql, value])
|
|
63
|
+
)
|
|
64
|
+
result.first.values.first
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def resolved_namespace
|
|
68
|
+
@namespace || Kaal.configuration.namespace
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
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/delayed_job_record'
|
|
12
|
+
require 'kaal/internal/active_record/lock_record'
|
|
13
|
+
require 'kaal/internal/active_record/definition_registry'
|
|
14
|
+
require 'kaal/internal/active_record/dispatch_registry'
|
|
15
|
+
require 'kaal/internal/active_record/delayed_job_registry'
|
|
16
|
+
require 'kaal/internal/active_record/database_backend'
|
|
17
|
+
require 'kaal/internal/active_record/postgres_backend'
|
|
18
|
+
require 'kaal/internal/active_record/mysql_backend'
|
|
19
|
+
require 'kaal/internal/active_record/migration_templates'
|
|
@@ -0,0 +1,79 @@
|
|
|
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/delayed_job/database_engine'
|
|
9
|
+
require 'kaal/persistence/database'
|
|
10
|
+
|
|
11
|
+
module Kaal
|
|
12
|
+
module Internal
|
|
13
|
+
module Sequel
|
|
14
|
+
# Shared table-backed Sequel SQL engine used by the public SQLite backend.
|
|
15
|
+
class DatabaseBackend < Kaal::Backend::Adapter
|
|
16
|
+
include Kaal::Backend::DispatchLogging
|
|
17
|
+
|
|
18
|
+
def initialize(database, namespace: nil)
|
|
19
|
+
super()
|
|
20
|
+
@database = Kaal::Persistence::Database.new(database)
|
|
21
|
+
@namespace = namespace
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dispatch_registry
|
|
25
|
+
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new(database: @database.connection, namespace: resolved_namespace)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def definition_registry
|
|
29
|
+
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delayed_store
|
|
33
|
+
@delayed_store ||= Kaal::DelayedJob::DatabaseEngine.new(database: @database.connection)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def acquire(key, ttl)
|
|
37
|
+
now = Time.now.utc
|
|
38
|
+
expires_at = now + ttl
|
|
39
|
+
|
|
40
|
+
2.times do |attempt|
|
|
41
|
+
cleanup_expired_locks if attempt.positive?
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
dataset.insert(key: key, acquired_at: now, expires_at: expires_at)
|
|
45
|
+
log_dispatch_attempt(key)
|
|
46
|
+
return true
|
|
47
|
+
rescue ::Sequel::UniqueConstraintViolation
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
false
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
raise Kaal::Backend::LockAdapterError, "Database acquire failed for #{key}: #{e.message}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def release(key)
|
|
58
|
+
dataset.where(key: key).delete.positive?
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
raise Kaal::Backend::LockAdapterError, "Database release failed for #{key}: #{e.message}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def cleanup_expired_locks
|
|
64
|
+
dataset.where { expires_at < Time.now.utc }.delete
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def dataset
|
|
70
|
+
@database.locks_dataset
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def resolved_namespace
|
|
74
|
+
@namespace || Kaal.configuration.namespace
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
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/delayed_job/mysql_version_support'
|
|
10
|
+
require 'kaal/persistence/database'
|
|
11
|
+
|
|
12
|
+
module Kaal
|
|
13
|
+
module Internal
|
|
14
|
+
module Sequel
|
|
15
|
+
# MySQL named-lock engine backed by Sequel.
|
|
16
|
+
class MySQLBackend < Kaal::Backend::Adapter
|
|
17
|
+
include Kaal::Backend::DispatchLogging
|
|
18
|
+
|
|
19
|
+
MAX_LOCK_NAME_LENGTH = 64
|
|
20
|
+
UNSET_SKIP_LOCKED_SUPPORT = Object.new.freeze
|
|
21
|
+
|
|
22
|
+
def initialize(database, namespace: nil, use_skip_locked: UNSET_SKIP_LOCKED_SUPPORT)
|
|
23
|
+
super()
|
|
24
|
+
@database = Kaal::Persistence::Database.new(database)
|
|
25
|
+
@namespace = namespace
|
|
26
|
+
@use_skip_locked = use_skip_locked
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dispatch_registry
|
|
30
|
+
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new(database: @database.connection, namespace: resolved_namespace)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def definition_registry
|
|
34
|
+
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delayed_store
|
|
38
|
+
@delayed_store ||= Kaal::DelayedJob::DatabaseEngine.new(database: @database.connection, use_skip_locked: supports_skip_locked?)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def acquire(key, _ttl)
|
|
42
|
+
acquired = scalar('SELECT GET_LOCK(?, 0) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
43
|
+
log_dispatch_attempt(key) if acquired
|
|
44
|
+
acquired
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
raise Kaal::Backend::LockAdapterError, "MySQL acquire failed for #{key}: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def release(key)
|
|
50
|
+
scalar('SELECT RELEASE_LOCK(?) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
raise Kaal::Backend::LockAdapterError, "MySQL release failed for #{key}: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.normalize_lock_name(key)
|
|
56
|
+
return key if key.length <= MAX_LOCK_NAME_LENGTH
|
|
57
|
+
|
|
58
|
+
digest = Digest::SHA256.hexdigest(key)
|
|
59
|
+
prefix_length = MAX_LOCK_NAME_LENGTH - 17
|
|
60
|
+
"#{key[0...prefix_length]}:#{digest[0...16]}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def scalar(sql, *binds)
|
|
66
|
+
row = @database.connection.fetch(sql, *binds).first
|
|
67
|
+
row.values.first
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resolved_namespace
|
|
71
|
+
@namespace || Kaal.configuration.namespace
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def supports_skip_locked?
|
|
75
|
+
return @use_skip_locked unless @use_skip_locked.equal?(UNSET_SKIP_LOCKED_SUPPORT)
|
|
76
|
+
|
|
77
|
+
version_string = scalar('SELECT VERSION() AS version')
|
|
78
|
+
Kaal::DelayedJob::MySQLVersionSupport.skip_locked_supported?(version_string)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
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
|
+
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 delayed_store
|
|
36
|
+
@delayed_store ||= Kaal::DelayedJob::DatabaseEngine.new(database: @database.connection, use_skip_locked: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def acquire(key, _ttl)
|
|
40
|
+
acquired = scalar('SELECT pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
|
|
41
|
+
log_dispatch_attempt(key) if acquired
|
|
42
|
+
acquired
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL acquire failed for #{key}: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def release(key)
|
|
48
|
+
scalar('SELECT pg_advisory_unlock(?) AS released', self.class.calculate_lock_id(key)) == true
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL release failed for #{key}: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.calculate_lock_id(key)
|
|
54
|
+
hash = Digest::MD5.digest(key).unpack1('Q>')
|
|
55
|
+
hash > SIGNED_64_MAX ? hash - UNSIGNED_64_RANGE : hash
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def scalar(sql, *binds)
|
|
61
|
+
row = @database.connection.fetch(sql, *binds).first
|
|
62
|
+
row.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,13 @@
|
|
|
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/delayed_job/database_engine'
|
|
12
|
+
require 'kaal/dispatch/database_engine'
|
|
13
|
+
require 'kaal/persistence/database'
|
|
@@ -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
|
+
# Shared job-class resolution and dispatch rules used by recurring and delayed jobs.
|
|
9
|
+
module JobDispatcher
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def resolve_job_class(job_class_name:, key:, queue: nil, apply_delayed_job_allow_list: true)
|
|
13
|
+
job_class = normalize_job_class(job_class_name, key, apply_delayed_job_allow_list:)
|
|
14
|
+
validate_dispatch_interface(job_class, key, queue)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def normalized_job_class_name(job_class_name:, key:, apply_delayed_job_allow_list: true)
|
|
18
|
+
normalized_job_class_name = normalize_job_class_name(job_class_name)
|
|
19
|
+
raise SchedulerConfigError, "Job class cannot be blank for key '#{key}'" if normalized_job_class_name.empty?
|
|
20
|
+
|
|
21
|
+
return normalized_job_class_name unless apply_delayed_job_allow_list
|
|
22
|
+
|
|
23
|
+
validate_allowed_job_class_name!(job_class_name: normalized_job_class_name, key:)
|
|
24
|
+
normalized_job_class_name
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dispatch(job_class:, queue:, args:, key: nil)
|
|
28
|
+
job_class_name = job_class.name
|
|
29
|
+
scheduler_context = key ? " for scheduler job '#{key}'" : ''
|
|
30
|
+
|
|
31
|
+
if queue && !job_class.respond_to?(:set)
|
|
32
|
+
raise SchedulerConfigError,
|
|
33
|
+
"job_class '#{job_class_name}' must respond to .set to use queue #{queue.inspect}#{scheduler_context}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if queue
|
|
37
|
+
job_class.set(queue: queue).perform_later(*args)
|
|
38
|
+
elsif job_class.respond_to?(:perform_later)
|
|
39
|
+
job_class.perform_later(*args)
|
|
40
|
+
elsif job_class.respond_to?(:perform)
|
|
41
|
+
job_class.perform(*args)
|
|
42
|
+
else
|
|
43
|
+
raise SchedulerConfigError,
|
|
44
|
+
"job_class '#{job_class_name}' must respond to .perform, .perform_later, or .set(...).perform_later#{scheduler_context}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def active_job_dispatch?(job_class, queue)
|
|
49
|
+
(queue && job_class.respond_to?(:set)) || job_class.respond_to?(:perform_later)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalize_job_class_name(job_class)
|
|
53
|
+
case job_class
|
|
54
|
+
when Module
|
|
55
|
+
job_class.name.to_s.strip
|
|
56
|
+
else
|
|
57
|
+
job_class.to_s.strip
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def normalize_job_class(job_class_name, key, apply_delayed_job_allow_list: true)
|
|
62
|
+
normalized_job_class_name = normalized_job_class_name(
|
|
63
|
+
job_class_name:,
|
|
64
|
+
key:,
|
|
65
|
+
apply_delayed_job_allow_list:
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return job_class_name if job_class_name.is_a?(Module)
|
|
69
|
+
|
|
70
|
+
job_class = begin
|
|
71
|
+
Kaal::Support::HashTools.constantize(normalized_job_class_name)
|
|
72
|
+
rescue NameError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
return job_class if job_class
|
|
77
|
+
|
|
78
|
+
raise SchedulerConfigError, "Unknown job_class #{normalized_job_class_name.inspect} for key '#{key}'"
|
|
79
|
+
end
|
|
80
|
+
private_class_method :normalize_job_class
|
|
81
|
+
|
|
82
|
+
def validate_allowed_job_class_name!(job_class_name:, key:)
|
|
83
|
+
allowed_prefixes = Array(Kaal.configuration.delayed_job_allowed_class_prefixes)
|
|
84
|
+
return if allowed_prefixes.empty?
|
|
85
|
+
return if allowed_prefixes.any? { |prefix| job_class_name.start_with?(prefix) }
|
|
86
|
+
|
|
87
|
+
raise SchedulerConfigError,
|
|
88
|
+
"job_class '#{job_class_name}' for key '#{key}' is not allowed by delayed_job_allowed_class_prefixes"
|
|
89
|
+
end
|
|
90
|
+
private_class_method :validate_allowed_job_class_name!
|
|
91
|
+
|
|
92
|
+
def validate_dispatch_interface(job_class, key, queue)
|
|
93
|
+
queue_present = !queue.nil?
|
|
94
|
+
no_queue = !queue_present
|
|
95
|
+
supports_set = job_class.respond_to?(:set)
|
|
96
|
+
supports_perform_later = job_class.respond_to?(:perform_later)
|
|
97
|
+
supports_perform = job_class.respond_to?(:perform)
|
|
98
|
+
|
|
99
|
+
return job_class if queue_present && supports_set
|
|
100
|
+
return job_class if no_queue && supports_perform_later
|
|
101
|
+
return job_class if no_queue && supports_perform
|
|
102
|
+
|
|
103
|
+
raise SchedulerConfigError,
|
|
104
|
+
"job_class '#{job_class.name}' for key '#{key}' must respond to .perform, .perform_later, or .set(...).perform_later"
|
|
105
|
+
end
|
|
106
|
+
private_class_method :validate_dispatch_interface
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
|
|
34
|
+
def delayed_jobs_dataset
|
|
35
|
+
connection[:kaal_delayed_jobs]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|