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.
- checksums.yaml +4 -4
- data/README.md +81 -286
- 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/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/redis_adapter.rb +6 -6
- 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/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/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/register_conflict_support.rb +0 -1
- data/lib/kaal/registry.rb +0 -1
- 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/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 +77 -397
- metadata +64 -44
- 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/definition/database_engine.rb +0 -50
- data/lib/kaal/dispatch/database_engine.rb +0 -94
- 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/core.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'kaal/core/coordinator'
|
|
8
|
+
require 'kaal/core/occurrence_finder'
|
|
9
|
+
require 'kaal/core/enabled_entry_enumerator'
|
|
10
|
+
|
|
11
|
+
module Kaal
|
|
12
|
+
# Core scheduling orchestration types.
|
|
13
|
+
module Core
|
|
14
|
+
Coordinator = ::Kaal::Coordinator
|
|
15
|
+
OccurrenceFinder = ::Kaal::OccurrenceFinder
|
|
16
|
+
EnabledEntryEnumerator = ::Kaal::EnabledEntryEnumerator
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -4,14 +4,16 @@
|
|
|
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
|
-
require 'active_support/core_ext/object/deep_dup'
|
|
9
7
|
require_relative 'registry'
|
|
8
|
+
require_relative 'persistence_helpers'
|
|
9
|
+
require 'kaal/support/hash_tools'
|
|
10
10
|
|
|
11
11
|
module Kaal
|
|
12
12
|
module Definition
|
|
13
13
|
# In-memory definition registry used when no persistent backend is configured.
|
|
14
14
|
class MemoryEngine < Registry
|
|
15
|
+
include Kaal::Support::HashTools
|
|
16
|
+
|
|
15
17
|
def initialize
|
|
16
18
|
super
|
|
17
19
|
@definitions = {}
|
|
@@ -20,42 +22,33 @@ module Kaal
|
|
|
20
22
|
|
|
21
23
|
def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
|
|
22
24
|
@mutex.synchronize do
|
|
23
|
-
now = Time.
|
|
25
|
+
now = Time.now.utc
|
|
24
26
|
existing = @definitions[key]
|
|
25
|
-
stored_metadata = (metadata || {}).deep_dup
|
|
26
|
-
disabled_at = if enabled
|
|
27
|
-
nil
|
|
28
|
-
elsif existing && existing[:enabled] == false
|
|
29
|
-
existing[:disabled_at]
|
|
30
|
-
else
|
|
31
|
-
now
|
|
32
|
-
end
|
|
33
27
|
definition = {
|
|
34
28
|
key: key,
|
|
35
29
|
cron: cron,
|
|
36
30
|
enabled: enabled,
|
|
37
31
|
source: source,
|
|
38
|
-
metadata:
|
|
32
|
+
metadata: deep_dup(metadata || {}),
|
|
39
33
|
created_at: existing ? existing[:created_at] : now,
|
|
40
34
|
updated_at: now,
|
|
41
|
-
disabled_at:
|
|
35
|
+
disabled_at: PersistenceHelpers.disabled_at_for(existing, enabled, now)
|
|
42
36
|
}
|
|
43
37
|
@definitions[key] = definition
|
|
44
|
-
|
|
45
|
-
definition.deep_dup
|
|
38
|
+
deep_dup(definition)
|
|
46
39
|
end
|
|
47
40
|
end
|
|
48
41
|
|
|
49
42
|
def remove_definition(key)
|
|
50
|
-
@mutex.synchronize { @definitions.delete(key)
|
|
43
|
+
@mutex.synchronize { deep_dup(@definitions.delete(key)) }
|
|
51
44
|
end
|
|
52
45
|
|
|
53
46
|
def find_definition(key)
|
|
54
|
-
@mutex.synchronize { @definitions[key]
|
|
47
|
+
@mutex.synchronize { deep_dup(@definitions[key]) }
|
|
55
48
|
end
|
|
56
49
|
|
|
57
50
|
def all_definitions
|
|
58
|
-
@mutex.synchronize { @definitions.values.map(
|
|
51
|
+
@mutex.synchronize { @definitions.values.sort_by { |definition| definition[:key] }.map { |definition| deep_dup(definition) } }
|
|
59
52
|
end
|
|
60
53
|
|
|
61
54
|
def clear
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'json'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
module Definition
|
|
11
|
+
# Shared pure helpers for persisted definition rows and metadata.
|
|
12
|
+
module PersistenceHelpers
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def disabled_at_for(existing, enabled, now)
|
|
16
|
+
return nil if enabled
|
|
17
|
+
return existing[:disabled_at] if existing && existing[:enabled] == false
|
|
18
|
+
|
|
19
|
+
now
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse_metadata(value)
|
|
23
|
+
return {} if value.to_s.empty?
|
|
24
|
+
|
|
25
|
+
JSON.parse(value, symbolize_names: true)
|
|
26
|
+
rescue JSON::ParserError
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -4,15 +4,18 @@
|
|
|
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 'json'
|
|
9
8
|
require 'time'
|
|
10
9
|
require_relative 'registry'
|
|
10
|
+
require 'kaal/support/hash_tools'
|
|
11
|
+
require 'kaal/definition/persistence_helpers'
|
|
11
12
|
|
|
12
13
|
module Kaal
|
|
13
14
|
module Definition
|
|
14
15
|
# Redis-backed definition registry shared across processes.
|
|
15
16
|
class RedisEngine < Registry
|
|
17
|
+
include Kaal::Support::HashTools
|
|
18
|
+
|
|
16
19
|
def initialize(redis, namespace: 'kaal')
|
|
17
20
|
super()
|
|
18
21
|
@redis = redis
|
|
@@ -20,21 +23,21 @@ module Kaal
|
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
|
|
23
|
-
now = Time.
|
|
26
|
+
now = Time.now.utc
|
|
24
27
|
existing = find_definition(key)
|
|
25
28
|
payload = {
|
|
26
29
|
key: key,
|
|
27
30
|
cron: cron,
|
|
28
31
|
enabled: enabled,
|
|
29
32
|
source: source,
|
|
30
|
-
metadata: metadata,
|
|
33
|
+
metadata: deep_dup(metadata || {}),
|
|
31
34
|
created_at: existing ? existing[:created_at] : now,
|
|
32
35
|
updated_at: now,
|
|
33
|
-
disabled_at: enabled
|
|
36
|
+
disabled_at: PersistenceHelpers.disabled_at_for(existing, enabled, now)
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
@redis.hset(storage_key, key, JSON.generate(self.class.serialize_payload(payload)))
|
|
37
|
-
payload
|
|
40
|
+
deep_dup(payload)
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def remove_definition(key)
|
|
@@ -49,7 +52,7 @@ module Kaal
|
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
def all_definitions
|
|
52
|
-
@redis.hvals(storage_key).filter_map { |raw| self.class.deserialize_payload(raw) }
|
|
55
|
+
@redis.hvals(storage_key).filter_map { |raw| self.class.deserialize_payload(raw) }.sort_by { |definition| definition[:key] }
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
private
|
|
@@ -4,9 +4,31 @@
|
|
|
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
|
module Kaal
|
|
9
8
|
module Definition
|
|
9
|
+
# Pure helpers for extracting persisted definition attributes without ActiveSupport.
|
|
10
|
+
module AttributeHelpers
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def definition_attributes(definition)
|
|
14
|
+
{
|
|
15
|
+
key: definition[:key],
|
|
16
|
+
cron: definition[:cron],
|
|
17
|
+
source: definition[:source],
|
|
18
|
+
metadata: definition[:metadata]
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def persisted_definition_attributes(definition)
|
|
23
|
+
return {} unless definition
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
enabled: definition[:enabled],
|
|
27
|
+
metadata: definition[:metadata]
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
10
32
|
# Base abstraction for cron definition storage.
|
|
11
33
|
class Registry
|
|
12
34
|
def upsert_definition(**)
|
|
@@ -43,7 +65,7 @@ module Kaal
|
|
|
43
65
|
definition = find_definition(key)
|
|
44
66
|
return nil unless definition
|
|
45
67
|
|
|
46
|
-
attributes =
|
|
68
|
+
attributes = AttributeHelpers.definition_attributes(definition).merge(enabled: enabled)
|
|
47
69
|
upsert_definition(**attributes)
|
|
48
70
|
end
|
|
49
71
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
module Kaal
|
|
8
|
+
module Definitions
|
|
9
|
+
# Registers code-defined jobs while preserving persisted definition state.
|
|
10
|
+
class RegistrationService
|
|
11
|
+
include RegisterConflictSupport
|
|
12
|
+
|
|
13
|
+
attr_reader :configuration, :definition_registry, :registry
|
|
14
|
+
|
|
15
|
+
def initialize(configuration:, definition_registry:, registry:)
|
|
16
|
+
@configuration = configuration
|
|
17
|
+
@definition_registry = definition_registry
|
|
18
|
+
@registry = registry
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(key:, cron:, enqueue:)
|
|
22
|
+
existing_definition = @definition_registry.find_definition(key)
|
|
23
|
+
existing_entry = @registry.find(key)
|
|
24
|
+
if existing_entry
|
|
25
|
+
conflict_result = resolve_register_conflict(
|
|
26
|
+
key: key,
|
|
27
|
+
cron: cron,
|
|
28
|
+
enqueue: enqueue,
|
|
29
|
+
existing_definition: existing_definition,
|
|
30
|
+
existing_entry: existing_entry
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return conflict_result if conflict_result
|
|
34
|
+
|
|
35
|
+
raise RegistryError, "Key '#{key}' is already registered"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
persisted_attributes = {
|
|
39
|
+
enabled: true,
|
|
40
|
+
source: 'code',
|
|
41
|
+
metadata: {}
|
|
42
|
+
}.merge(Definition::AttributeHelpers.persisted_definition_attributes(existing_definition))
|
|
43
|
+
@definition_registry.upsert_definition(key: key, cron: cron, **persisted_attributes)
|
|
44
|
+
with_registered_definition_rollback(key, existing_definition) do
|
|
45
|
+
@registry.add(key: key, cron: cron, enqueue: enqueue)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def rollback_registered_definition(key, existing_definition)
|
|
52
|
+
if existing_definition
|
|
53
|
+
@definition_registry.upsert_definition(
|
|
54
|
+
**Definition::AttributeHelpers.definition_attributes(existing_definition), enabled: existing_definition[:enabled]
|
|
55
|
+
)
|
|
56
|
+
elsif !@registry.registered?(key)
|
|
57
|
+
@definition_registry.remove_definition(key)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
module Kaal
|
|
8
|
+
module Definitions
|
|
9
|
+
# Resolves the active definition registry with an in-memory fallback.
|
|
10
|
+
class RegistryAccessor
|
|
11
|
+
def initialize(configuration:, fallback_registry_provider:)
|
|
12
|
+
@configuration = configuration
|
|
13
|
+
@fallback_registry_provider = fallback_registry_provider
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
configured_backend = @configuration.backend
|
|
18
|
+
registry = configured_backend&.definition_registry
|
|
19
|
+
return registry if registry
|
|
20
|
+
|
|
21
|
+
fallback_registry
|
|
22
|
+
rescue NoMethodError
|
|
23
|
+
fallback_registry
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def fallback_registry
|
|
29
|
+
@fallback_registry_provider.call
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -4,7 +4,6 @@
|
|
|
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_relative 'registry'
|
|
9
8
|
|
|
10
9
|
module Kaal
|
|
@@ -17,8 +16,8 @@ module Kaal
|
|
|
17
16
|
#
|
|
18
17
|
# @example Usage
|
|
19
18
|
# registry = Kaal::Dispatch::MemoryEngine.new
|
|
20
|
-
# registry.log_dispatch('daily_report', Time.
|
|
21
|
-
# registry.dispatched?('daily_report', Time.
|
|
19
|
+
# registry.log_dispatch('daily_report', Time.now.utc, 'node-1')
|
|
20
|
+
# registry.dispatched?('daily_report', Time.now.utc) # => true
|
|
22
21
|
class MemoryEngine < Registry
|
|
23
22
|
##
|
|
24
23
|
# Initialize a new in-memory registry.
|
|
@@ -42,7 +41,7 @@ module Kaal
|
|
|
42
41
|
@dispatches[storage_key] = {
|
|
43
42
|
key: key,
|
|
44
43
|
fire_time: fire_time,
|
|
45
|
-
dispatched_at: Time.
|
|
44
|
+
dispatched_at: Time.now.utc,
|
|
46
45
|
node_id: node_id,
|
|
47
46
|
status: status
|
|
48
47
|
}
|
|
@@ -4,7 +4,6 @@
|
|
|
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 'json'
|
|
9
8
|
require_relative 'registry'
|
|
10
9
|
|
|
@@ -19,7 +18,7 @@ module Kaal
|
|
|
19
18
|
# @example Usage
|
|
20
19
|
# redis = Redis.new(url: ENV['REDIS_URL'])
|
|
21
20
|
# registry = Kaal::Dispatch::RedisEngine.new(redis, namespace: 'myapp')
|
|
22
|
-
# registry.log_dispatch('daily_report', Time.
|
|
21
|
+
# registry.log_dispatch('daily_report', Time.now.utc, 'node-1')
|
|
23
22
|
class RedisEngine < Registry
|
|
24
23
|
# Default TTL for dispatch records (7 days in seconds)
|
|
25
24
|
DEFAULT_TTL = 7 * 24 * 60 * 60
|
|
@@ -50,7 +49,7 @@ module Kaal
|
|
|
50
49
|
record = {
|
|
51
50
|
key: key,
|
|
52
51
|
fire_time: fire_time.to_i,
|
|
53
|
-
dispatched_at: Time.
|
|
52
|
+
dispatched_at: Time.now.utc.to_i,
|
|
54
53
|
node_id: node_id,
|
|
55
54
|
status: status
|
|
56
55
|
}
|
data/lib/kaal/registry.rb
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'pathname'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
# Resolves environment and path information for plain-Ruby runtime loading.
|
|
11
|
+
class RuntimeContext
|
|
12
|
+
DEFAULT_ENVIRONMENT_NAME = 'development'
|
|
13
|
+
ENVIRONMENT_KEYS = %w[KAAL_ENV RAILS_ENV APP_ENV RACK_ENV].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :environment_name, :root_path
|
|
16
|
+
|
|
17
|
+
def self.default(env: ENV, root_path: Dir.pwd)
|
|
18
|
+
new(root_path: root_path, environment_name: environment_name_from(env))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.environment_name_from(env)
|
|
22
|
+
ENVIRONMENT_KEYS.each do |key|
|
|
23
|
+
value = env[key].to_s.strip
|
|
24
|
+
return value unless value.empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
DEFAULT_ENVIRONMENT_NAME
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(root_path:, environment_name:)
|
|
31
|
+
@root_path = Pathname.new(root_path)
|
|
32
|
+
@environment_name = environment_name.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolve_path(path)
|
|
36
|
+
return path.to_s if Pathname.new(path).absolute?
|
|
37
|
+
|
|
38
|
+
root_path.join(path).to_s
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
# Loads scheduler.yml at framework boot time while respecting missing-file policy.
|
|
9
|
+
class SchedulerBootLoader
|
|
10
|
+
def initialize(configuration_provider:, logger:, runtime_context:, load_scheduler_file:)
|
|
11
|
+
@configuration_provider = configuration_provider
|
|
12
|
+
@logger = logger
|
|
13
|
+
@runtime_context = runtime_context
|
|
14
|
+
@load_scheduler_file = load_scheduler_file
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load_on_boot
|
|
18
|
+
load_on_boot!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load_on_boot!
|
|
22
|
+
configuration = fetch_configuration
|
|
23
|
+
return unless configuration
|
|
24
|
+
|
|
25
|
+
return load_scheduler_file if configuration.scheduler_missing_file_policy == :error
|
|
26
|
+
|
|
27
|
+
scheduler_path = configuration.scheduler_config_path.to_s.strip
|
|
28
|
+
return if scheduler_path.empty?
|
|
29
|
+
|
|
30
|
+
absolute_path = @runtime_context.resolve_path(scheduler_path)
|
|
31
|
+
unless File.exist?(absolute_path)
|
|
32
|
+
@logger&.warn("Scheduler file not found at #{absolute_path}")
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
load_scheduler_file
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def load_scheduler_file
|
|
42
|
+
@load_scheduler_file.call
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fetch_configuration
|
|
46
|
+
@configuration_provider.call
|
|
47
|
+
rescue NameError => e
|
|
48
|
+
@logger&.debug("Skipping scheduler file boot load due to configuration error: #{e.message}")
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
# Invokes a previously registered signal handler when it is safe to do so.
|
|
9
|
+
class SignalHandlerChain
|
|
10
|
+
RESERVED_COMMAND_HANDLERS = %w[DEFAULT IGNORE].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(signal:, previous_handler:, logger:)
|
|
13
|
+
@signal = signal
|
|
14
|
+
@previous_handler = previous_handler
|
|
15
|
+
@logger = logger
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(...)
|
|
19
|
+
return unless @previous_handler
|
|
20
|
+
|
|
21
|
+
case @previous_handler
|
|
22
|
+
when Proc, Method
|
|
23
|
+
invoke_callable(...)
|
|
24
|
+
when String
|
|
25
|
+
return if RESERVED_COMMAND_HANDLERS.include?(@previous_handler)
|
|
26
|
+
|
|
27
|
+
@logger&.debug("Previous #{@signal} handler was a command: #{@previous_handler}")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def invoke_callable(*args)
|
|
34
|
+
arity = @previous_handler.arity
|
|
35
|
+
return @previous_handler.call if arity.zero?
|
|
36
|
+
|
|
37
|
+
argument_length = args.length
|
|
38
|
+
argument_count = arity.negative? ? argument_length : [arity, argument_length].min
|
|
39
|
+
@previous_handler.call(*args.first(argument_count))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
module Kaal
|
|
8
|
+
# Installs signal handlers while preserving the previous handlers for chaining.
|
|
9
|
+
class SignalHandlerInstaller
|
|
10
|
+
SIGNALS = %w[TERM INT].freeze
|
|
11
|
+
IGNORE_HANDLER = 'IGNORE'
|
|
12
|
+
|
|
13
|
+
def initialize(signal_module: Signal)
|
|
14
|
+
@signal_module = signal_module
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def install(signals: SIGNALS)
|
|
18
|
+
signals.each_with_object({}) do |signal, previous_handlers|
|
|
19
|
+
previous_handler = capture_previous_handler(signal)
|
|
20
|
+
@signal_module.trap(signal) { yield(signal, previous_handler) }
|
|
21
|
+
previous_handlers[signal] = previous_handler
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def capture_previous_handler(signal)
|
|
28
|
+
previous_handler = @signal_module.trap(signal, IGNORE_HANDLER)
|
|
29
|
+
restore_previous_handler(signal, previous_handler)
|
|
30
|
+
previous_handler
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def restore_previous_handler(signal, previous_handler)
|
|
34
|
+
return unless previous_handler && previous_handler != IGNORE_HANDLER
|
|
35
|
+
|
|
36
|
+
@signal_module.trap(signal, previous_handler)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/kaal/runtime.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'kaal/runtime/runtime_context'
|
|
8
|
+
require 'kaal/runtime/scheduler_boot_loader'
|
|
9
|
+
require 'kaal/runtime/signal_handler_chain'
|
|
10
|
+
require 'kaal/runtime/signal_handler_installer'
|
|
11
|
+
|
|
12
|
+
module Kaal
|
|
13
|
+
# Runtime wiring and lifecycle helpers.
|
|
14
|
+
module Runtime
|
|
15
|
+
RuntimeContext = ::Kaal::RuntimeContext
|
|
16
|
+
SchedulerBootLoader = ::Kaal::SchedulerBootLoader
|
|
17
|
+
SignalHandlerChain = ::Kaal::SignalHandlerChain
|
|
18
|
+
SignalHandlerInstaller = ::Kaal::SignalHandlerInstaller
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'kaal/support/hash_tools'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
# Shared deep hash key transformation helpers for scheduler payloads.
|
|
11
|
+
module SchedulerHashTransform
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def stringify_keys(object)
|
|
15
|
+
Kaal::Support::HashTools.stringify_keys(object)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def symbolize_keys_deep(object)
|
|
19
|
+
Kaal::Support::HashTools.symbolize_keys(object)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
class SchedulerFileLoader
|
|
9
|
+
# Exposes hash/placeholder helpers to extracted scheduler-file collaborators.
|
|
10
|
+
class HelperBundle
|
|
11
|
+
def initialize(loader:)
|
|
12
|
+
@loader = loader
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stringify_keys(payload)
|
|
16
|
+
@loader.send(:stringify_keys, payload)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolve_placeholders(value, context)
|
|
20
|
+
@loader.send(:resolve_placeholders, value, context)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_placeholders(value, key:)
|
|
24
|
+
@loader.send(:validate_placeholders, value, key:)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|