tasker-rb 0.1.3-arm64-darwin
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 +7 -0
- data/README.md +55 -0
- data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
- data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
- data/lib/tasker_core/bootstrap.rb +394 -0
- data/lib/tasker_core/client.rb +165 -0
- data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
- data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
- data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
- data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
- data/lib/tasker_core/domain_events.rb +43 -0
- data/lib/tasker_core/errors/CLAUDE.md +7 -0
- data/lib/tasker_core/errors/common.rb +305 -0
- data/lib/tasker_core/errors/error_classifier.rb +61 -0
- data/lib/tasker_core/errors.rb +4 -0
- data/lib/tasker_core/event_bridge.rb +330 -0
- data/lib/tasker_core/handlers.rb +159 -0
- data/lib/tasker_core/internal.rb +31 -0
- data/lib/tasker_core/logger.rb +234 -0
- data/lib/tasker_core/models.rb +337 -0
- data/lib/tasker_core/observability/types.rb +158 -0
- data/lib/tasker_core/observability.rb +292 -0
- data/lib/tasker_core/registry/handler_registry.rb +453 -0
- data/lib/tasker_core/registry/resolver_chain.rb +258 -0
- data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
- data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
- data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
- data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
- data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
- data/lib/tasker_core/registry/resolvers.rb +42 -0
- data/lib/tasker_core/registry.rb +12 -0
- data/lib/tasker_core/step_handler/api.rb +48 -0
- data/lib/tasker_core/step_handler/base.rb +354 -0
- data/lib/tasker_core/step_handler/batchable.rb +50 -0
- data/lib/tasker_core/step_handler/decision.rb +53 -0
- data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
- data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
- data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
- data/lib/tasker_core/step_handler/mixins.rb +66 -0
- data/lib/tasker_core/subscriber.rb +212 -0
- data/lib/tasker_core/task_handler/base.rb +254 -0
- data/lib/tasker_core/tasker_rb.bundle +0 -0
- data/lib/tasker_core/template_discovery.rb +181 -0
- data/lib/tasker_core/test_environment.rb +313 -0
- data/lib/tasker_core/tracing.rb +166 -0
- data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
- data/lib/tasker_core/types/client_types.rb +145 -0
- data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
- data/lib/tasker_core/types/error_types.rb +72 -0
- data/lib/tasker_core/types/simple_message.rb +151 -0
- data/lib/tasker_core/types/step_context.rb +328 -0
- data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
- data/lib/tasker_core/types/step_message.rb +112 -0
- data/lib/tasker_core/types/step_types.rb +207 -0
- data/lib/tasker_core/types/task_template.rb +240 -0
- data/lib/tasker_core/types/task_types.rb +148 -0
- data/lib/tasker_core/types.rb +132 -0
- data/lib/tasker_core/version.rb +13 -0
- data/lib/tasker_core/worker/CLAUDE.md +7 -0
- data/lib/tasker_core/worker/event_poller.rb +224 -0
- data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
- data/lib/tasker_core.rb +161 -0
- metadata +292 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
|
|
5
|
+
module TaskerCore
|
|
6
|
+
module DomainEvents
|
|
7
|
+
# TAS-65: Registry for domain event subscribers
|
|
8
|
+
#
|
|
9
|
+
# Manages the lifecycle of domain event subscribers. Subscribers are registered
|
|
10
|
+
# at bootstrap time and started/stopped together with the worker.
|
|
11
|
+
#
|
|
12
|
+
# @example Registering subscribers at bootstrap
|
|
13
|
+
# registry = TaskerCore::DomainEvents::SubscriberRegistry.instance
|
|
14
|
+
#
|
|
15
|
+
# # Register subscriber classes (instantiation deferred)
|
|
16
|
+
# registry.register(PaymentEventSubscriber)
|
|
17
|
+
# registry.register(MetricsSubscriber)
|
|
18
|
+
#
|
|
19
|
+
# # Or register instances directly
|
|
20
|
+
# registry.register_instance(CustomSubscriber.new(some_config))
|
|
21
|
+
#
|
|
22
|
+
# # Start all subscribers
|
|
23
|
+
# registry.start_all!
|
|
24
|
+
#
|
|
25
|
+
# @example Stopping all subscribers
|
|
26
|
+
# registry.stop_all!
|
|
27
|
+
#
|
|
28
|
+
class SubscriberRegistry
|
|
29
|
+
include Singleton
|
|
30
|
+
|
|
31
|
+
attr_reader :logger, :subscribers
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@logger = TaskerCore::Logger.instance
|
|
35
|
+
@subscribers = []
|
|
36
|
+
@started = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Register a subscriber class
|
|
40
|
+
#
|
|
41
|
+
# The class will be instantiated when start_all! is called.
|
|
42
|
+
#
|
|
43
|
+
# @param subscriber_class [Class] A subclass of BaseSubscriber
|
|
44
|
+
# @return [void]
|
|
45
|
+
def register(subscriber_class)
|
|
46
|
+
unless subscriber_class < BaseSubscriber
|
|
47
|
+
raise ArgumentError, "#{subscriber_class} must be a subclass of BaseSubscriber"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
logger.info "SubscriberRegistry: Registered #{subscriber_class.name}"
|
|
51
|
+
@subscriber_classes ||= []
|
|
52
|
+
@subscriber_classes << subscriber_class
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Register a subscriber instance directly
|
|
56
|
+
#
|
|
57
|
+
# Use this when your subscriber needs custom initialization.
|
|
58
|
+
#
|
|
59
|
+
# @param subscriber [BaseSubscriber] A subscriber instance
|
|
60
|
+
# @return [void]
|
|
61
|
+
def register_instance(subscriber)
|
|
62
|
+
raise ArgumentError, "Expected BaseSubscriber, got #{subscriber.class}" unless subscriber.is_a?(BaseSubscriber)
|
|
63
|
+
|
|
64
|
+
logger.info "SubscriberRegistry: Registered instance of #{subscriber.class.name}"
|
|
65
|
+
@subscribers << subscriber
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Start all registered subscribers
|
|
69
|
+
#
|
|
70
|
+
# Instantiates registered classes and starts all subscribers.
|
|
71
|
+
#
|
|
72
|
+
# @return [void]
|
|
73
|
+
def start_all!
|
|
74
|
+
return if @started
|
|
75
|
+
|
|
76
|
+
# Instantiate registered classes
|
|
77
|
+
(@subscriber_classes || []).each do |klass|
|
|
78
|
+
@subscribers << klass.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Start all subscribers
|
|
82
|
+
@subscribers.each do |subscriber|
|
|
83
|
+
subscriber.start!
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
logger.error "Failed to start #{subscriber.class.name}: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
@started = true
|
|
89
|
+
logger.info "SubscriberRegistry: Started #{@subscribers.size} subscribers"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Stop all subscribers
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
95
|
+
def stop_all!
|
|
96
|
+
return unless @started
|
|
97
|
+
|
|
98
|
+
@subscribers.each do |subscriber|
|
|
99
|
+
subscriber.stop!
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
logger.error "Failed to stop #{subscriber.class.name}: #{e.message}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@started = false
|
|
105
|
+
logger.info 'SubscriberRegistry: Stopped all subscribers'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if subscribers have been started
|
|
109
|
+
#
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
def started?
|
|
112
|
+
@started
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get count of registered subscribers
|
|
116
|
+
#
|
|
117
|
+
# @return [Integer]
|
|
118
|
+
def count
|
|
119
|
+
@subscribers.size + (@subscriber_classes&.size || 0)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get subscriber statistics
|
|
123
|
+
#
|
|
124
|
+
# @return [Hash]
|
|
125
|
+
def stats
|
|
126
|
+
{
|
|
127
|
+
started: @started,
|
|
128
|
+
subscriber_count: @subscribers.size,
|
|
129
|
+
active_count: @subscribers.count(&:active?),
|
|
130
|
+
subscribers: @subscribers.map do |s|
|
|
131
|
+
{
|
|
132
|
+
class: s.class.name,
|
|
133
|
+
active: s.active?,
|
|
134
|
+
patterns: s.class.patterns
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Reset the registry (for testing)
|
|
141
|
+
#
|
|
142
|
+
# @note This stops all subscribers first
|
|
143
|
+
def reset!
|
|
144
|
+
stop_all! if @started
|
|
145
|
+
@subscribers.clear
|
|
146
|
+
@subscriber_classes&.clear
|
|
147
|
+
@started = false
|
|
148
|
+
logger.info 'SubscriberRegistry: Reset'
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# TAS-65: Domain Events Module
|
|
4
|
+
#
|
|
5
|
+
# Provides infrastructure for custom domain event publishers.
|
|
6
|
+
# Domain events are business events (e.g., "order.processed", "payment.completed")
|
|
7
|
+
# published after step execution based on YAML declarations.
|
|
8
|
+
#
|
|
9
|
+
# @example Using the domain events infrastructure
|
|
10
|
+
# # Register custom publishers at bootstrap
|
|
11
|
+
# require 'tasker_core/domain_events'
|
|
12
|
+
#
|
|
13
|
+
# class PaymentEventPublisher < TaskerCore::DomainEvents::BasePublisher
|
|
14
|
+
# def name
|
|
15
|
+
# 'PaymentEventPublisher'
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# def transform_payload(step_result, event_declaration, step_context)
|
|
19
|
+
# {
|
|
20
|
+
# payment_id: step_result[:result][:payment_id],
|
|
21
|
+
# amount: step_result[:result][:amount],
|
|
22
|
+
# status: step_result[:success] ? 'succeeded' : 'failed'
|
|
23
|
+
# }
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# TaskerCore::DomainEvents::PublisherRegistry.instance.register(
|
|
28
|
+
# PaymentEventPublisher.new
|
|
29
|
+
# )
|
|
30
|
+
#
|
|
31
|
+
module TaskerCore
|
|
32
|
+
module DomainEvents
|
|
33
|
+
# Load domain events components
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Publishers
|
|
38
|
+
require_relative 'domain_events/base_publisher'
|
|
39
|
+
require_relative 'domain_events/publisher_registry'
|
|
40
|
+
|
|
41
|
+
# Subscribers
|
|
42
|
+
require_relative 'domain_events/base_subscriber'
|
|
43
|
+
require_relative 'domain_events/subscriber_registry'
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
module Errors
|
|
5
|
+
# Base error class for all TaskerCore-related errors
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Raised when there are configuration-related issues in TaskerCore
|
|
9
|
+
class ConfigurationError < Error; end
|
|
10
|
+
|
|
11
|
+
# Base class for all TaskerCore-specific errors that occur during workflow execution
|
|
12
|
+
# Maps to the comprehensive error system in Rust orchestration/errors.rs
|
|
13
|
+
class ProceduralError < Error; end
|
|
14
|
+
|
|
15
|
+
# Raised when there are orchestration-related issues in TaskerCore
|
|
16
|
+
class OrchestrationError < Error; end
|
|
17
|
+
|
|
18
|
+
# Raised when there are database-related issues in TaskerCore
|
|
19
|
+
class DatabaseError < Error; end
|
|
20
|
+
|
|
21
|
+
# Error indicating a step failed but should be retried with backoff
|
|
22
|
+
# Maps to Rust StepExecutionError::Retryable
|
|
23
|
+
#
|
|
24
|
+
# Use this error when an operation fails due to temporary conditions like:
|
|
25
|
+
# - Network timeouts
|
|
26
|
+
# - Rate limiting (429 status)
|
|
27
|
+
# - Server errors (5xx status)
|
|
28
|
+
# - Temporary service unavailability
|
|
29
|
+
# - Resource exhaustion that may resolve
|
|
30
|
+
#
|
|
31
|
+
# @example Basic retryable error
|
|
32
|
+
# raise TaskerCore::Errors::RetryableError, "Payment service timeout"
|
|
33
|
+
#
|
|
34
|
+
# @example With retry delay
|
|
35
|
+
# raise TaskerCore::Errors::RetryableError.new("Rate limited", retry_after: 60)
|
|
36
|
+
#
|
|
37
|
+
# @example With context for monitoring
|
|
38
|
+
# raise TaskerCore::Errors::RetryableError.new(
|
|
39
|
+
# "External API unavailable",
|
|
40
|
+
# retry_after: 30,
|
|
41
|
+
# context: { service: 'billing_api', error_code: 503 }
|
|
42
|
+
# )
|
|
43
|
+
class RetryableError < ProceduralError
|
|
44
|
+
# @return [Integer, nil] Suggested retry delay in seconds
|
|
45
|
+
attr_reader :retry_after
|
|
46
|
+
|
|
47
|
+
# @return [Hash] Additional context for error monitoring and debugging
|
|
48
|
+
attr_reader :context
|
|
49
|
+
|
|
50
|
+
# @param message [String] Error message
|
|
51
|
+
# @param retry_after [Integer, nil] Suggested retry delay in seconds
|
|
52
|
+
# @param context [Hash] Additional context for monitoring
|
|
53
|
+
def initialize(message, retry_after: nil, context: {})
|
|
54
|
+
super(message)
|
|
55
|
+
@retry_after = retry_after
|
|
56
|
+
@context = context
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get the error class name for Rust FFI compatibility
|
|
60
|
+
def error_class
|
|
61
|
+
'RetryableError'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Error indicating a step failed permanently and should not be retried
|
|
66
|
+
# Maps to Rust StepExecutionError::Permanent
|
|
67
|
+
#
|
|
68
|
+
# Use this error when an operation fails due to permanent conditions like:
|
|
69
|
+
# - Invalid request data (400 status)
|
|
70
|
+
# - Authentication/authorization failures (401/403 status)
|
|
71
|
+
# - Validation errors (422 status)
|
|
72
|
+
# - Resource not found when it should exist (404 status in some contexts)
|
|
73
|
+
# - Business logic violations
|
|
74
|
+
# - Configuration errors
|
|
75
|
+
#
|
|
76
|
+
# @example Basic permanent error
|
|
77
|
+
# raise TaskerCore::Errors::PermanentError, "Invalid user ID format"
|
|
78
|
+
#
|
|
79
|
+
# @example With error code for categorization
|
|
80
|
+
# raise TaskerCore::Errors::PermanentError.new(
|
|
81
|
+
# "Insufficient funds for transaction",
|
|
82
|
+
# error_code: 'INSUFFICIENT_FUNDS'
|
|
83
|
+
# )
|
|
84
|
+
#
|
|
85
|
+
# @example With context for monitoring
|
|
86
|
+
# raise TaskerCore::Errors::PermanentError.new(
|
|
87
|
+
# "User not authorized for this operation",
|
|
88
|
+
# error_code: 'AUTHORIZATION_FAILED',
|
|
89
|
+
# context: { user_id: 123, operation: 'admin_access' }
|
|
90
|
+
# )
|
|
91
|
+
class PermanentError < ProceduralError
|
|
92
|
+
# @return [String, nil] Machine-readable error code for categorization
|
|
93
|
+
attr_reader :error_code
|
|
94
|
+
|
|
95
|
+
# @return [Hash] Additional context for error monitoring and debugging
|
|
96
|
+
attr_reader :context
|
|
97
|
+
|
|
98
|
+
# @param message [String] Error message
|
|
99
|
+
# @param error_code [String, nil] Machine-readable error code
|
|
100
|
+
# @param context [Hash] Additional context for monitoring
|
|
101
|
+
def initialize(message, error_code: nil, context: {})
|
|
102
|
+
super(message)
|
|
103
|
+
@error_code = error_code
|
|
104
|
+
@context = context
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get the error class name for Rust FFI compatibility
|
|
108
|
+
def error_class
|
|
109
|
+
'PermanentError'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Error indicating a timeout occurred during step execution
|
|
114
|
+
# Maps to Rust StepExecutionError::Timeout
|
|
115
|
+
#
|
|
116
|
+
# Use this error when an operation fails due to timeout conditions:
|
|
117
|
+
# - Step execution timeout
|
|
118
|
+
# - Network request timeout
|
|
119
|
+
# - Database operation timeout
|
|
120
|
+
# - External service timeout
|
|
121
|
+
#
|
|
122
|
+
# @example Basic timeout error
|
|
123
|
+
# raise TaskerCore::Errors::TimeoutError, "Payment processing timed out"
|
|
124
|
+
#
|
|
125
|
+
# @example With timeout duration
|
|
126
|
+
# raise TaskerCore::Errors::TimeoutError.new(
|
|
127
|
+
# "Database query timed out",
|
|
128
|
+
# timeout_duration: 30
|
|
129
|
+
# )
|
|
130
|
+
class TimeoutError < ProceduralError
|
|
131
|
+
# @return [Integer, nil] Timeout duration in seconds
|
|
132
|
+
attr_reader :timeout_duration
|
|
133
|
+
|
|
134
|
+
# @return [Hash] Additional context for error monitoring and debugging
|
|
135
|
+
attr_reader :context
|
|
136
|
+
|
|
137
|
+
# @param message [String] Error message
|
|
138
|
+
# @param timeout_duration [Integer, nil] Timeout duration in seconds
|
|
139
|
+
# @param context [Hash] Additional context for monitoring
|
|
140
|
+
def initialize(message, timeout_duration: nil, context: {})
|
|
141
|
+
super(message)
|
|
142
|
+
@timeout_duration = timeout_duration
|
|
143
|
+
@context = context
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Get the error class name for Rust FFI compatibility
|
|
147
|
+
def error_class
|
|
148
|
+
'TimeoutError'
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Error indicating a network-related failure
|
|
153
|
+
# Maps to Rust StepExecutionError::NetworkError
|
|
154
|
+
#
|
|
155
|
+
# Use this error when an operation fails due to network conditions:
|
|
156
|
+
# - Connection failures
|
|
157
|
+
# - DNS resolution errors
|
|
158
|
+
# - HTTP errors (4xx/5xx when network-related)
|
|
159
|
+
# - TLS/SSL errors
|
|
160
|
+
# - Network timeouts
|
|
161
|
+
#
|
|
162
|
+
# @example Basic network error
|
|
163
|
+
# raise TaskerCore::Errors::NetworkError, "Connection refused"
|
|
164
|
+
#
|
|
165
|
+
# @example With HTTP status code
|
|
166
|
+
# raise TaskerCore::Errors::NetworkError.new(
|
|
167
|
+
# "Service unavailable",
|
|
168
|
+
# status_code: 503
|
|
169
|
+
# )
|
|
170
|
+
class NetworkError < ProceduralError
|
|
171
|
+
# @return [Integer, nil] HTTP status code if applicable
|
|
172
|
+
attr_reader :status_code
|
|
173
|
+
|
|
174
|
+
# @return [Hash] Additional context for error monitoring and debugging
|
|
175
|
+
attr_reader :context
|
|
176
|
+
|
|
177
|
+
# @param message [String] Error message
|
|
178
|
+
# @param status_code [Integer, nil] HTTP status code if applicable
|
|
179
|
+
# @param context [Hash] Additional context for monitoring
|
|
180
|
+
def initialize(message, status_code: nil, context: {})
|
|
181
|
+
super(message)
|
|
182
|
+
@status_code = status_code
|
|
183
|
+
@context = context
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Get the error class name for Rust FFI compatibility
|
|
187
|
+
def error_class
|
|
188
|
+
'NetworkError'
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Error indicating validation failed
|
|
193
|
+
# Maps to Rust OrchestrationError::ValidationError
|
|
194
|
+
#
|
|
195
|
+
# Use this error when data validation fails:
|
|
196
|
+
# - Schema validation errors
|
|
197
|
+
# - Business rule validation
|
|
198
|
+
# - Input format validation
|
|
199
|
+
# - Required field validation
|
|
200
|
+
class ValidationError < PermanentError
|
|
201
|
+
# @return [String, nil] Field that failed validation
|
|
202
|
+
attr_reader :field
|
|
203
|
+
|
|
204
|
+
# @param message [String] Error message
|
|
205
|
+
# @param field [String, nil] Field that failed validation
|
|
206
|
+
# @param error_code [String, nil] Machine-readable error code
|
|
207
|
+
# @param context [Hash] Additional context for monitoring
|
|
208
|
+
def initialize(message, field: nil, error_code: nil, context: {})
|
|
209
|
+
super(message, error_code: error_code, context: context)
|
|
210
|
+
@field = field
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Get the error class name for Rust FFI compatibility
|
|
214
|
+
def error_class
|
|
215
|
+
'ValidationError'
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Error indicating a handler or step was not found
|
|
220
|
+
# Maps to Rust OrchestrationError::HandlerNotFound and StepHandlerNotFound
|
|
221
|
+
class NotFoundError < PermanentError
|
|
222
|
+
# @return [String, nil] Type of resource not found (handler, step, etc.)
|
|
223
|
+
attr_reader :resource_type
|
|
224
|
+
|
|
225
|
+
# @param message [String] Error message
|
|
226
|
+
# @param resource_type [String, nil] Type of resource not found
|
|
227
|
+
# @param error_code [String, nil] Machine-readable error code
|
|
228
|
+
# @param context [Hash] Additional context for monitoring
|
|
229
|
+
def initialize(message, resource_type: nil, error_code: nil, context: {})
|
|
230
|
+
super(message, error_code: error_code, context: context)
|
|
231
|
+
@resource_type = resource_type
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Get the error class name for Rust FFI compatibility
|
|
235
|
+
def error_class
|
|
236
|
+
'NotFoundError'
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Error indicating an FFI operation failed
|
|
241
|
+
# Maps to Rust OrchestrationError::FfiBridgeError
|
|
242
|
+
class FFIError < Error
|
|
243
|
+
# @return [String, nil] FFI operation that failed
|
|
244
|
+
attr_reader :operation
|
|
245
|
+
|
|
246
|
+
# @return [Hash] Additional context for error monitoring and debugging
|
|
247
|
+
attr_reader :context
|
|
248
|
+
|
|
249
|
+
# @param message [String] Error message
|
|
250
|
+
# @param operation [String, nil] FFI operation that failed
|
|
251
|
+
# @param context [Hash] Additional context for monitoring
|
|
252
|
+
def initialize(message, operation: nil, context: {})
|
|
253
|
+
super(message)
|
|
254
|
+
@operation = operation
|
|
255
|
+
@context = context
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Get the error class name for Rust FFI compatibility
|
|
259
|
+
def error_class
|
|
260
|
+
'FFIError'
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Error indicating embedded server operation failed
|
|
265
|
+
# Maps to Rust ServerError types
|
|
266
|
+
#
|
|
267
|
+
# Use this error when embedded server operations fail:
|
|
268
|
+
# - Server startup failures
|
|
269
|
+
# - Server shutdown failures
|
|
270
|
+
# - Configuration errors
|
|
271
|
+
# - Runtime errors
|
|
272
|
+
# - Server already running/not running
|
|
273
|
+
#
|
|
274
|
+
# @example Basic server error
|
|
275
|
+
# raise TaskerCore::Errors::ServerError, "Failed to start embedded server"
|
|
276
|
+
#
|
|
277
|
+
# @example With server operation context
|
|
278
|
+
# raise TaskerCore::Errors::ServerError.new(
|
|
279
|
+
# "Server startup failed",
|
|
280
|
+
# operation: 'start',
|
|
281
|
+
# context: { bind_address: '127.0.0.1:8080', error_code: 'ADDRESS_IN_USE' }
|
|
282
|
+
# )
|
|
283
|
+
class ServerError < Error
|
|
284
|
+
# @return [String, nil] Server operation that failed
|
|
285
|
+
attr_reader :operation
|
|
286
|
+
|
|
287
|
+
# @return [Hash] Additional context for error monitoring and debugging
|
|
288
|
+
attr_reader :context
|
|
289
|
+
|
|
290
|
+
# @param message [String] Error message
|
|
291
|
+
# @param operation [String, nil] Server operation that failed (start, stop, config, etc.)
|
|
292
|
+
# @param context [Hash] Additional context for monitoring
|
|
293
|
+
def initialize(message, operation: nil, context: {})
|
|
294
|
+
super(message)
|
|
295
|
+
@operation = operation
|
|
296
|
+
@context = context
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Get the error class name for Rust FFI compatibility
|
|
300
|
+
def error_class
|
|
301
|
+
'ServerError'
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'common'
|
|
4
|
+
|
|
5
|
+
module TaskerCore
|
|
6
|
+
module Errors
|
|
7
|
+
# ErrorClassifier provides systematic classification of errors for retry logic
|
|
8
|
+
#
|
|
9
|
+
# This classifier determines whether errors should be retried based on their type.
|
|
10
|
+
# It maintains explicit lists of permanent (non-retryable) and retryable error classes.
|
|
11
|
+
#
|
|
12
|
+
# @example Classify a configuration error
|
|
13
|
+
# ErrorClassifier.retryable?(ConfigurationError.new("Missing handler"))
|
|
14
|
+
# # => false
|
|
15
|
+
#
|
|
16
|
+
# @example Classify a network error
|
|
17
|
+
# ErrorClassifier.retryable?(NetworkError.new("Connection timeout"))
|
|
18
|
+
# # => true
|
|
19
|
+
class ErrorClassifier
|
|
20
|
+
# System-level errors that should NEVER be retried
|
|
21
|
+
# These represent fundamental configuration or setup issues that won't resolve on retry
|
|
22
|
+
PERMANENT_ERROR_CLASSES = [
|
|
23
|
+
ConfigurationError, # Configuration issues (missing config, invalid settings)
|
|
24
|
+
PermanentError, # Explicitly marked permanent errors
|
|
25
|
+
ValidationError, # Data validation failures
|
|
26
|
+
NotFoundError, # Resource not found (handler, step, etc.)
|
|
27
|
+
DatabaseError, # Database schema/constraint errors
|
|
28
|
+
FFIError # FFI bridge failures
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# Application errors that are retryable by default
|
|
32
|
+
# These represent transient failures that may resolve on subsequent attempts
|
|
33
|
+
RETRYABLE_ERROR_CLASSES = [
|
|
34
|
+
RetryableError, # Explicitly marked retryable errors
|
|
35
|
+
NetworkError, # Network/connection failures
|
|
36
|
+
TimeoutError # Timeout errors
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# Determine if an error should be retried
|
|
40
|
+
#
|
|
41
|
+
# @param error [StandardError] The error to classify
|
|
42
|
+
# @return [Boolean] true if the error should be retried, false otherwise
|
|
43
|
+
#
|
|
44
|
+
# @note The default behavior is to mark errors as retryable unless they are
|
|
45
|
+
# explicitly in the PERMANENT_ERROR_CLASSES list. This follows the principle
|
|
46
|
+
# that it's safer to retry an error that shouldn't be retried (will eventually
|
|
47
|
+
# hit retry limit) than to not retry an error that should be (will fail immediately).
|
|
48
|
+
def self.retryable?(error)
|
|
49
|
+
# Explicit permanent errors - never retry
|
|
50
|
+
return false if PERMANENT_ERROR_CLASSES.any? { |klass| error.is_a?(klass) }
|
|
51
|
+
|
|
52
|
+
# Explicit retryable errors - always retry
|
|
53
|
+
return true if RETRYABLE_ERROR_CLASSES.any? { |klass| error.is_a?(klass) }
|
|
54
|
+
|
|
55
|
+
# Default: StandardError and subclasses are retryable unless explicitly marked permanent
|
|
56
|
+
# This is a safe default - worst case, we retry until max_attempts is hit
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|