kaal 0.4.0 → 0.6.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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -25
  3. data/config/kaal.yml +12 -0
  4. data/lib/kaal/active_record_support.rb +2 -2
  5. data/lib/kaal/backend/adapter.rb +8 -0
  6. data/lib/kaal/backend/memory_adapter.rb +5 -0
  7. data/lib/kaal/backend/mysql.rb +25 -3
  8. data/lib/kaal/backend/postgres.rb +6 -2
  9. data/lib/kaal/backend/redis_adapter.rb +5 -0
  10. data/lib/kaal/backend/sqlite.rb +4 -0
  11. data/lib/kaal/cli.rb +38 -33
  12. data/lib/kaal/config/backend_factory.rb +178 -0
  13. data/lib/kaal/config/configuration.rb +98 -9
  14. data/lib/kaal/config/delayed_job_security_policy.rb +60 -0
  15. data/lib/kaal/config/file_loader.rb +187 -0
  16. data/lib/kaal/config.rb +3 -0
  17. data/lib/kaal/core/coordinator.rb +68 -19
  18. data/lib/kaal/delayed_job/database_engine.rb +116 -0
  19. data/lib/kaal/delayed_job/dispatch_failure_logger.rb +31 -0
  20. data/lib/kaal/delayed_job/memory_engine.rb +79 -0
  21. data/lib/kaal/delayed_job/mysql_version_support.rb +43 -0
  22. data/lib/kaal/delayed_job/redis_engine.rb +119 -0
  23. data/lib/kaal/delayed_job/registry.rb +39 -0
  24. data/lib/kaal/internal/active_record/database_backend.rb +5 -0
  25. data/lib/kaal/internal/active_record/delayed_job_record.rb +16 -0
  26. data/lib/kaal/internal/active_record/delayed_job_registry.rb +119 -0
  27. data/lib/kaal/internal/active_record/migration_templates.rb +33 -3
  28. data/lib/kaal/internal/active_record/mysql_backend.rb +23 -5
  29. data/lib/kaal/internal/active_record/postgres_backend.rb +4 -0
  30. data/lib/kaal/internal/active_record.rb +2 -0
  31. data/lib/kaal/internal/sequel/database_backend.rb +5 -0
  32. data/lib/kaal/internal/sequel/mysql_backend.rb +15 -1
  33. data/lib/kaal/internal/sequel/postgres_backend.rb +4 -0
  34. data/lib/kaal/internal/sequel.rb +1 -0
  35. data/lib/kaal/job_dispatcher.rb +108 -0
  36. data/lib/kaal/persistence/database.rb +4 -0
  37. data/lib/kaal/persistence/migration_templates.rb +35 -3
  38. data/lib/kaal/runtime/scheduler_boot_loader.rb +3 -1
  39. data/lib/kaal/scheduler_file/job_applier.rb +28 -53
  40. data/lib/kaal/scheduler_file/loader.rb +1 -1
  41. data/lib/kaal/sequel_support.rb +2 -2
  42. data/lib/kaal/version.rb +1 -1
  43. data/lib/kaal.rb +118 -0
  44. data/sig/00_types.rbs +12 -0
  45. data/sig/dependencies.rbs +49 -0
  46. data/sig/kaal/active_record_support.rbs +23 -0
  47. data/sig/kaal/backend/adapter.rbs +26 -0
  48. data/sig/kaal/backend/dispatch_attempt_logger.rbs +17 -0
  49. data/sig/kaal/backend/dispatch_logging.rbs +23 -0
  50. data/sig/kaal/backend/dispatch_registry_accessor.rbs +17 -0
  51. data/sig/kaal/backend/memory_adapter.rbs +33 -0
  52. data/sig/kaal/backend/mysql.rbs +25 -0
  53. data/sig/kaal/backend/postgres.rbs +19 -0
  54. data/sig/kaal/backend/redis_adapter.rbs +41 -0
  55. data/sig/kaal/backend/sqlite.rbs +19 -0
  56. data/sig/kaal/cli.rbs +41 -0
  57. data/sig/kaal/config/backend_factory.rbs +41 -0
  58. data/sig/kaal/config/configuration.rbs +70 -0
  59. data/sig/kaal/config/delayed_job_security_policy.rbs +19 -0
  60. data/sig/kaal/config/file_loader.rbs +35 -0
  61. data/sig/kaal/config/scheduler_config_error.rbs +4 -0
  62. data/sig/kaal/config/scheduler_time_zone_resolver.rbs +19 -0
  63. data/sig/kaal/config.rbs +11 -0
  64. data/sig/kaal/core/coordinator.rbs +103 -0
  65. data/sig/kaal/core/enabled_entry_enumerator.rbs +21 -0
  66. data/sig/kaal/core/occurrence_finder.rbs +9 -0
  67. data/sig/kaal/core.rbs +9 -0
  68. data/sig/kaal/definition/database_engine.rbs +25 -0
  69. data/sig/kaal/definition/memory_engine.rbs +23 -0
  70. data/sig/kaal/definition/persistence_helpers.rbs +9 -0
  71. data/sig/kaal/definition/redis_engine.rbs +33 -0
  72. data/sig/kaal/definition/registry.rbs +29 -0
  73. data/sig/kaal/definitions/registration_service.rbs +27 -0
  74. data/sig/kaal/definitions/registry_accessor.rbs +17 -0
  75. data/sig/kaal/delayed_job/database_engine.rbs +37 -0
  76. data/sig/kaal/delayed_job/dispatch_failure_logger.rbs +7 -0
  77. data/sig/kaal/delayed_job/memory_engine.rbs +29 -0
  78. data/sig/kaal/delayed_job/mysql_version_support.rbs +15 -0
  79. data/sig/kaal/delayed_job/redis_engine.rbs +31 -0
  80. data/sig/kaal/delayed_job/registry.rbs +20 -0
  81. data/sig/kaal/dispatch/database_engine.rbs +39 -0
  82. data/sig/kaal/dispatch/memory_engine.rbs +23 -0
  83. data/sig/kaal/dispatch/redis_engine.rbs +25 -0
  84. data/sig/kaal/dispatch/registry.rbs +11 -0
  85. data/sig/kaal/internal/active_record/base_record.rbs +8 -0
  86. data/sig/kaal/internal/active_record/connection_support.rbs +25 -0
  87. data/sig/kaal/internal/active_record/database_backend.rbs +37 -0
  88. data/sig/kaal/internal/active_record/definition_record.rbs +8 -0
  89. data/sig/kaal/internal/active_record/definition_registry.rbs +27 -0
  90. data/sig/kaal/internal/active_record/delayed_job_record.rbs +8 -0
  91. data/sig/kaal/internal/active_record/delayed_job_registry.rbs +39 -0
  92. data/sig/kaal/internal/active_record/dispatch_record.rbs +8 -0
  93. data/sig/kaal/internal/active_record/dispatch_registry.rbs +43 -0
  94. data/sig/kaal/internal/active_record/lock_record.rbs +8 -0
  95. data/sig/kaal/internal/active_record/migration_templates.rbs +17 -0
  96. data/sig/kaal/internal/active_record/mysql_backend.rbs +45 -0
  97. data/sig/kaal/internal/active_record/postgres_backend.rbs +41 -0
  98. data/sig/kaal/internal/active_record.rbs +0 -0
  99. data/sig/kaal/internal/sequel/database_backend.rbs +39 -0
  100. data/sig/kaal/internal/sequel/mysql_backend.rbs +47 -0
  101. data/sig/kaal/internal/sequel/postgres_backend.rbs +43 -0
  102. data/sig/kaal/internal/sequel.rbs +0 -0
  103. data/sig/kaal/job_dispatcher.rbs +19 -0
  104. data/sig/kaal/persistence/database.rbs +19 -0
  105. data/sig/kaal/persistence/migration_templates.rbs +15 -0
  106. data/sig/kaal/register_conflict_support.rbs +11 -0
  107. data/sig/kaal/registry.rbs +44 -0
  108. data/sig/kaal/runtime/runtime_context.rbs +23 -0
  109. data/sig/kaal/runtime/scheduler_boot_loader.rbs +23 -0
  110. data/sig/kaal/runtime/signal_handler_chain.rbs +19 -0
  111. data/sig/kaal/runtime/signal_handler_installer.rbs +19 -0
  112. data/sig/kaal/runtime.rbs +11 -0
  113. data/sig/kaal/scheduler_file/hash_transform.rbs +9 -0
  114. data/sig/kaal/scheduler_file/helper_bundle.rbs +15 -0
  115. data/sig/kaal/scheduler_file/job_applier.rbs +43 -0
  116. data/sig/kaal/scheduler_file/job_normalizer.rbs +27 -0
  117. data/sig/kaal/scheduler_file/loader.rbs +69 -0
  118. data/sig/kaal/scheduler_file/payload_loader.rbs +33 -0
  119. data/sig/kaal/scheduler_file/placeholder_support.rbs +19 -0
  120. data/sig/kaal/scheduler_file.rbs +9 -0
  121. data/sig/kaal/sequel_support.rbs +25 -0
  122. data/sig/kaal/support/hash_tools.rbs +27 -0
  123. data/sig/kaal/utils/cron_humanizer.rbs +39 -0
  124. data/sig/kaal/utils/cron_utils.rbs +43 -0
  125. data/sig/kaal/utils/idempotency_key_generator.rbs +5 -0
  126. data/sig/kaal/utils.rbs +9 -0
  127. data/sig/kaal/version.rbs +3 -0
  128. data/sig/kaal.rbs +145 -0
  129. metadata +100 -3
  130. data/config/kaal.rb +0 -15
  131. /data/config/{scheduler.yml → kaal-scheduler.yml} +0 -0
@@ -32,6 +32,10 @@ module Kaal
32
32
  @definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
33
33
  end
34
34
 
35
+ def delayed_store
36
+ @delayed_store ||= Kaal::DelayedJob::DatabaseEngine.new(database: @database.connection, use_skip_locked: true)
37
+ end
38
+
35
39
  def acquire(key, _ttl)
36
40
  acquired = scalar('SELECT pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
37
41
  log_dispatch_attempt(key) if acquired
@@ -8,5 +8,6 @@ require 'kaal/internal/sequel/database_backend'
8
8
  require 'kaal/internal/sequel/postgres_backend'
9
9
  require 'kaal/internal/sequel/mysql_backend'
10
10
  require 'kaal/definition/database_engine'
11
+ require 'kaal/delayed_job/database_engine'
11
12
  require 'kaal/dispatch/database_engine'
12
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
@@ -30,6 +30,10 @@ module Kaal
30
30
  def locks_dataset
31
31
  connection[:kaal_locks]
32
32
  end
33
+
34
+ def delayed_jobs_dataset
35
+ connection[:kaal_delayed_jobs]
36
+ end
33
37
  end
34
38
  end
35
39
  end
@@ -11,17 +11,21 @@ module Kaal
11
11
  module_function
12
12
 
13
13
  def for_backend(backend)
14
- case backend.to_s
14
+ backend_name = backend.to_s
15
+
16
+ case backend_name
15
17
  when 'sqlite'
16
18
  {
17
19
  '001_create_kaal_dispatches.rb' => dispatches_template,
18
20
  '002_create_kaal_locks.rb' => locks_template,
19
- '003_create_kaal_definitions.rb' => definitions_template
21
+ '003_create_kaal_definitions.rb' => definitions_template,
22
+ '004_create_kaal_delayed_jobs.rb' => delayed_jobs_template('sqlite')
20
23
  }
21
24
  when 'postgres', 'mysql'
22
25
  {
23
26
  '001_create_kaal_dispatches.rb' => dispatches_template,
24
- '002_create_kaal_definitions.rb' => definitions_template
27
+ '002_create_kaal_definitions.rb' => definitions_template,
28
+ '003_create_kaal_delayed_jobs.rb' => delayed_jobs_template(backend_name)
25
29
  }
26
30
  else
27
31
  {}
@@ -92,6 +96,34 @@ module Kaal
92
96
  end
93
97
  RUBY
94
98
  end
99
+
100
+ def delayed_jobs_template(backend)
101
+ args_definition =
102
+ if backend == 'mysql'
103
+ 'String :args, text: true, null: false'
104
+ else
105
+ "String :args, text: true, null: false, default: '[]'"
106
+ end
107
+
108
+ <<~RUBY
109
+ Sequel.migration do
110
+ change do
111
+ create_table?(:kaal_delayed_jobs) do
112
+ primary_key :id
113
+ String :job_id, null: false
114
+ Time :run_at, null: false
115
+ String :job_class, null: false
116
+ #{args_definition}
117
+ String :queue
118
+ Time :created_at, null: false
119
+ end
120
+
121
+ add_index :kaal_delayed_jobs, :job_id, unique: true
122
+ add_index :kaal_delayed_jobs, :run_at
123
+ end
124
+ end
125
+ RUBY
126
+ end
95
127
  end
96
128
  end
97
129
  end
@@ -5,7 +5,7 @@
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
7
  module Kaal
8
- # Loads scheduler.yml at framework boot time while respecting missing-file policy.
8
+ # Loads kaal-scheduler.yml at framework boot time while respecting missing-file policy.
9
9
  class SchedulerBootLoader
10
10
  def initialize(configuration_provider:, logger:, runtime_context:, load_scheduler_file:)
11
11
  @configuration_provider = configuration_provider
@@ -22,6 +22,8 @@ module Kaal
22
22
  configuration = fetch_configuration
23
23
  return unless configuration
24
24
 
25
+ Kaal.warn_on_risky_configuration!(configuration:, logger: @logger)
26
+
25
27
  return load_scheduler_file if configuration.scheduler_missing_file_policy == :error
26
28
 
27
29
  scheduler_path = configuration.scheduler_config_path.to_s.strip
@@ -77,7 +77,12 @@ module Kaal
77
77
  end
78
78
 
79
79
  def resolved_job_class(job_class_name:, key:, queue: nil)
80
- resolve_job_class(job_class_name:, key:, queue:)
80
+ Kaal::JobDispatcher.resolve_job_class(
81
+ job_class_name:,
82
+ key:,
83
+ queue:,
84
+ apply_delayed_job_allow_list: false
85
+ )
81
86
  end
82
87
 
83
88
  def conflict?(key:, existing_definition:)
@@ -157,7 +162,7 @@ module Kaal
157
162
  validate_keyword_keys(raw_kwargs, key)
158
163
 
159
164
  resolved_kwargs = raw_kwargs.transform_keys(&:to_sym)
160
- dispatch_job(job_class, queue, resolved_args, resolved_kwargs)
165
+ dispatch_job(job_class, queue, resolved_args, resolved_kwargs, key)
161
166
  end
162
167
  end
163
168
 
@@ -178,64 +183,34 @@ module Kaal
178
183
  nil
179
184
  end
180
185
 
181
- def resolve_job_class(job_class_name:, key:, queue: nil)
182
- normalized_job_class_name = job_class_name.to_s.strip
183
- raise SchedulerConfigError, "Job class cannot be blank for key '#{key}'" if normalized_job_class_name.empty?
184
-
185
- error_message = "Unknown job_class #{normalized_job_class_name.inspect} for key '#{key}'"
186
- job_class = begin
187
- Kaal::Support::HashTools.constantize(normalized_job_class_name)
188
- rescue NameError
189
- nil
190
- end
191
-
192
- return validate_dispatch_interface(job_class, key, queue) if job_class
193
-
194
- raise_unknown_job_class(error_message)
195
- end
196
-
197
- private :build_callback, :resolve_job_class
198
-
199
- def dispatch_job(job_class, queue, args, kwargs)
200
- job_class_name = job_class.name
201
-
202
- if queue && !job_class.respond_to?(:set)
203
- raise SchedulerConfigError,
204
- "job_class '#{job_class_name}' must respond to .set to use queue #{queue.inspect}"
205
- end
186
+ private :build_callback
206
187
 
207
- if queue
208
- job_class.set(queue: queue).perform_later(*args, **kwargs)
209
- elsif job_class.respond_to?(:perform_later)
210
- job_class.perform_later(*args, **kwargs)
211
- elsif job_class.respond_to?(:perform)
212
- job_class.perform(*args, **kwargs)
188
+ def dispatch_job(job_class, queue, args, kwargs, key)
189
+ if kwargs.empty?
190
+ Kaal::JobDispatcher.dispatch(job_class:, queue:, args:, key:)
213
191
  else
214
- raise SchedulerConfigError,
215
- "job_class '#{job_class_name}' must respond to .perform, .perform_later, or .set(...).perform_later"
216
- end
217
- end
218
-
219
- def raise_unknown_job_class(error_message)
220
- raise SchedulerConfigError, error_message
221
- end
222
-
223
- def validate_dispatch_interface(job_class, key, queue)
224
- queue_present = !queue.nil?
225
- supports_set = job_class.respond_to?(:set)
226
- supports_perform_later = job_class.respond_to?(:perform_later)
227
- supports_perform = job_class.respond_to?(:perform)
192
+ job_class_name = job_class.name
228
193
 
229
- return job_class if queue_present && supports_set
230
- return job_class if !queue_present && supports_perform_later
231
- return job_class if !queue_present && supports_perform
194
+ if queue && !job_class.respond_to?(:set)
195
+ raise SchedulerConfigError,
196
+ "job_class '#{job_class_name}' must respond to .set to use queue #{queue.inspect} for scheduler job '#{key}'"
197
+ end
232
198
 
233
- raise SchedulerConfigError,
234
- "job_class '#{job_class.name}' for key '#{key}' must respond to .perform, .perform_later, or .set(...).perform_later"
199
+ if queue
200
+ job_class.set(queue: queue).perform_later(*args, **kwargs)
201
+ elsif job_class.respond_to?(:perform_later)
202
+ job_class.perform_later(*args, **kwargs)
203
+ elsif job_class.respond_to?(:perform)
204
+ job_class.perform(*args, **kwargs)
205
+ else
206
+ raise SchedulerConfigError,
207
+ "job_class '#{job_class_name}' must respond to .perform, .perform_later, or .set(...).perform_later for scheduler job '#{key}'"
208
+ end
209
+ end
235
210
  end
236
211
 
237
212
  def active_job_dispatch?(job_class, queue)
238
- (queue && job_class.respond_to?(:set)) || job_class.respond_to?(:perform_later)
213
+ Kaal::JobDispatcher.active_job_dispatch?(job_class, queue)
239
214
  end
240
215
  end
241
216
  end
@@ -14,7 +14,7 @@ require_relative 'job_normalizer'
14
14
  require_relative 'job_applier'
15
15
 
16
16
  module Kaal
17
- # Loads scheduler definitions from config/scheduler.yml and registers them.
17
+ # Loads scheduler definitions from config/kaal-scheduler.yml and registers them.
18
18
  class SchedulerFileLoader
19
19
  include SchedulerHashTransform
20
20
  include SchedulerPlaceholderSupport
@@ -62,9 +62,9 @@ module Kaal
62
62
  end
63
63
 
64
64
  def migration_suffixes_for(backend)
65
- return %w[dispatches locks definitions] if backend.to_s == 'sqlite'
65
+ return %w[dispatches locks definitions delayed_jobs] if backend.to_s == 'sqlite'
66
66
 
67
- %w[dispatches definitions]
67
+ %w[dispatches definitions delayed_jobs]
68
68
  end
69
69
 
70
70
  def timestamp(offset = 0)
data/lib/kaal/version.rb CHANGED
@@ -5,5 +5,5 @@
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
7
  module Kaal
8
- VERSION = '0.4.0'
8
+ VERSION = '0.6.0'
9
9
  end
data/lib/kaal.rb CHANGED
@@ -10,6 +10,11 @@ require 'kaal/registry'
10
10
  require 'kaal/dispatch/registry'
11
11
  require 'kaal/dispatch/memory_engine'
12
12
  require 'kaal/dispatch/redis_engine'
13
+ require 'kaal/delayed_job/registry'
14
+ require 'kaal/delayed_job/memory_engine'
15
+ require 'kaal/delayed_job/redis_engine'
16
+ require 'kaal/delayed_job/dispatch_failure_logger'
17
+ require 'kaal/delayed_job/mysql_version_support'
13
18
  require 'kaal/definition/registry'
14
19
  require 'kaal/definition/memory_engine'
15
20
  require 'kaal/definition/redis_engine'
@@ -25,6 +30,7 @@ require 'kaal/persistence/migration_templates'
25
30
  require 'kaal/sequel_support'
26
31
  require 'kaal/active_record_support'
27
32
  require 'kaal/utils'
33
+ require 'kaal/job_dispatcher'
28
34
  require 'kaal/register_conflict_support'
29
35
  require 'kaal/definitions/registry_accessor'
30
36
  require 'kaal/definitions/registration_service'
@@ -56,12 +62,15 @@ module Kaal
56
62
  @definition_registry = nil
57
63
  @definitions_registry_accessor = nil
58
64
  @dispatch_registry_accessor = nil
65
+ @risky_configuration_warnings_emitted = {}
59
66
  end
60
67
 
61
68
  def reset_registry!
62
69
  @registry = Registry.new
63
70
  definition_registry = @definition_registry
64
71
  definition_registry.clear if definition_registry.respond_to?(:clear)
72
+ delayed_store = configuration.backend&.delayed_store
73
+ delayed_store.clear if delayed_store.respond_to?(:clear)
65
74
  @coordinator = nil
66
75
  end
67
76
 
@@ -79,11 +88,19 @@ module Kaal
79
88
  yield(configuration) if block_given?
80
89
  end
81
90
 
91
+ def load_config_file!(path: 'config/kaal.yml', runtime_context: RuntimeContext.default)
92
+ Config::FileLoader.new(
93
+ configuration: configuration,
94
+ runtime_context:
95
+ ).load(path:)
96
+ end
97
+
82
98
  def register(key:, cron:, enqueue:)
83
99
  registration_service.call(key:, cron:, enqueue:)
84
100
  end
85
101
 
86
102
  def load_scheduler_file!(runtime_context: RuntimeContext.default)
103
+ warn_on_risky_configuration!
87
104
  SchedulerFileLoader.new(
88
105
  configuration: configuration,
89
106
  definition_registry: definition_registry,
@@ -119,6 +136,7 @@ module Kaal
119
136
  end
120
137
 
121
138
  def start!
139
+ warn_on_risky_configuration!
122
140
  coordinator.start!
123
141
  end
124
142
 
@@ -138,6 +156,30 @@ module Kaal
138
156
  coordinator.tick!
139
157
  end
140
158
 
159
+ # Enqueue a one-off delayed job. Delivery is at-most-once after claim.
160
+ def enqueue_at(at:, job_class:, args:, job_id:, queue: nil, connection: nil)
161
+ delayed_store = delayed_store!
162
+ resolved_run_at = normalize_delayed_run_at(at)
163
+ resolved_args = normalize_delayed_args(args)
164
+ resolved_queue = normalize_delayed_queue(queue)
165
+ resolved_job_id = normalize_delayed_job_id(job_id)
166
+ resolved_job_class = JobDispatcher.resolve_job_class(
167
+ job_class_name: job_class,
168
+ key: resolved_job_id,
169
+ queue: resolved_queue
170
+ )
171
+ resolved_job_class_name = JobDispatcher.normalize_job_class_name(resolved_job_class)
172
+
173
+ delayed_store.enqueue(
174
+ job_id: resolved_job_id,
175
+ run_at: resolved_run_at,
176
+ job_class: resolved_job_class_name,
177
+ args: resolved_args,
178
+ queue: resolved_queue,
179
+ connection: connection
180
+ )
181
+ end
182
+
141
183
  def with_idempotency(key, fire_time)
142
184
  raise ArgumentError, 'block required' unless block_given?
143
185
 
@@ -193,6 +235,14 @@ module Kaal
193
235
  configuration.time_zone = value
194
236
  end
195
237
 
238
+ def delayed_job_allowed_class_prefixes
239
+ configuration.delayed_job_allowed_class_prefixes
240
+ end
241
+
242
+ def delayed_job_allowed_class_prefixes=(value)
243
+ configuration.delayed_job_allowed_class_prefixes = value
244
+ end
245
+
196
246
  def definition_registry
197
247
  definitions_registry_accessor.call
198
248
  end
@@ -205,6 +255,25 @@ module Kaal
205
255
  configuration.validate!
206
256
  end
207
257
 
258
+ def validation_warnings
259
+ configuration.validation_warnings
260
+ end
261
+
262
+ def warn_on_risky_configuration!(configuration: self.configuration, logger: configuration.logger)
263
+ warnings = configuration.validation_warnings
264
+ return [] if warnings.empty?
265
+
266
+ @risky_configuration_warnings_emitted ||= {}
267
+ warnings.each do |warning|
268
+ next if @risky_configuration_warnings_emitted[warning]
269
+
270
+ logger&.warn(warning)
271
+ @risky_configuration_warnings_emitted[warning] = true
272
+ end
273
+
274
+ warnings
275
+ end
276
+
208
277
  def valid?(expression)
209
278
  CronUtils.valid?(expression)
210
279
  end
@@ -253,5 +322,54 @@ module Kaal
253
322
  def dispatch_registry_accessor
254
323
  @dispatch_registry_accessor ||= Backend::DispatchRegistryAccessor.new(configuration: configuration)
255
324
  end
325
+
326
+ def delayed_store!
327
+ delayed_store = configuration.backend&.delayed_store
328
+ return delayed_store if delayed_store
329
+
330
+ raise ArgumentError, 'Configured backend does not support delayed jobs'
331
+ end
332
+
333
+ def normalize_delayed_run_at(at)
334
+ time = at.is_a?(Time) ? at : at&.to_time
335
+ ensure_delayed_time!(time)
336
+
337
+ time.utc
338
+ rescue NoMethodError
339
+ ensure_delayed_time!(nil)
340
+ end
341
+
342
+ def normalize_delayed_args(args)
343
+ raise ArgumentError, 'args must be an array' unless args.is_a?(Array)
344
+
345
+ args.dup
346
+ end
347
+
348
+ def normalize_delayed_queue(queue)
349
+ case queue
350
+ when nil
351
+ return nil
352
+ end
353
+
354
+ normalized_queue = queue.to_s.strip
355
+ raise ArgumentError, 'queue cannot be blank' if normalized_queue.empty?
356
+
357
+ normalized_queue
358
+ end
359
+
360
+ def normalize_delayed_job_id(job_id)
361
+ normalized_job_id = job_id.to_s.strip
362
+ raise ArgumentError, 'job_id cannot be blank' if normalized_job_id.empty?
363
+
364
+ normalized_job_id
365
+ end
366
+
367
+ def invalid_delayed_time_error
368
+ ArgumentError.new('at must be a Time or time-like value')
369
+ end
370
+
371
+ def ensure_delayed_time!(time)
372
+ raise invalid_delayed_time_error unless time
373
+ end
256
374
  end
257
375
  end
data/sig/00_types.rbs ADDED
@@ -0,0 +1,12 @@
1
+ module Kaal
2
+ interface _RBSOpaque
3
+ end
4
+
5
+ interface _RBSCallable
6
+ def call: (*rbs_any args, **rbs_any kwargs) -> rbs_any
7
+ end
8
+
9
+ type rbs_scalar = nil | bool | Integer | Float | Rational | String | Symbol | Time
10
+ type rbs_hash_key = String | Symbol | Integer
11
+ type rbs_any = rbs_scalar | _RBSOpaque | _RBSCallable | Array[rbs_any] | Hash[rbs_hash_key, rbs_any]
12
+ end
@@ -0,0 +1,49 @@
1
+ class Thor
2
+ class Error < StandardError
3
+ end
4
+
5
+ class Base
6
+ module Shell
7
+ def self.new: () -> Kaal::_RBSOpaque
8
+ end
9
+
10
+ def self.shell: () -> singleton(Shell)
11
+ end
12
+
13
+ def self.package_name: (String name) -> void
14
+ def self.class_option: (*Kaal::rbs_any args, **Kaal::rbs_any kwargs) -> void
15
+ def self.desc: (String usage, String description) -> void
16
+ def self.option: (*Kaal::rbs_any args, **Kaal::rbs_any kwargs) -> void
17
+ def self.no_commands: () { () -> Kaal::rbs_any } -> void
18
+ end
19
+
20
+ class Redis
21
+ def initialize: (*Kaal::rbs_any args, **Kaal::rbs_any kwargs) -> void
22
+ end
23
+
24
+ module Sequel
25
+ class Database
26
+ def connection: () -> Kaal::_RBSOpaque
27
+ def database_type: () -> String
28
+ def adapter_scheme: () -> String
29
+ end
30
+
31
+ def self.connect: (*Kaal::rbs_any args, **Kaal::rbs_any kwargs) -> Database
32
+ end
33
+
34
+ module ActiveSupport
35
+ module Inflector
36
+ def self.underscore: (String value) -> String
37
+ end
38
+ end
39
+
40
+ module ActiveRecord
41
+ class ConnectionNotEstablished < StandardError
42
+ end
43
+
44
+ class RecordNotUnique < StandardError
45
+ end
46
+
47
+ class Base
48
+ end
49
+ end
@@ -0,0 +1,23 @@
1
+ module Kaal
2
+ module ActiveRecord
3
+ def self.install_postgres_migration: (target_dir: Kaal::rbs_any, ?migration_name: ::String) -> Kaal::rbs_any
4
+
5
+ def self.install_mysql_migration: (target_dir: Kaal::rbs_any, ?migration_name: ::String) -> Kaal::rbs_any
6
+
7
+ def self.install_sqlite_migration: (target_dir: Kaal::rbs_any, ?migration_name: ::String) -> Kaal::rbs_any
8
+
9
+ def self.install_migrations: (target_dir: Kaal::rbs_any, backend: Kaal::rbs_any, ?migration_name: Kaal::rbs_any?, ?time_source: Kaal::rbs_any) -> Kaal::rbs_any
10
+
11
+ def self.require_activerecord!: () -> Kaal::rbs_any
12
+
13
+ def self.normalize_migration_name: (Kaal::rbs_any name, fallback: Kaal::rbs_any) -> Kaal::rbs_any
14
+
15
+ def self.underscore: (Kaal::rbs_any value) -> Kaal::rbs_any
16
+
17
+ def self.default_migration_class_for: (Kaal::rbs_any backend) -> ::String
18
+
19
+ def self.migration_suffixes_for: (Kaal::rbs_any backend) -> (::Array["dispatches" | "locks" | "definitions" | "delayed_jobs"] | ::Array["dispatches" | "definitions" | "delayed_jobs"])
20
+
21
+ def self.alphanumeric?: (Kaal::rbs_any char) -> Kaal::rbs_any
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module Kaal
2
+ module Backend
3
+ class Adapter
4
+ def acquire: (Kaal::rbs_any _key, Kaal::rbs_any _ttl) -> Kaal::rbs_any
5
+
6
+ def release: (Kaal::rbs_any _key) -> Kaal::rbs_any
7
+
8
+ def with_lock: (Kaal::rbs_any key, ttl: Kaal::rbs_any) { () -> Kaal::rbs_any } -> (nil | Kaal::rbs_any)
9
+
10
+ def definition_registry: () -> nil
11
+
12
+ def delayed_store: () -> nil
13
+ end
14
+
15
+ class NullAdapter < Adapter
16
+ def acquire: (Kaal::rbs_any _key, Kaal::rbs_any _ttl) -> true
17
+
18
+ def release: (Kaal::rbs_any _key) -> true
19
+
20
+ def with_lock: (Kaal::rbs_any _key, **Kaal::rbs_any) { () -> Kaal::rbs_any } -> Kaal::rbs_any
21
+ end
22
+
23
+ class LockAdapterError < StandardError
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ module Kaal
2
+ module Backend
3
+ class DispatchAttemptLogger
4
+ @configuration: Kaal::rbs_any
5
+
6
+ @dispatch_registry_provider: Kaal::rbs_any
7
+
8
+ @logger: Kaal::rbs_any
9
+
10
+ @node_id_provider: Kaal::rbs_any
11
+
12
+ def initialize: (configuration: Kaal::rbs_any, dispatch_registry_provider: Kaal::rbs_any, ?logger: Kaal::rbs_any?, ?node_id_provider: Kaal::rbs_any) -> void
13
+
14
+ def call: (Kaal::rbs_any lock_key) -> Kaal::rbs_any
15
+ end
16
+ end
17
+ end