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
data/lib/kaal.rb
CHANGED
|
@@ -4,224 +4,100 @@
|
|
|
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
|
require 'kaal/version'
|
|
9
|
-
require 'kaal/
|
|
8
|
+
require 'kaal/config'
|
|
10
9
|
require 'kaal/registry'
|
|
11
10
|
require 'kaal/dispatch/registry'
|
|
12
11
|
require 'kaal/dispatch/memory_engine'
|
|
13
12
|
require 'kaal/dispatch/redis_engine'
|
|
14
|
-
require 'kaal/dispatch/database_engine'
|
|
15
13
|
require 'kaal/definition/registry'
|
|
16
14
|
require 'kaal/definition/memory_engine'
|
|
17
15
|
require 'kaal/definition/redis_engine'
|
|
18
|
-
require 'kaal/definition/database_engine'
|
|
19
16
|
require 'kaal/backend/adapter'
|
|
20
17
|
require 'kaal/backend/memory_adapter'
|
|
21
18
|
require 'kaal/backend/redis_adapter'
|
|
22
|
-
require 'kaal/backend/
|
|
23
|
-
require 'kaal/backend/
|
|
24
|
-
require 'kaal/backend/
|
|
25
|
-
require 'kaal/
|
|
26
|
-
require 'kaal/
|
|
27
|
-
require 'kaal/
|
|
28
|
-
require 'kaal/
|
|
29
|
-
require 'kaal/
|
|
19
|
+
require 'kaal/backend/sqlite'
|
|
20
|
+
require 'kaal/backend/postgres'
|
|
21
|
+
require 'kaal/backend/mysql'
|
|
22
|
+
require 'kaal/backend/dispatch_registry_accessor'
|
|
23
|
+
require 'kaal/backend/dispatch_attempt_logger'
|
|
24
|
+
require 'kaal/persistence/migration_templates'
|
|
25
|
+
require 'kaal/sequel_support'
|
|
26
|
+
require 'kaal/active_record_support'
|
|
27
|
+
require 'kaal/utils'
|
|
30
28
|
require 'kaal/register_conflict_support'
|
|
31
|
-
require 'kaal/
|
|
32
|
-
require 'kaal/
|
|
33
|
-
require 'kaal/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
# @example Configure Kaal
|
|
40
|
-
# Kaal.configure do |config|
|
|
41
|
-
# config.tick_interval = 5
|
|
42
|
-
# config.backend = Kaal::Backend::RedisAdapter.new(Redis.new(url: ENV["REDIS_URL"]))
|
|
43
|
-
# end
|
|
44
|
-
#
|
|
45
|
-
# @example Register a cron job
|
|
46
|
-
# Kaal.register(
|
|
47
|
-
# key: "reports:daily",
|
|
48
|
-
# cron: "0 9 * * *",
|
|
49
|
-
# enqueue: ->(fire_time:, idempotency_key:) { MyJob.perform_later }
|
|
50
|
-
# )
|
|
29
|
+
require 'kaal/definitions/registry_accessor'
|
|
30
|
+
require 'kaal/definitions/registration_service'
|
|
31
|
+
require 'kaal/runtime'
|
|
32
|
+
require 'kaal/scheduler_file'
|
|
33
|
+
require 'kaal/core'
|
|
34
|
+
require 'kaal/support/hash_tools'
|
|
35
|
+
|
|
36
|
+
# Plain-Ruby scheduler surface with configurable backends, registries, and CLI helpers.
|
|
51
37
|
module Kaal
|
|
52
38
|
class << self
|
|
53
39
|
include RegisterConflictSupport
|
|
54
40
|
|
|
55
|
-
##
|
|
56
|
-
# Get the current configuration instance.
|
|
57
|
-
#
|
|
58
|
-
# @return [Configuration] the global configuration object
|
|
59
41
|
def configuration
|
|
60
42
|
@configuration ||= Configuration.new
|
|
61
43
|
end
|
|
62
44
|
|
|
63
|
-
##
|
|
64
|
-
# Get the current registry instance.
|
|
65
|
-
#
|
|
66
|
-
# @return [Registry] the global registry object
|
|
67
45
|
def registry
|
|
68
46
|
@registry ||= Registry.new
|
|
69
47
|
end
|
|
70
48
|
|
|
71
|
-
##
|
|
72
|
-
# Get the coordinator instance.
|
|
73
|
-
#
|
|
74
|
-
# @return [Coordinator] the global coordinator object
|
|
75
49
|
def coordinator
|
|
76
50
|
@coordinator ||= Coordinator.new(configuration: configuration, registry: registry)
|
|
77
51
|
end
|
|
78
52
|
|
|
79
|
-
##
|
|
80
|
-
# Reset configuration to defaults. Primarily used in tests.
|
|
81
|
-
#
|
|
82
|
-
# @return [Configuration] a fresh configuration object
|
|
83
53
|
def reset_configuration!
|
|
84
54
|
@configuration = Configuration.new
|
|
85
|
-
@coordinator = nil
|
|
55
|
+
@coordinator = nil
|
|
86
56
|
@definition_registry = nil
|
|
57
|
+
@definitions_registry_accessor = nil
|
|
58
|
+
@dispatch_registry_accessor = nil
|
|
87
59
|
end
|
|
88
60
|
|
|
89
|
-
##
|
|
90
|
-
# Reset registry to empty state. Primarily used in tests.
|
|
91
|
-
#
|
|
92
|
-
# @return [Registry] a fresh registry object
|
|
93
61
|
def reset_registry!
|
|
94
62
|
@registry = Registry.new
|
|
95
|
-
@definition_registry
|
|
96
|
-
|
|
63
|
+
definition_registry = @definition_registry
|
|
64
|
+
definition_registry.clear if definition_registry.respond_to?(:clear)
|
|
65
|
+
@coordinator = nil
|
|
97
66
|
end
|
|
98
67
|
|
|
99
|
-
##
|
|
100
|
-
# Reset coordinator to initial state. Primarily used in tests.
|
|
101
|
-
#
|
|
102
|
-
# Stops any running coordinator and creates a fresh instance.
|
|
103
|
-
#
|
|
104
|
-
# @return [Coordinator] a fresh coordinator object
|
|
105
|
-
# @raise [RuntimeError] if the running coordinator cannot be stopped within timeout
|
|
106
68
|
def reset_coordinator!
|
|
107
|
-
# Stop the existing coordinator if it's running
|
|
108
69
|
if @coordinator&.running?
|
|
109
70
|
stopped = @coordinator.stop!
|
|
110
71
|
raise 'Failed to stop coordinator thread within timeout' unless stopped
|
|
111
72
|
end
|
|
112
73
|
|
|
113
|
-
# Create and return a fresh coordinator
|
|
114
74
|
@coordinator = nil
|
|
115
75
|
coordinator
|
|
116
76
|
end
|
|
117
77
|
|
|
118
|
-
##
|
|
119
|
-
# Configure Kaal via a block.
|
|
120
|
-
#
|
|
121
|
-
# @yield [config] yields the configuration object
|
|
122
|
-
# @yieldparam config [Configuration] the configuration instance to customize
|
|
123
|
-
# @return [void]
|
|
124
|
-
#
|
|
125
|
-
# @example
|
|
126
|
-
# Kaal.configure do |config|
|
|
127
|
-
# config.tick_interval = 10
|
|
128
|
-
# config.lease_ttl = 120
|
|
129
|
-
# end
|
|
130
78
|
def configure
|
|
131
79
|
yield(configuration) if block_given?
|
|
132
80
|
end
|
|
133
81
|
|
|
134
|
-
##
|
|
135
|
-
# Register a new cron job.
|
|
136
|
-
#
|
|
137
|
-
# @param key [String] unique identifier for the cron task
|
|
138
|
-
# @param cron [String] cron expression (e.g., "0 9 * * *", "@daily")
|
|
139
|
-
# @param enqueue [Proc, Lambda] callable executed when cron fires
|
|
140
|
-
# @return [Registry::Entry] the registered entry
|
|
141
|
-
#
|
|
142
|
-
# @raise [ArgumentError] if parameters are invalid
|
|
143
|
-
# @raise [RegistryError] if key is already registered
|
|
144
|
-
#
|
|
145
|
-
# @example
|
|
146
|
-
# Kaal.register(
|
|
147
|
-
# key: "reports:weekly_summary",
|
|
148
|
-
# cron: "0 9 * * 1",
|
|
149
|
-
# enqueue: ->(fire_time:, idempotency_key:) { WeeklySummaryJob.perform_later }
|
|
150
|
-
# )
|
|
151
82
|
def register(key:, cron:, enqueue:)
|
|
152
|
-
|
|
153
|
-
existing_entry = registry.find(key)
|
|
154
|
-
if existing_entry
|
|
155
|
-
conflict_result = resolve_register_conflict(
|
|
156
|
-
key: key,
|
|
157
|
-
cron: cron,
|
|
158
|
-
enqueue: enqueue,
|
|
159
|
-
existing_definition: existing_definition,
|
|
160
|
-
existing_entry: existing_entry
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
return conflict_result if conflict_result
|
|
164
|
-
|
|
165
|
-
raise RegistryError, "Key '#{key}' is already registered"
|
|
166
|
-
end
|
|
167
|
-
persisted_attributes = {
|
|
168
|
-
enabled: true,
|
|
169
|
-
source: 'code',
|
|
170
|
-
metadata: {}
|
|
171
|
-
}.merge(existing_definition&.slice(:enabled, :metadata) || {})
|
|
172
|
-
definition_registry.upsert_definition(key: key, cron: cron, **persisted_attributes)
|
|
173
|
-
with_registered_definition_rollback(key, existing_definition) do
|
|
174
|
-
registry.add(key: key, cron: cron, enqueue: enqueue)
|
|
175
|
-
end
|
|
83
|
+
registration_service.call(key:, cron:, enqueue:)
|
|
176
84
|
end
|
|
177
85
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
#
|
|
181
|
-
# @return [Array<Hash>] normalized jobs applied from scheduler file
|
|
182
|
-
# @raise [SchedulerConfigError] if scheduler file is invalid
|
|
183
|
-
def load_scheduler_file!
|
|
184
|
-
loader = SchedulerFileLoader.new(
|
|
86
|
+
def load_scheduler_file!(runtime_context: RuntimeContext.default)
|
|
87
|
+
SchedulerFileLoader.new(
|
|
185
88
|
configuration: configuration,
|
|
186
89
|
definition_registry: definition_registry,
|
|
187
90
|
registry: registry,
|
|
188
|
-
logger: configuration.logger
|
|
189
|
-
|
|
190
|
-
|
|
91
|
+
logger: configuration.logger,
|
|
92
|
+
runtime_context: runtime_context
|
|
93
|
+
).load
|
|
191
94
|
end
|
|
192
95
|
|
|
193
|
-
##
|
|
194
|
-
# Unregister (remove) a cron job by key.
|
|
195
|
-
#
|
|
196
|
-
# @param key [String] the unique identifier of the job to remove
|
|
197
|
-
# @return [Registry::Entry, nil] the removed entry, or nil if not found
|
|
198
96
|
def unregister(key:)
|
|
199
97
|
definition_registry.remove_definition(key)
|
|
200
98
|
registry.remove(key)
|
|
201
99
|
end
|
|
202
100
|
|
|
203
|
-
def rollback_registered_definition(key, existing_definition)
|
|
204
|
-
if existing_definition
|
|
205
|
-
definition_registry.upsert_definition(
|
|
206
|
-
key: existing_definition[:key],
|
|
207
|
-
cron: existing_definition[:cron],
|
|
208
|
-
enabled: existing_definition[:enabled],
|
|
209
|
-
source: existing_definition[:source],
|
|
210
|
-
metadata: existing_definition[:metadata]
|
|
211
|
-
)
|
|
212
|
-
elsif !registry.registered?(key)
|
|
213
|
-
definition_registry.remove_definition(key)
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
private :rollback_registered_definition
|
|
217
|
-
|
|
218
|
-
##
|
|
219
|
-
# Get all registered cron jobs.
|
|
220
|
-
#
|
|
221
|
-
# @return [Array<Registry::Entry>] array of all registered entries
|
|
222
|
-
#
|
|
223
|
-
# @example
|
|
224
|
-
# Kaal.registered.each { |entry| puts entry.key }
|
|
225
101
|
def registered
|
|
226
102
|
definition_registry.all_definitions.map do |definition|
|
|
227
103
|
key = definition[:key]
|
|
@@ -230,297 +106,97 @@ module Kaal
|
|
|
230
106
|
end
|
|
231
107
|
end
|
|
232
108
|
|
|
233
|
-
##
|
|
234
|
-
# Check if a cron job is registered by key.
|
|
235
|
-
#
|
|
236
|
-
# @param key [String] the unique identifier to check
|
|
237
|
-
# @return [Boolean] true if the key is registered, false otherwise
|
|
238
|
-
#
|
|
239
|
-
# @example
|
|
240
|
-
# Kaal.registered?(key: "reports:daily") # => true
|
|
241
109
|
def registered?(key:)
|
|
242
|
-
definition_registry.find_definition(key)
|
|
110
|
+
!!definition_registry.find_definition(key)
|
|
243
111
|
end
|
|
244
112
|
|
|
245
|
-
##
|
|
246
|
-
# Enable a registered cron definition by key.
|
|
247
|
-
#
|
|
248
|
-
# @param key [String] the unique identifier to enable
|
|
249
|
-
# @return [Hash, nil] enabled definition hash or nil if missing
|
|
250
113
|
def enable(key:)
|
|
251
114
|
definition_registry.enable_definition(key)
|
|
252
115
|
end
|
|
253
116
|
|
|
254
|
-
##
|
|
255
|
-
# Disable a registered cron definition by key.
|
|
256
|
-
#
|
|
257
|
-
# @param key [String] the unique identifier to disable
|
|
258
|
-
# @return [Hash, nil] disabled definition hash or nil if missing
|
|
259
117
|
def disable(key:)
|
|
260
118
|
definition_registry.disable_definition(key)
|
|
261
119
|
end
|
|
262
120
|
|
|
263
|
-
##
|
|
264
|
-
# Start the scheduler background thread.
|
|
265
|
-
#
|
|
266
|
-
# The coordinator will calculate due fire times for each registered cron
|
|
267
|
-
# on each tick and attempt to dispatch work.
|
|
268
|
-
#
|
|
269
|
-
# @return [Thread] the started thread, or nil if already running
|
|
270
|
-
#
|
|
271
|
-
# @example
|
|
272
|
-
# Kaal.start!
|
|
273
121
|
def start!
|
|
274
122
|
coordinator.start!
|
|
275
123
|
end
|
|
276
124
|
|
|
277
|
-
##
|
|
278
|
-
# Stop the scheduler gracefully.
|
|
279
|
-
#
|
|
280
|
-
# Signals the coordinator to stop after the current tick completes,
|
|
281
|
-
# then waits for the thread to finish.
|
|
282
|
-
#
|
|
283
|
-
# @param timeout [Integer] seconds to wait for graceful shutdown (default: 30)
|
|
284
|
-
# @return [Boolean] true if stopped successfully
|
|
285
|
-
#
|
|
286
|
-
# @example
|
|
287
|
-
# Kaal.stop!
|
|
288
|
-
# @example
|
|
289
|
-
# Kaal.stop!(timeout: 60)
|
|
290
125
|
def stop!(timeout: 30)
|
|
291
126
|
coordinator.stop!(timeout: timeout)
|
|
292
127
|
end
|
|
293
128
|
|
|
294
|
-
##
|
|
295
|
-
# Check if the scheduler is currently running.
|
|
296
|
-
#
|
|
297
|
-
# @return [Boolean] true if running, false otherwise
|
|
298
|
-
#
|
|
299
|
-
# @example
|
|
300
|
-
# if Kaal.running?
|
|
301
|
-
# puts "Scheduler is active"
|
|
302
|
-
# end
|
|
303
129
|
def running?
|
|
304
130
|
coordinator.running?
|
|
305
131
|
end
|
|
306
132
|
|
|
307
|
-
##
|
|
308
|
-
# Restart the scheduler (stop then start).
|
|
309
|
-
#
|
|
310
|
-
# @return [Thread] the started thread
|
|
311
|
-
#
|
|
312
|
-
# @example
|
|
313
|
-
# Kaal.restart!
|
|
314
133
|
def restart!
|
|
315
134
|
coordinator.restart!
|
|
316
135
|
end
|
|
317
136
|
|
|
318
|
-
##
|
|
319
|
-
# Execute a single scheduler tick manually.
|
|
320
|
-
#
|
|
321
|
-
# This is useful for testing and Rake tasks that want to trigger
|
|
322
|
-
# the scheduler without running the background loop.
|
|
323
|
-
#
|
|
324
|
-
# @return [void]
|
|
325
|
-
#
|
|
326
|
-
# @example
|
|
327
|
-
# Kaal.tick!
|
|
328
137
|
def tick!
|
|
329
138
|
coordinator.tick!
|
|
330
139
|
end
|
|
331
140
|
|
|
332
|
-
##
|
|
333
|
-
# Generate an idempotency key for a cron job and yield to a block.
|
|
334
|
-
#
|
|
335
|
-
# Useful for advanced use cases where you need to generate an idempotency key
|
|
336
|
-
# outside of the normal enqueue flow, or for internal utilities.
|
|
337
|
-
#
|
|
338
|
-
# @param key [String] the cron job key
|
|
339
|
-
# @param fire_time [Time] the fire time
|
|
340
|
-
# @yield [String] yields the generated idempotency key
|
|
341
|
-
# @return [Object] the result of the block
|
|
342
|
-
#
|
|
343
|
-
# @raise [ArgumentError] if no block is provided
|
|
344
|
-
#
|
|
345
|
-
# @example
|
|
346
|
-
# Kaal.with_idempotency('reports:daily', Time.current) do |idempotency_key|
|
|
347
|
-
# MyJob.perform_later(key: idempotency_key)
|
|
348
|
-
# end
|
|
349
141
|
def with_idempotency(key, fire_time)
|
|
350
142
|
raise ArgumentError, 'block required' unless block_given?
|
|
351
143
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
##
|
|
357
|
-
# Check if a cron job has already been dispatched for a given fire time.
|
|
358
|
-
#
|
|
359
|
-
# Useful for implementing deduplication logic to prevent duplicate job enqueues.
|
|
360
|
-
# Returns true if dispatch logging is enabled and the job was previously dispatched,
|
|
361
|
-
# returns false if not found or dispatch logging is disabled.
|
|
362
|
-
#
|
|
363
|
-
# Safe to call from enqueue callbacks - will return false on any error (e.g., backend
|
|
364
|
-
# misconfiguration or temporary failure), log via configuration.logger, and never raise.
|
|
365
|
-
#
|
|
366
|
-
# @param key [String] the cron job key
|
|
367
|
-
# @param fire_time [Time] the fire time to check
|
|
368
|
-
# @return [Boolean] true if dispatch exists, false otherwise (never raises)
|
|
369
|
-
#
|
|
370
|
-
# @example
|
|
371
|
-
# Kaal.dispatched?('reports:daily', Time.current)
|
|
372
|
-
# # => true if already dispatched, false otherwise
|
|
373
|
-
def dispatched?(key, fire_time)
|
|
374
|
-
adapter = configuration.backend
|
|
375
|
-
return false if adapter.nil? || !adapter.respond_to?(:dispatch_registry)
|
|
376
|
-
|
|
377
|
-
registry = adapter.dispatch_registry
|
|
378
|
-
return false if registry.nil?
|
|
379
|
-
|
|
380
|
-
registry.dispatched?(key, fire_time)
|
|
381
|
-
rescue StandardError => e
|
|
382
|
-
configuration.logger&.warn("Error checking dispatch status for #{key}: #{e.message}")
|
|
383
|
-
false
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
##
|
|
387
|
-
# Get the dispatch log registry for querying dispatch history.
|
|
388
|
-
#
|
|
389
|
-
# Returns the underlying dispatch registry engine which allows querying
|
|
390
|
-
# dispatch records. The specific methods available depend on the adapter type:
|
|
391
|
-
#
|
|
392
|
-
# **Common methods (all adapters):**
|
|
393
|
-
# - `find_dispatch(key, fire_time)` - Find a specific dispatch record
|
|
394
|
-
# - `dispatched?(key, fire_time)` - Check if a dispatch exists
|
|
395
|
-
#
|
|
396
|
-
# **Database adapter specific methods:**
|
|
397
|
-
# - `find_by_key(key)` - Find all dispatches for a job key
|
|
398
|
-
# - `find_by_node(node_id)` - Find all dispatches from a node, ordered by fire_time
|
|
399
|
-
# - `find_by_status(status)` - Find dispatches by status ('dispatched', 'failed', etc.)
|
|
400
|
-
# - `cleanup(recovery_window: 86400)` - Delete dispatch records older than window
|
|
401
|
-
#
|
|
402
|
-
# **Redis adapter:**
|
|
403
|
-
# - Uses automatic TTL expiration (no cleanup needed)
|
|
404
|
-
#
|
|
405
|
-
# **Memory adapter (development/testing):**
|
|
406
|
-
# - `clear()` - Clear all stored records
|
|
407
|
-
# - `size()` - Get count of stored records
|
|
408
|
-
#
|
|
409
|
-
# Safe for production diagnostics - will return nil on any error (e.g., backend
|
|
410
|
-
# misconfiguration or temporary failure), log via configuration.logger, and never raise.
|
|
411
|
-
#
|
|
412
|
-
# @return [Dispatch::Registry, nil] the dispatch registry instance, or nil if adapter doesn't support it or on error
|
|
413
|
-
#
|
|
414
|
-
# @example Query dispatches with database adapter
|
|
415
|
-
# registry = Kaal.dispatch_log_registry
|
|
416
|
-
# # Find all dispatches for a job
|
|
417
|
-
# registry.find_by_key('reports:daily')
|
|
418
|
-
# # Find failed attempts
|
|
419
|
-
# registry.find_by_status('failed')
|
|
420
|
-
# # Clean up old records (over 30 days old)
|
|
421
|
-
# registry.cleanup(recovery_window: 30 * 24 * 60 * 60)
|
|
422
|
-
#
|
|
423
|
-
# @example Query dispatches with memory adapter
|
|
424
|
-
# registry = Kaal.dispatch_log_registry
|
|
425
|
-
# record = registry.find_dispatch('reports:daily', Time.current)
|
|
426
|
-
# total = registry.size
|
|
427
|
-
# registry.clear
|
|
428
|
-
def dispatch_log_registry
|
|
429
|
-
adapter = configuration.backend
|
|
430
|
-
return nil if adapter.nil? || !adapter.respond_to?(:dispatch_registry)
|
|
431
|
-
|
|
432
|
-
registry = adapter.dispatch_registry
|
|
433
|
-
return nil if registry.nil?
|
|
144
|
+
yield(IdempotencyKeyGenerator.call(key, fire_time, configuration: configuration))
|
|
145
|
+
end
|
|
434
146
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
configuration.logger&.warn("Error accessing dispatch registry: #{e.message}")
|
|
438
|
-
nil
|
|
147
|
+
def dispatched?(key, fire_time)
|
|
148
|
+
dispatch_registry_accessor.dispatched?(key, fire_time)
|
|
439
149
|
end
|
|
440
150
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
def tick_interval
|
|
444
|
-
configuration.tick_interval
|
|
151
|
+
def dispatch_log_registry
|
|
152
|
+
dispatch_registry_accessor.registry
|
|
445
153
|
end
|
|
446
154
|
|
|
155
|
+
def tick_interval = configuration.tick_interval
|
|
156
|
+
def window_lookback = configuration.window_lookback
|
|
157
|
+
def window_lookahead = configuration.window_lookahead
|
|
158
|
+
def lease_ttl = configuration.lease_ttl
|
|
159
|
+
def namespace = configuration.namespace
|
|
160
|
+
def backend = configuration.backend
|
|
161
|
+
def logger = configuration.logger
|
|
162
|
+
def time_zone = configuration.time_zone
|
|
163
|
+
|
|
447
164
|
def tick_interval=(value)
|
|
448
165
|
configuration.tick_interval = value
|
|
449
166
|
end
|
|
450
167
|
|
|
451
|
-
def window_lookback
|
|
452
|
-
configuration.window_lookback
|
|
453
|
-
end
|
|
454
|
-
|
|
455
168
|
def window_lookback=(value)
|
|
456
169
|
configuration.window_lookback = value
|
|
457
170
|
end
|
|
458
171
|
|
|
459
|
-
def window_lookahead
|
|
460
|
-
configuration.window_lookahead
|
|
461
|
-
end
|
|
462
|
-
|
|
463
172
|
def window_lookahead=(value)
|
|
464
173
|
configuration.window_lookahead = value
|
|
465
174
|
end
|
|
466
175
|
|
|
467
|
-
def lease_ttl
|
|
468
|
-
configuration.lease_ttl
|
|
469
|
-
end
|
|
470
|
-
|
|
471
176
|
def lease_ttl=(value)
|
|
472
177
|
configuration.lease_ttl = value
|
|
473
178
|
end
|
|
474
179
|
|
|
475
|
-
def namespace
|
|
476
|
-
configuration.namespace
|
|
477
|
-
end
|
|
478
|
-
|
|
479
180
|
def namespace=(value)
|
|
480
181
|
configuration.namespace = value
|
|
481
182
|
end
|
|
482
183
|
|
|
483
|
-
def backend
|
|
484
|
-
configuration.backend
|
|
485
|
-
end
|
|
486
|
-
|
|
487
184
|
def backend=(value)
|
|
488
185
|
configuration.backend = value
|
|
489
186
|
end
|
|
490
187
|
|
|
491
|
-
##
|
|
492
|
-
# Definition registry access.
|
|
493
|
-
#
|
|
494
|
-
# Uses backend-provided definition registry when available, otherwise a
|
|
495
|
-
# process-local in-memory fallback.
|
|
496
|
-
#
|
|
497
|
-
# @return [Kaal::Definition::Registry]
|
|
498
|
-
def definition_registry
|
|
499
|
-
configured_backend = configuration.backend
|
|
500
|
-
registry = configured_backend&.definition_registry
|
|
501
|
-
return registry if registry
|
|
502
|
-
|
|
503
|
-
@definition_registry ||= Definition::MemoryEngine.new
|
|
504
|
-
rescue NoMethodError
|
|
505
|
-
@definition_registry ||= Definition::MemoryEngine.new
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
def logger
|
|
509
|
-
configuration.logger
|
|
510
|
-
end
|
|
511
|
-
|
|
512
188
|
def logger=(value)
|
|
513
189
|
configuration.logger = value
|
|
514
190
|
end
|
|
515
191
|
|
|
516
|
-
def time_zone
|
|
517
|
-
configuration.time_zone
|
|
518
|
-
end
|
|
519
|
-
|
|
520
192
|
def time_zone=(value)
|
|
521
193
|
configuration.time_zone = value
|
|
522
194
|
end
|
|
523
195
|
|
|
196
|
+
def definition_registry
|
|
197
|
+
definitions_registry_accessor.call
|
|
198
|
+
end
|
|
199
|
+
|
|
524
200
|
def validate
|
|
525
201
|
configuration.validate
|
|
526
202
|
end
|
|
@@ -529,43 +205,53 @@ module Kaal
|
|
|
529
205
|
configuration.validate!
|
|
530
206
|
end
|
|
531
207
|
|
|
532
|
-
##
|
|
533
|
-
# Validate a cron expression.
|
|
534
|
-
#
|
|
535
|
-
# @param expression [String] cron expression
|
|
536
|
-
# @return [Boolean] true if valid, false otherwise
|
|
537
208
|
def valid?(expression)
|
|
538
209
|
CronUtils.valid?(expression)
|
|
539
210
|
end
|
|
540
211
|
|
|
541
|
-
##
|
|
542
|
-
# Simplify a cron expression to a predefined macro when possible.
|
|
543
|
-
#
|
|
544
|
-
# @param expression [String] cron expression
|
|
545
|
-
# @return [String] simplified expression or canonical input
|
|
546
|
-
# @raise [ArgumentError] when expression is invalid
|
|
547
212
|
def simplify(expression)
|
|
548
213
|
CronUtils.simplify(expression)
|
|
549
214
|
end
|
|
550
215
|
|
|
551
|
-
##
|
|
552
|
-
# Lint a cron expression and return warnings/errors.
|
|
553
|
-
#
|
|
554
|
-
# @param expression [String] cron expression
|
|
555
|
-
# @return [Array<String>] lint warnings/errors
|
|
556
216
|
def lint(expression)
|
|
557
217
|
CronUtils.lint(expression)
|
|
558
218
|
end
|
|
559
219
|
|
|
560
|
-
##
|
|
561
|
-
# Convert a cron expression to a human-friendly phrase.
|
|
562
|
-
#
|
|
563
|
-
# @param expression [String] cron expression
|
|
564
|
-
# @param locale [Symbol, String, nil] locale override (defaults to current I18n.locale)
|
|
565
|
-
# @return [String] localized phrase
|
|
566
|
-
# @raise [ArgumentError] when expression is invalid
|
|
567
220
|
def to_human(expression, locale: nil)
|
|
568
221
|
CronHumanizer.to_human(expression, locale: locale)
|
|
569
222
|
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def rollback_registered_definition(key, existing_definition)
|
|
227
|
+
if existing_definition
|
|
228
|
+
definition_registry.upsert_definition(
|
|
229
|
+
**Definition::AttributeHelpers.definition_attributes(existing_definition), enabled: existing_definition[:enabled]
|
|
230
|
+
)
|
|
231
|
+
elsif !registry.registered?(key)
|
|
232
|
+
definition_registry.remove_definition(key)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def registration_service
|
|
237
|
+
@registration_service ||= Definitions::RegistrationService.new(
|
|
238
|
+
configuration: configuration,
|
|
239
|
+
definition_registry: definition_registry,
|
|
240
|
+
registry: registry
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def definitions_registry_accessor
|
|
245
|
+
@definitions_registry_accessor ||= Definitions::RegistryAccessor.new(
|
|
246
|
+
configuration: configuration,
|
|
247
|
+
fallback_registry_provider: lambda {
|
|
248
|
+
@definition_registry ||= Definition::MemoryEngine.new
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def dispatch_registry_accessor
|
|
254
|
+
@dispatch_registry_accessor ||= Backend::DispatchRegistryAccessor.new(configuration: configuration)
|
|
255
|
+
end
|
|
570
256
|
end
|
|
571
257
|
end
|