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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +79 -287
  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/active_record_support.rb +82 -0
  8. data/lib/kaal/backend/adapter.rb +0 -1
  9. data/lib/kaal/backend/dispatch_attempt_logger.rb +33 -0
  10. data/lib/kaal/backend/dispatch_logging.rb +36 -23
  11. data/lib/kaal/backend/dispatch_registry_accessor.rb +43 -0
  12. data/lib/kaal/backend/memory_adapter.rb +7 -5
  13. data/lib/kaal/backend/mysql.rb +41 -0
  14. data/lib/kaal/backend/postgres.rb +41 -0
  15. data/lib/kaal/backend/redis_adapter.rb +6 -6
  16. data/lib/kaal/backend/sqlite.rb +41 -0
  17. data/lib/kaal/cli.rb +230 -0
  18. data/lib/kaal/{configuration.rb → config/configuration.rb} +0 -1
  19. data/lib/kaal/{scheduler_config_error.rb → config/scheduler_config_error.rb} +0 -1
  20. data/lib/kaal/config/scheduler_time_zone_resolver.rb +50 -0
  21. data/lib/kaal/config.rb +19 -0
  22. data/lib/kaal/{coordinator.rb → core/coordinator.rb} +42 -62
  23. data/lib/kaal/core/enabled_entry_enumerator.rb +51 -0
  24. data/lib/kaal/core/occurrence_finder.rb +38 -0
  25. data/lib/kaal/core.rb +18 -0
  26. data/lib/kaal/definition/database_engine.rb +54 -16
  27. data/lib/kaal/definition/memory_engine.rb +11 -18
  28. data/lib/kaal/definition/persistence_helpers.rb +31 -0
  29. data/lib/kaal/definition/redis_engine.rb +9 -6
  30. data/lib/kaal/definition/registry.rb +24 -2
  31. data/lib/kaal/definitions/registration_service.rb +62 -0
  32. data/lib/kaal/definitions/registry_accessor.rb +33 -0
  33. data/lib/kaal/dispatch/database_engine.rb +87 -61
  34. data/lib/kaal/dispatch/memory_engine.rb +3 -4
  35. data/lib/kaal/dispatch/redis_engine.rb +2 -3
  36. data/lib/kaal/dispatch/registry.rb +0 -1
  37. data/lib/kaal/internal/active_record/base_record.rb +16 -0
  38. data/lib/kaal/internal/active_record/connection_support.rb +96 -0
  39. data/lib/kaal/internal/active_record/database_backend.rb +73 -0
  40. data/lib/kaal/internal/active_record/definition_record.rb +16 -0
  41. data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
  42. data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
  43. data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
  44. data/lib/kaal/internal/active_record/lock_record.rb +16 -0
  45. data/lib/kaal/internal/active_record/migration_templates.rb +108 -0
  46. data/lib/kaal/internal/active_record/mysql_backend.rb +71 -0
  47. data/lib/kaal/internal/active_record/postgres_backend.rb +69 -0
  48. data/lib/kaal/internal/active_record.rb +17 -0
  49. data/lib/kaal/internal/sequel/database_backend.rb +74 -0
  50. data/lib/kaal/internal/sequel/mysql_backend.rb +69 -0
  51. data/lib/kaal/internal/sequel/postgres_backend.rb +67 -0
  52. data/lib/kaal/internal/sequel.rb +12 -0
  53. data/lib/kaal/persistence/database.rb +35 -0
  54. data/lib/kaal/persistence/migration_templates.rb +97 -0
  55. data/lib/kaal/register_conflict_support.rb +0 -1
  56. data/lib/kaal/registry.rb +0 -3
  57. data/lib/kaal/runtime/runtime_context.rb +41 -0
  58. data/lib/kaal/runtime/scheduler_boot_loader.rb +52 -0
  59. data/lib/kaal/runtime/signal_handler_chain.rb +42 -0
  60. data/lib/kaal/runtime/signal_handler_installer.rb +39 -0
  61. data/lib/kaal/runtime.rb +20 -0
  62. data/lib/kaal/scheduler_file/hash_transform.rb +22 -0
  63. data/lib/kaal/scheduler_file/helper_bundle.rb +28 -0
  64. data/lib/kaal/scheduler_file/job_applier.rb +242 -0
  65. data/lib/kaal/scheduler_file/job_normalizer.rb +90 -0
  66. data/lib/kaal/scheduler_file/loader.rb +152 -0
  67. data/lib/kaal/scheduler_file/payload_loader.rb +95 -0
  68. data/lib/kaal/{scheduler_placeholder_support.rb → scheduler_file/placeholder_support.rb} +0 -1
  69. data/lib/kaal/scheduler_file.rb +18 -0
  70. data/lib/kaal/sequel_support.rb +82 -0
  71. data/lib/kaal/support/hash_tools.rb +93 -0
  72. data/lib/kaal/{cron_humanizer.rb → utils/cron_humanizer.rb} +19 -1
  73. data/lib/kaal/{cron_utils.rb → utils/cron_utils.rb} +0 -1
  74. data/lib/kaal/{idempotency_key_generator.rb → utils/idempotency_key_generator.rb} +3 -3
  75. data/lib/kaal/utils.rb +18 -0
  76. data/lib/kaal/version.rb +1 -2
  77. data/lib/kaal.rb +83 -397
  78. metadata +87 -42
  79. data/app/models/kaal/cron_definition.rb +0 -76
  80. data/app/models/kaal/cron_dispatch.rb +0 -50
  81. data/app/models/kaal/cron_lock.rb +0 -38
  82. data/lib/generators/kaal/install/install_generator.rb +0 -72
  83. data/lib/generators/kaal/install/templates/create_kaal_definitions.rb.tt +0 -21
  84. data/lib/generators/kaal/install/templates/create_kaal_dispatches.rb.tt +0 -20
  85. data/lib/generators/kaal/install/templates/create_kaal_locks.rb.tt +0 -17
  86. data/lib/generators/kaal/install/templates/kaal.rb.tt +0 -31
  87. data/lib/generators/kaal/install/templates/scheduler.yml.tt +0 -22
  88. data/lib/kaal/backend/mysql_adapter.rb +0 -170
  89. data/lib/kaal/backend/postgres_adapter.rb +0 -134
  90. data/lib/kaal/backend/sqlite_adapter.rb +0 -116
  91. data/lib/kaal/railtie.rb +0 -183
  92. data/lib/kaal/rake_tasks.rb +0 -184
  93. data/lib/kaal/scheduler_file_loader.rb +0 -321
  94. 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
data/lib/kaal/railtie.rb DELETED
@@ -1,183 +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 'pathname'
9
-
10
- module Kaal
11
- ##
12
- # Railtie class to integrate Kaal with Rails applications.
13
- # Initializes configuration, sets up the default logger, and handles signal management.
14
- class Railtie < ::Rails::Railtie
15
- ##
16
- # Ensure configuration logger uses Rails.logger when available.
17
- def self.ensure_logger!
18
- logger = Rails.logger
19
- return unless logger
20
-
21
- Kaal.configure do |config|
22
- config.logger ||= logger
23
- end
24
- rescue NoMethodError
25
- nil
26
- end
27
-
28
- ##
29
- # Register signal handlers for graceful shutdown.
30
- # Captures and chains any previously registered handlers to cooperate with other components.
31
- def self.register_signal_handlers
32
- logger = Kaal.logger
33
-
34
- %w[TERM INT].each do |signal|
35
- # Capture the previous handler by temporarily setting to IGNORE and restoring
36
- old_handler = Signal.trap(signal, 'IGNORE')
37
- Signal.trap(signal, old_handler) if old_handler && old_handler != 'IGNORE'
38
-
39
- # Now install our handler that chains to the previous one
40
- Signal.trap(signal) do
41
- handle_shutdown_signal(signal, old_handler, logger)
42
- end
43
- end
44
- rescue StandardError => e
45
- logger&.warn("Failed to register signal handlers: #{e.full_message}")
46
- end
47
-
48
- ##
49
- # Handle a shutdown signal and chain to previous handler.
50
- def self.handle_shutdown_signal(signal, old_handler, logger)
51
- logger&.info("Received #{signal} signal, stopping scheduler...")
52
- begin
53
- stopped = Kaal.stop!(timeout: 30)
54
- logger&.warn('Scheduler did not stop within timeout, thread may still be running') unless stopped
55
- rescue StandardError => e
56
- logger&.error("Error stopping scheduler on #{signal} signal: #{e.full_message}")
57
- end
58
-
59
- chain_previous_handler(signal, old_handler, logger)
60
- end
61
-
62
- ##
63
- # Chain to a previous signal handler if it exists.
64
- def self.chain_previous_handler(signal, old_handler, logger)
65
- if old_handler.respond_to?(:call)
66
- old_handler.call
67
- elsif old_handler.is_a?(String) && old_handler != 'DEFAULT' && old_handler != 'IGNORE'
68
- # If previous handler was a command string, we can't easily re-invoke it
69
- logger&.debug("Previous #{signal} handler was a command: #{old_handler}")
70
- end
71
- end
72
-
73
- ##
74
- # Load scheduler file at boot while respecting missing-file policy.
75
- def self.load_scheduler_file_on_boot!
76
- configuration = fetch_configuration_for_boot
77
- return unless configuration
78
-
79
- if configuration.scheduler_missing_file_policy == :error
80
- load_scheduler_file_now!
81
- return
82
- end
83
-
84
- scheduler_path = configuration.scheduler_config_path.to_s.strip
85
- return if scheduler_path.empty?
86
-
87
- absolute_path = resolve_scheduler_path(scheduler_path)
88
- unless File.exist?(absolute_path)
89
- Kaal.logger&.warn("Scheduler file not found at #{absolute_path}")
90
- return
91
- end
92
-
93
- load_scheduler_file_now!
94
- end
95
-
96
- def self.resolve_scheduler_path(path)
97
- candidate = Pathname.new(path)
98
- candidate.absolute? ? candidate.to_s : Rails.root.join(candidate).to_s
99
- end
100
-
101
- def self.load_scheduler_file_now!
102
- Kaal.load_scheduler_file!
103
- end
104
-
105
- def self.fetch_configuration_for_boot
106
- Kaal.configuration
107
- rescue NameError => e
108
- Kaal.logger&.debug("Skipping scheduler file boot load due to configuration error: #{e.message}")
109
- nil
110
- end
111
-
112
- ##
113
- # Autoload paths for Kaal models and other components
114
- initializer 'kaal.autoload' do |_app|
115
- models_path = File.expand_path('../../app/models', __dir__)
116
- Rails.autoloaders.main.push_dir(models_path)
117
- end
118
-
119
- ##
120
- # Initialize Kaal when Rails boots.
121
- # Sets the default logger to Rails.logger if available.
122
- initializer 'kaal.configuration' do |_app|
123
- # Set default logger to Rails.logger if not already configured
124
- Kaal::Railtie.ensure_logger!
125
- end
126
-
127
- ##
128
- # Load gem i18n files into Rails I18n load path for host applications.
129
- initializer 'kaal.i18n', before: 'i18n.load_path' do |app|
130
- locales = Dir[File.expand_path('../../config/locales/*.yml', __dir__)]
131
- app.config.i18n.load_path |= locales
132
- end
133
-
134
- ##
135
- # Load rake tasks into host Rails applications.
136
- rake_tasks do
137
- load File.expand_path('../tasks/kaal_tasks.rake', __dir__)
138
- end
139
-
140
- ##
141
- # Load the default initializer after Rails has finished initialization.
142
- # This ensures Rails.logger is fully available and sets up signal handlers.
143
- config.after_initialize do
144
- # Re-ensure logger is set in case it wasn't available during first initializer
145
- Kaal::Railtie.ensure_logger!
146
-
147
- # Load scheduler definitions from file when available (or required by policy)
148
- Kaal::Railtie.load_scheduler_file_on_boot!
149
-
150
- # Register signal handlers for graceful shutdown
151
- Kaal::Railtie.register_signal_handlers
152
- end
153
-
154
- ##
155
- # Handle graceful shutdown when Rails exits.
156
- def self.handle_shutdown
157
- return unless Kaal.running?
158
-
159
- logger = Kaal.logger
160
-
161
- logger&.info('Rails is shutting down, stopping Kaal scheduler...')
162
- begin
163
- stopped = Kaal.stop!(timeout: 10)
164
- return if stopped
165
-
166
- pid = Process.pid
167
- message_array = [
168
- 'Kaal scheduler did not stop within timeout.',
169
- "Process #{pid} may still be running. You may need to kill it manually with `kill -9 #{pid}`."
170
- ]
171
- logger&.warn(message_array.join(' '))
172
- rescue StandardError => e
173
- logger&.error("Error stopping scheduler during shutdown: #{e.message}")
174
- end
175
- end
176
-
177
- ##
178
- # Ensure graceful shutdown on Rails shutdown.
179
- at_exit do
180
- Kaal::Railtie.handle_shutdown
181
- end
182
- end
183
- end