kaal 0.2.1 → 0.3.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -286
  3. data/Rakefile +4 -2
  4. data/config/kaal.rb +15 -0
  5. data/config/scheduler.yml +12 -0
  6. data/{lib/tasks/kaal_tasks.rake → exe/kaal} +5 -3
  7. data/lib/kaal/backend/adapter.rb +0 -1
  8. data/lib/kaal/backend/dispatch_attempt_logger.rb +33 -0
  9. data/lib/kaal/backend/dispatch_logging.rb +36 -23
  10. data/lib/kaal/backend/dispatch_registry_accessor.rb +43 -0
  11. data/lib/kaal/backend/memory_adapter.rb +7 -5
  12. data/lib/kaal/backend/redis_adapter.rb +6 -6
  13. data/lib/kaal/cli.rb +230 -0
  14. data/lib/kaal/{configuration.rb → config/configuration.rb} +0 -1
  15. data/lib/kaal/{scheduler_config_error.rb → config/scheduler_config_error.rb} +0 -1
  16. data/lib/kaal/config/scheduler_time_zone_resolver.rb +50 -0
  17. data/lib/kaal/config.rb +19 -0
  18. data/lib/kaal/{coordinator.rb → core/coordinator.rb} +42 -62
  19. data/lib/kaal/core/enabled_entry_enumerator.rb +51 -0
  20. data/lib/kaal/core/occurrence_finder.rb +38 -0
  21. data/lib/kaal/core.rb +18 -0
  22. data/lib/kaal/definition/memory_engine.rb +11 -18
  23. data/lib/kaal/definition/persistence_helpers.rb +31 -0
  24. data/lib/kaal/definition/redis_engine.rb +9 -6
  25. data/lib/kaal/definition/registry.rb +24 -2
  26. data/lib/kaal/definitions/registration_service.rb +62 -0
  27. data/lib/kaal/definitions/registry_accessor.rb +33 -0
  28. data/lib/kaal/dispatch/memory_engine.rb +3 -4
  29. data/lib/kaal/dispatch/redis_engine.rb +2 -3
  30. data/lib/kaal/dispatch/registry.rb +0 -1
  31. data/lib/kaal/register_conflict_support.rb +0 -1
  32. data/lib/kaal/registry.rb +0 -1
  33. data/lib/kaal/runtime/runtime_context.rb +41 -0
  34. data/lib/kaal/runtime/scheduler_boot_loader.rb +52 -0
  35. data/lib/kaal/runtime/signal_handler_chain.rb +42 -0
  36. data/lib/kaal/runtime/signal_handler_installer.rb +39 -0
  37. data/lib/kaal/runtime.rb +20 -0
  38. data/lib/kaal/scheduler_file/hash_transform.rb +22 -0
  39. data/lib/kaal/scheduler_file/helper_bundle.rb +28 -0
  40. data/lib/kaal/scheduler_file/job_applier.rb +242 -0
  41. data/lib/kaal/scheduler_file/job_normalizer.rb +90 -0
  42. data/lib/kaal/scheduler_file/loader.rb +152 -0
  43. data/lib/kaal/scheduler_file/payload_loader.rb +95 -0
  44. data/lib/kaal/{scheduler_placeholder_support.rb → scheduler_file/placeholder_support.rb} +0 -1
  45. data/lib/kaal/scheduler_file.rb +18 -0
  46. data/lib/kaal/support/hash_tools.rb +93 -0
  47. data/lib/kaal/{cron_humanizer.rb → utils/cron_humanizer.rb} +19 -1
  48. data/lib/kaal/{cron_utils.rb → utils/cron_utils.rb} +0 -1
  49. data/lib/kaal/{idempotency_key_generator.rb → utils/idempotency_key_generator.rb} +3 -3
  50. data/lib/kaal/utils.rb +18 -0
  51. data/lib/kaal/version.rb +1 -2
  52. data/lib/kaal.rb +77 -397
  53. metadata +64 -44
  54. data/app/models/kaal/cron_definition.rb +0 -76
  55. data/app/models/kaal/cron_dispatch.rb +0 -50
  56. data/app/models/kaal/cron_lock.rb +0 -38
  57. data/lib/generators/kaal/install/install_generator.rb +0 -72
  58. data/lib/generators/kaal/install/templates/create_kaal_definitions.rb.tt +0 -21
  59. data/lib/generators/kaal/install/templates/create_kaal_dispatches.rb.tt +0 -20
  60. data/lib/generators/kaal/install/templates/create_kaal_locks.rb.tt +0 -17
  61. data/lib/generators/kaal/install/templates/kaal.rb.tt +0 -31
  62. data/lib/generators/kaal/install/templates/scheduler.yml.tt +0 -22
  63. data/lib/kaal/backend/mysql_adapter.rb +0 -170
  64. data/lib/kaal/backend/postgres_adapter.rb +0 -134
  65. data/lib/kaal/backend/sqlite_adapter.rb +0 -116
  66. data/lib/kaal/definition/database_engine.rb +0 -50
  67. data/lib/kaal/dispatch/database_engine.rb +0 -94
  68. data/lib/kaal/railtie.rb +0 -183
  69. data/lib/kaal/rake_tasks.rb +0 -184
  70. data/lib/kaal/scheduler_file_loader.rb +0 -321
  71. data/lib/kaal/scheduler_hash_transform.rb +0 -45
@@ -1,170 +0,0 @@
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
-
8
- require 'socket'
9
- require 'digest'
10
- require_relative 'dispatch_logging'
11
- require_relative '../definition/database_engine'
12
-
13
- module Kaal
14
- module Backend
15
- ##
16
- # Distributed backend adapter using MySQL named locks (GET_LOCK/RELEASE_LOCK).
17
- #
18
- # This adapter uses MySQL's GET_LOCK and RELEASE_LOCK functions for
19
- # distributed locking across multiple nodes. Locks are connection-based
20
- # and automatically released when the database connection is closed.
21
- #
22
- # **IMPORTANT LIMITATIONS:**
23
- # - Locks are connection-scoped: if a process crashes, the lock persists until
24
- # the database connection timeout occurs (typically 28,800 seconds or 8 hours).
25
- # For critical systems, consider monitoring stale locks or using a time-based
26
- # fallback mechanism.
27
- # - MySQL named locks have a maximum length of 64 characters. Lock keys longer than
28
- # 64 characters use a deterministic hash-based shortening scheme (prefix + SHA256
29
- # digest) to avoid collisions while respecting the limit.
30
- # - Uses non-blocking acquisition: GET_LOCK is called with timeout=0 for immediate
31
- # return (does not block waiting for the lock).
32
- # - Ensure connection pooling is properly configured to release connections
33
- # promptly when processes terminate.
34
- #
35
- # Optionally logs all dispatch attempts to the database when
36
- # enable_log_dispatch_registry is enabled in configuration.
37
- #
38
- # @example Using the MySQL adapter
39
- # Kaal.configure do |config|
40
- # config.backend = Kaal::Backend::MySQLAdapter.new
41
- # config.enable_log_dispatch_registry = true # Enable dispatch logging
42
- # end
43
- class MySQLAdapter < Adapter
44
- include DispatchLogging
45
-
46
- # MySQL named locks have a maximum length of 64 characters
47
- MAX_LOCK_NAME_LENGTH = 64
48
-
49
- def initialize
50
- super
51
- @lock_name_length_limit = MAX_LOCK_NAME_LENGTH
52
- @false_value_pattern = /\A(0|f|false|)\z/i
53
- end
54
-
55
- ##
56
- # Initialize a new MySQL adapter.
57
-
58
- ##
59
- # Get the dispatch registry for database logging.
60
- #
61
- # @return [Kaal::Dispatch::DatabaseEngine] database engine instance
62
- def dispatch_registry
63
- @dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new
64
- end
65
-
66
- ##
67
- # Get the definition registry for database-backed definition persistence.
68
- #
69
- # @return [Kaal::Definition::DatabaseEngine] database definition engine instance
70
- def definition_registry
71
- @definition_registry ||= Kaal::Definition::DatabaseEngine.new
72
- end
73
-
74
- ##
75
- # Attempt to acquire a distributed lock using MySQL GET_LOCK.
76
- #
77
- # Uses MySQL's GET_LOCK(name, timeout) function with a timeout of 0 seconds
78
- # to perform non-blocking acquisition. If successful, logs the dispatch
79
- # attempt when enable_log_dispatch_registry is enabled.
80
- #
81
- # **Note:** The +ttl+ parameter is ignored. MySQL named locks are connection-based
82
- # and do not have automatic expiration. The lock will be held until explicitly
83
- # released or the database connection is closed. See class documentation for
84
- # limitations.
85
- #
86
- # @param key [String] the lock key (format: "namespace:dispatch:cron_key:fire_time")
87
- # @param ttl [Integer] time-to-live in seconds (ignored; see class docs)
88
- # @return [Boolean] true if acquired, false if held by another process
89
- def acquire(key, _ttl)
90
- lock_name = normalize_lock_name(key)
91
-
92
- # GET_LOCK returns 1 on success, 0 on timeout, NULL on error
93
- sql = ActiveRecord::Base.sanitize_sql_array(['SELECT GET_LOCK(?, 0) as lock_result', lock_name])
94
- result_set = ActiveRecord::Base.connection.execute(sql)
95
- # Convert result to array and get first row, then first column value
96
- result_row = result_set.to_a.first
97
- result_value = result_row.is_a?(Hash) ? result_row['lock_result'] : result_row&.first
98
- acquired = cast_to_boolean(result_value)
99
-
100
- log_dispatch_attempt(key) if acquired
101
-
102
- acquired
103
- rescue StandardError => e
104
- raise LockAdapterError, "MySQL acquire failed for #{key}: #{e.message}"
105
- end
106
-
107
- ##
108
- # Release a distributed lock held by MySQL GET_LOCK.
109
- #
110
- # @param key [String] the lock key
111
- # @return [Boolean] true if released, false if not held
112
- def release(key)
113
- lock_name = normalize_lock_name(key)
114
-
115
- # RELEASE_LOCK returns 1 if held and released, 0 if not held, NULL on error
116
- sql = ActiveRecord::Base.sanitize_sql_array(['SELECT RELEASE_LOCK(?) as lock_result', lock_name])
117
- result_set = ActiveRecord::Base.connection.execute(sql)
118
- # Convert result to array and get first row, then first column value
119
- result_row = result_set.to_a.first
120
- result_value = result_row.is_a?(Hash) ? result_row['lock_result'] : result_row&.first
121
- cast_to_boolean(result_value)
122
- rescue StandardError => e
123
- raise LockAdapterError, "MySQL release failed for #{key}: #{e.message}"
124
- end
125
-
126
- private
127
-
128
- def cast_to_boolean(value)
129
- # MySQL GET_LOCK/RELEASE_LOCK returns 1 (success), 0 (failure), or NULL (error).
130
- # Cast integer/nil to boolean: 1 => true, 0 or nil => false.
131
- case value
132
- when 1
133
- true
134
- when 0
135
- false
136
- when true, false
137
- value
138
- else
139
- !value.to_s.match?(@false_value_pattern)
140
- end
141
- end
142
-
143
- ##
144
- # Normalize lock names to fit MySQL's 64-character limit.
145
- #
146
- # For keys exceeding the limit, uses a deterministic hash-based scheme
147
- # (prefix + SHA256 digest) to avoid collisions.
148
- #
149
- # @param key [String] the lock key to normalize
150
- # @return [String] normalized key (max 64 characters)
151
- def normalize_lock_name(key)
152
- return key if key.length <= @lock_name_length_limit
153
-
154
- # Use SHA256 digest to ensure uniqueness while respecting the 64-char limit.
155
- # Format: "prefix:hash" where hash is first 16 hex chars (~8 bytes entropy)
156
- digest = Digest::SHA256.hexdigest(key)
157
- # Reserve 17 chars for `:` + 16 hex chars, use remainder for prefix
158
- prefix_length = @lock_name_length_limit - 17
159
- normalized = "#{key[0...prefix_length]}:#{digest[0...16]}"
160
-
161
- Kaal.logger&.warn(
162
- "Lock key '#{key}' exceeds MySQL named lock limit of #{@lock_name_length_limit} characters. " \
163
- "Using hash-based shortening to avoid collisions: '#{normalized}'."
164
- )
165
-
166
- normalized
167
- end
168
- end
169
- end
170
- end
@@ -1,134 +0,0 @@
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
-
8
- require 'digest'
9
- require 'socket'
10
- require_relative 'dispatch_logging'
11
- require_relative '../definition/database_engine'
12
-
13
- module Kaal
14
- module Backend
15
- ##
16
- # Distributed backend adapter using PostgreSQL advisory locks.
17
- #
18
- # This adapter uses PostgreSQL's pg_try_advisory_lock function for
19
- # distributed locking across multiple nodes. Locks are connection-based
20
- # and automatically released when the database connection is closed.
21
- #
22
- # **IMPORTANT LIMITATIONS:**
23
- # - The +ttl+ parameter is ignored. Locks do not auto-expire based on time;
24
- # they persist until the database connection terminates.
25
- # - If a process crashes while holding a lock, the lock will remain held until
26
- # the connection timeout occurs (typically 30-60 minutes). For critical systems,
27
- # consider monitoring stale locks or using a time-based fallback mechanism.
28
- # - Ensure connection pooling is properly configured to release connections
29
- # promptly when processes terminate.
30
- #
31
- # Optionally logs all dispatch attempts to the database when
32
- # enable_log_dispatch_registry is enabled in configuration.
33
- #
34
- # @example Using the PostgreSQL adapter
35
- # Kaal.configure do |config|
36
- # config.backend = Kaal::Backend::PostgresAdapter.new
37
- # config.enable_log_dispatch_registry = true # Enable dispatch logging
38
- # end
39
- class PostgresAdapter < Adapter
40
- include DispatchLogging
41
-
42
- ##
43
- # Initialize a new PostgreSQL adapter.
44
- def initialize
45
- super
46
- @signed_64_max = 9_223_372_036_854_775_807
47
- @unsigned_64_range = 18_446_744_073_709_551_616
48
- @false_value_pattern = /\A(f|false|0|)\z/i
49
- end
50
-
51
- ##
52
- # Get the dispatch registry for database logging.
53
- #
54
- # @return [Kaal::Dispatch::DatabaseEngine] database engine instance
55
- def dispatch_registry
56
- @dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new
57
- end
58
-
59
- ##
60
- # Get the definition registry for database-backed definition persistence.
61
- #
62
- # @return [Kaal::Definition::DatabaseEngine] database definition engine instance
63
- def definition_registry
64
- @definition_registry ||= Kaal::Definition::DatabaseEngine.new
65
- end
66
-
67
- ##
68
- # Attempt to acquire a distributed lock using PostgreSQL advisory lock.
69
- #
70
- # Converts the lock key to a deterministic 64-bit integer hash and attempts
71
- # to acquire the advisory lock. If successful, logs the dispatch attempt
72
- # when enable_log_dispatch_registry is enabled.
73
- #
74
- # **Note:** The +ttl+ parameter is ignored. PostgreSQL advisory locks are
75
- # connection-based and do not auto-expire. The lock will be held until the
76
- # database connection is closed. See class documentation for limitations.
77
- #
78
- # @param key [String] the lock key (format: "namespace:dispatch:cron_key:fire_time")
79
- # @param ttl [Integer] time-to-live in seconds (ignored; see class docs)
80
- # @return [Boolean] true if acquired, false if held by another process
81
- def acquire(key, _ttl)
82
- lock_id = calculate_lock_id(key)
83
-
84
- sql = ActiveRecord::Base.sanitize_sql_array(['SELECT pg_try_advisory_lock(?)', lock_id])
85
- acquired = cast_to_boolean(ActiveRecord::Base.connection.execute(sql).first['pg_try_advisory_lock'])
86
-
87
- log_dispatch_attempt(key) if acquired
88
-
89
- acquired
90
- rescue StandardError => e
91
- raise LockAdapterError, "PostgreSQL acquire failed for #{key}: #{e.message}"
92
- end
93
-
94
- ##
95
- # Release a distributed lock held by PostgreSQL advisory lock.
96
- #
97
- # @param key [String] the lock key
98
- # @return [Boolean] true if released, false if not held
99
- def release(key)
100
- lock_id = calculate_lock_id(key)
101
-
102
- sql = ActiveRecord::Base.sanitize_sql_array(['SELECT pg_advisory_unlock(?)', lock_id])
103
- cast_to_boolean(ActiveRecord::Base.connection.execute(sql).first['pg_advisory_unlock'])
104
- rescue StandardError => e
105
- raise LockAdapterError, "PostgreSQL release failed for #{key}: #{e.message}"
106
- end
107
-
108
- private
109
-
110
- def cast_to_boolean(value)
111
- # PostgreSQL's `.execute` returns "t"/"f" strings for boolean columns,
112
- # not Ruby true/false. Explicitly cast to boolean for proper semantics.
113
- case value
114
- when 't'
115
- true
116
- when 'f'
117
- false
118
- when true, false
119
- value
120
- else
121
- !value.to_s.match?(@false_value_pattern)
122
- end
123
- end
124
-
125
- def calculate_lock_id(key)
126
- # Use MD5 hash of the key and convert to 64-bit signed integer
127
- # Ensure it's in the range of a signed 64-bit integer
128
- hash = Digest::MD5.digest(key).unpack1('Q>')
129
- # Convert to signed 64-bit integer
130
- hash > @signed_64_max ? hash - @unsigned_64_range : hash
131
- end
132
- end
133
- end
134
- end
@@ -1,116 +0,0 @@
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
-
8
- require_relative 'dispatch_logging'
9
- require_relative '../definition/database_engine'
10
-
11
- module Kaal
12
- module Backend
13
- ##
14
- # Distributed backend adapter using any ActiveRecord-backed SQL database.
15
- #
16
- # Despite the "SQLiteAdapter" name, this adapter works with any SQL database
17
- # supported by Rails (SQLite, PostgreSQL, MySQL, etc.) via ActiveRecord. It stores
18
- # locks in a database table with TTL-based expiration and uses a UNIQUE constraint
19
- # on the key column to ensure atomicity.
20
- #
21
- # Suitable for single-server or development environments. For production
22
- # multi-node deployments, use Redis or PostgreSQL adapters instead.
23
- #
24
- # @example Using the adapter with any SQL database
25
- # Kaal.configure do |config|
26
- # config.backend = Kaal::Backend::SQLiteAdapter.new
27
- # config.enable_log_dispatch_registry = true # Enable dispatch logging
28
- # end
29
- class SQLiteAdapter < Adapter
30
- include DispatchLogging
31
-
32
- ##
33
- # Initialize a new database-backed adapter.
34
- #
35
- # @return [SQLiteAdapter] a new instance
36
- # (Note: Despite the class name, this works with any ActiveRecord SQL database)
37
-
38
- ##
39
- # Get the dispatch registry for database logging.
40
- #
41
- # @return [Kaal::Dispatch::DatabaseEngine] database engine instance
42
- def dispatch_registry
43
- @dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new
44
- end
45
-
46
- ##
47
- # Get the definition registry for database-backed definition persistence.
48
- #
49
- # @return [Kaal::Definition::DatabaseEngine] database definition engine instance
50
- def definition_registry
51
- @definition_registry ||= Kaal::Definition::DatabaseEngine.new
52
- end
53
-
54
- ##
55
- # Attempt to acquire a distributed lock in the database.
56
- #
57
- # Attempts to insert a new lock record. If the key already exists, cleans up
58
- # any expired locks and retries once. This avoids unnecessary cleanup in the
59
- # common case and reduces the window for race conditions.
60
- #
61
- # @param key [String] the lock key
62
- # @param ttl [Integer] time-to-live in seconds
63
- # @return [Boolean] true if acquired, false if held by another process
64
- def acquire(key, ttl)
65
- now = Time.current
66
- expires_at = now + ttl.seconds
67
- acquired = false
68
- attempt_cleanup = false
69
-
70
- 2.times do
71
- Kaal::CronLock.cleanup_expired if attempt_cleanup
72
- begin
73
- Kaal::CronLock.create!(
74
- key: key,
75
- acquired_at: now,
76
- expires_at: expires_at
77
- )
78
- acquired = true
79
- break
80
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
81
- attempt_cleanup = true
82
- rescue ActiveRecord::StatementInvalid => e
83
- raise unless wrapped_contention_error?(e)
84
-
85
- attempt_cleanup = true
86
- end
87
- end
88
-
89
- log_dispatch_attempt(key) if acquired
90
-
91
- acquired
92
- rescue StandardError => e
93
- raise LockAdapterError, "SQLite acquire failed for #{key}: #{e.message}"
94
- end
95
-
96
- ##
97
- # Release a previously acquired lock.
98
- #
99
- # @param key [String] the lock key to release
100
- # @return [Boolean] true if released (key existed and was deleted), false if not held
101
- def release(key)
102
- deleted = Kaal::CronLock.where(key: key).delete_all
103
- deleted.positive?
104
- rescue StandardError => e
105
- raise LockAdapterError, "SQLite release failed for #{key}: #{e.message}"
106
- end
107
-
108
- private
109
-
110
- def wrapped_contention_error?(error)
111
- cause = error.cause
112
- cause.is_a?(ActiveRecord::RecordNotUnique) || error.message.match?(/unique|duplicate/i)
113
- end
114
- end
115
- end
116
- end
@@ -1,50 +0,0 @@
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
-
8
- require_relative 'registry'
9
-
10
- module Kaal
11
- module Definition
12
- # ActiveRecord-backed definition registry persisted in kaal_definitions.
13
- class DatabaseEngine < Registry
14
- def initialize
15
- super
16
- @definition_model = ::Kaal::CronDefinition
17
- end
18
-
19
- def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
20
- @definition_model.upsert_definition!(
21
- key: key,
22
- cron: cron,
23
- enabled: enabled,
24
- source: source,
25
- metadata: metadata
26
- ).to_definition_hash
27
- end
28
-
29
- def remove_definition(key)
30
- record = @definition_model.find_by(key: key)
31
- return nil unless record
32
-
33
- record.destroy_and_return_definition_hash
34
- end
35
-
36
- def find_definition(key)
37
- record = @definition_model.find_by(key: key)
38
- record&.to_definition_hash
39
- end
40
-
41
- def all_definitions
42
- @definition_model.order(:key).map(&:to_definition_hash)
43
- end
44
-
45
- def enabled_definitions
46
- @definition_model.where(enabled: true).order(:key).map(&:to_definition_hash)
47
- end
48
- end
49
- end
50
- end
@@ -1,94 +0,0 @@
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
-
8
- require_relative 'registry'
9
-
10
- module Kaal
11
- module Dispatch
12
- ##
13
- # Database-backed dispatch registry using ActiveRecord.
14
- #
15
- # Stores dispatch records in the database using the CronDispatch model.
16
- # Provides persistent, queryable audit logs across all nodes.
17
- #
18
- # @example Usage
19
- # registry = Kaal::Dispatch::DatabaseEngine.new
20
- # registry.log_dispatch('daily_report', Time.current, 'node-1')
21
- # registry.dispatched?('daily_report', Time.current) # => true
22
- class DatabaseEngine < Registry
23
- ##
24
- # Log a dispatch attempt in the database.
25
- #
26
- # @param key [String] the cron job key
27
- # @param fire_time [Time] when the job was scheduled to fire
28
- # @param node_id [String] identifier for the dispatching node
29
- # @param status [String] dispatch status ('dispatched', 'failed', etc.)
30
- # @return [Kaal::CronDispatch] the created dispatch record
31
- # @raise [ActiveRecord::RecordInvalid] if the record is invalid
32
- def log_dispatch(key, fire_time, node_id, status = 'dispatched')
33
- ::Kaal::CronDispatch.create!(
34
- key: key,
35
- fire_time: fire_time,
36
- dispatched_at: Time.current,
37
- node_id: node_id,
38
- status: status
39
- )
40
- end
41
-
42
- ##
43
- # Find a dispatch record for a specific job and fire time.
44
- #
45
- # @param key [String] the cron job key
46
- # @param fire_time [Time] when the job was scheduled to fire
47
- # @return [Kaal::CronDispatch, nil] dispatch record or nil if not found
48
- def find_dispatch(key, fire_time)
49
- ::Kaal::CronDispatch.find_by(key: key, fire_time: fire_time)
50
- end
51
-
52
- ##
53
- # Find all dispatch records for a specific job key.
54
- #
55
- # @param key [String] the cron job key
56
- # @return [ActiveRecord::Relation] collection of dispatch records
57
- def find_by_key(key)
58
- ::Kaal::CronDispatch.where(key: key).order(fire_time: :desc)
59
- end
60
-
61
- ##
62
- # Find all dispatch records by node ID.
63
- #
64
- # @param node_id [String] the node identifier
65
- # @return [ActiveRecord::Relation] collection of dispatch records
66
- def find_by_node(node_id)
67
- ::Kaal::CronDispatch.where(node_id: node_id).order(fire_time: :desc)
68
- end
69
-
70
- ##
71
- # Find all dispatch records with a specific status.
72
- #
73
- # @param status [String] the dispatch status
74
- # @return [ActiveRecord::Relation] collection of dispatch records
75
- def find_by_status(status)
76
- ::Kaal::CronDispatch.where(status: status).order(fire_time: :desc)
77
- end
78
-
79
- ##
80
- # Delete old dispatch records older than the specified time.
81
- #
82
- # This cleanup prevents unbounded database growth by removing records
83
- # that are older than the recovery window, making them irrelevant for
84
- # future recovery operations.
85
- #
86
- # @param recovery_window [Integer] seconds to keep records for (e.g., 86400 for 24h)
87
- # @return [Integer] number of records deleted
88
- def cleanup(recovery_window: 86_400)
89
- cutoff_time = Time.current - recovery_window
90
- ::Kaal::CronDispatch.where('fire_time < ?', cutoff_time).delete_all
91
- end
92
- end
93
- end
94
- end