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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -11
  3. data/lib/kaal/active_record_support.rb +82 -0
  4. data/lib/kaal/backend/adapter.rb +4 -0
  5. data/lib/kaal/backend/memory_adapter.rb +5 -0
  6. data/lib/kaal/backend/mysql.rb +63 -0
  7. data/lib/kaal/backend/postgres.rb +45 -0
  8. data/lib/kaal/backend/redis_adapter.rb +5 -0
  9. data/lib/kaal/backend/sqlite.rb +45 -0
  10. data/lib/kaal/cli.rb +1 -0
  11. data/lib/kaal/config/configuration.rb +33 -2
  12. data/lib/kaal/config/delayed_job_security_policy.rb +60 -0
  13. data/lib/kaal/config.rb +1 -0
  14. data/lib/kaal/core/coordinator.rb +68 -19
  15. data/lib/kaal/definition/database_engine.rb +88 -0
  16. data/lib/kaal/delayed_job/database_engine.rb +116 -0
  17. data/lib/kaal/delayed_job/dispatch_failure_logger.rb +31 -0
  18. data/lib/kaal/delayed_job/memory_engine.rb +79 -0
  19. data/lib/kaal/delayed_job/mysql_version_support.rb +43 -0
  20. data/lib/kaal/delayed_job/redis_engine.rb +119 -0
  21. data/lib/kaal/delayed_job/registry.rb +39 -0
  22. data/lib/kaal/dispatch/database_engine.rb +120 -0
  23. data/lib/kaal/internal/active_record/base_record.rb +16 -0
  24. data/lib/kaal/internal/active_record/connection_support.rb +96 -0
  25. data/lib/kaal/internal/active_record/database_backend.rb +78 -0
  26. data/lib/kaal/internal/active_record/definition_record.rb +16 -0
  27. data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
  28. data/lib/kaal/internal/active_record/delayed_job_record.rb +16 -0
  29. data/lib/kaal/internal/active_record/delayed_job_registry.rb +119 -0
  30. data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
  31. data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
  32. data/lib/kaal/internal/active_record/lock_record.rb +16 -0
  33. data/lib/kaal/internal/active_record/migration_templates.rb +138 -0
  34. data/lib/kaal/internal/active_record/mysql_backend.rb +89 -0
  35. data/lib/kaal/internal/active_record/postgres_backend.rb +73 -0
  36. data/lib/kaal/internal/active_record.rb +19 -0
  37. data/lib/kaal/internal/sequel/database_backend.rb +79 -0
  38. data/lib/kaal/internal/sequel/mysql_backend.rb +83 -0
  39. data/lib/kaal/internal/sequel/postgres_backend.rb +71 -0
  40. data/lib/kaal/internal/sequel.rb +13 -0
  41. data/lib/kaal/job_dispatcher.rb +108 -0
  42. data/lib/kaal/persistence/database.rb +39 -0
  43. data/lib/kaal/persistence/migration_templates.rb +129 -0
  44. data/lib/kaal/registry.rb +0 -2
  45. data/lib/kaal/runtime/scheduler_boot_loader.rb +2 -0
  46. data/lib/kaal/scheduler_file/job_applier.rb +28 -53
  47. data/lib/kaal/sequel_support.rb +82 -0
  48. data/lib/kaal/version.rb +1 -1
  49. data/lib/kaal.rb +117 -0
  50. 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