tasker-rb 0.1.1
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/DEVELOPMENT.md +548 -0
- data/README.md +87 -0
- data/ext/tasker_core/Cargo.lock +4720 -0
- data/ext/tasker_core/Cargo.toml +76 -0
- data/ext/tasker_core/extconf.rb +38 -0
- data/ext/tasker_core/src/CLAUDE.md +7 -0
- data/ext/tasker_core/src/bootstrap.rs +320 -0
- data/ext/tasker_core/src/bridge.rs +400 -0
- data/ext/tasker_core/src/client_ffi.rs +173 -0
- data/ext/tasker_core/src/conversions.rs +131 -0
- data/ext/tasker_core/src/diagnostics.rs +57 -0
- data/ext/tasker_core/src/event_handler.rs +179 -0
- data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
- data/ext/tasker_core/src/ffi_logging.rs +245 -0
- data/ext/tasker_core/src/global_event_system.rs +16 -0
- data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
- data/ext/tasker_core/src/lib.rs +41 -0
- data/ext/tasker_core/src/observability_ffi.rs +339 -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/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.so +0 -0
- data/lib/tasker_core/template_discovery.rb +181 -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 +160 -0
- metadata +322 -0
|
@@ -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
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'models'
|
|
4
|
+
|
|
5
|
+
module TaskerCore
|
|
6
|
+
module Worker
|
|
7
|
+
# Event bridge between Rust and Ruby using dry-events
|
|
8
|
+
#
|
|
9
|
+
# Handles bidirectional event communication between the Rust orchestration
|
|
10
|
+
# layer and Ruby business logic handlers. The bridge uses dry-events for
|
|
11
|
+
# Ruby-side pub/sub and FFI for cross-language communication.
|
|
12
|
+
#
|
|
13
|
+
# Event Flow:
|
|
14
|
+
# 1. **Rust → Ruby**: StepExecutionEvent indicates step is ready for processing
|
|
15
|
+
# 2. **Ruby Processing**: Handler executes business logic
|
|
16
|
+
# 3. **Ruby → Rust**: StepExecutionCompletionEvent returns results
|
|
17
|
+
#
|
|
18
|
+
# The EventBridge automatically wraps raw FFI data in accessor objects for
|
|
19
|
+
# developer convenience and maintains event schema validation.
|
|
20
|
+
#
|
|
21
|
+
# @example Publishing step execution (from Rust FFI)
|
|
22
|
+
# # This is called automatically by Rust via FFI when a step is ready
|
|
23
|
+
# bridge = TaskerCore::Worker::EventBridge.instance
|
|
24
|
+
# bridge.publish_step_execution({
|
|
25
|
+
# event_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
26
|
+
# task_uuid: "7c9e6679-7425-40de-944b-e07fc1f90ae7",
|
|
27
|
+
# step_uuid: "123e4567-e89b-12d3-a456-426614174000",
|
|
28
|
+
# task_sequence_step: {
|
|
29
|
+
# task: { context: { amount: 100.00, currency: "USD" } },
|
|
30
|
+
# step: { name: "process_payment", handler_class: "ProcessPaymentHandler" },
|
|
31
|
+
# workflow_step: { state: "in_progress", attempts: 1 }
|
|
32
|
+
# }
|
|
33
|
+
# })
|
|
34
|
+
# # => Publishes 'step.execution.received' event to Ruby subscribers
|
|
35
|
+
#
|
|
36
|
+
# @example Subscribing to step execution events
|
|
37
|
+
# # This is typically done in StepExecutionSubscriber
|
|
38
|
+
# bridge.subscribe_to_step_execution do |event|
|
|
39
|
+
# # Resolve handler
|
|
40
|
+
# handler_class = event[:task_sequence_step].handler_class
|
|
41
|
+
# handler = registry.resolve_handler(handler_class)
|
|
42
|
+
#
|
|
43
|
+
# # Create context and execute handler
|
|
44
|
+
# context = TaskerCore::Types::StepContext.new(event[:task_sequence_step])
|
|
45
|
+
# result = handler.call(context)
|
|
46
|
+
#
|
|
47
|
+
# # Send completion back to Rust
|
|
48
|
+
# bridge.publish_step_completion({
|
|
49
|
+
# event_id: event[:event_id],
|
|
50
|
+
# task_uuid: event[:task_uuid],
|
|
51
|
+
# step_uuid: event[:step_uuid],
|
|
52
|
+
# success: true,
|
|
53
|
+
# result: result
|
|
54
|
+
# })
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
# @example Sending completion back to Rust
|
|
58
|
+
# bridge.publish_step_completion({
|
|
59
|
+
# event_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
60
|
+
# task_uuid: "7c9e6679-7425-40de-944b-e07fc1f90ae7",
|
|
61
|
+
# step_uuid: "123e4567-e89b-12d3-a456-426614174000",
|
|
62
|
+
# success: true,
|
|
63
|
+
# result: { payment_id: "pay_123", status: "succeeded" },
|
|
64
|
+
# metadata: {
|
|
65
|
+
# handler_class: "ProcessPaymentHandler",
|
|
66
|
+
# execution_time_ms: 125
|
|
67
|
+
# }
|
|
68
|
+
# })
|
|
69
|
+
# # => Sends completion to Rust via FFI and publishes 'step.completion.sent'
|
|
70
|
+
#
|
|
71
|
+
# @example Handling errors in step execution
|
|
72
|
+
# begin
|
|
73
|
+
# result = handler.call(context)
|
|
74
|
+
# bridge.publish_step_completion({
|
|
75
|
+
# event_id: event_id,
|
|
76
|
+
# task_uuid: task_uuid,
|
|
77
|
+
# step_uuid: step_uuid,
|
|
78
|
+
# success: true,
|
|
79
|
+
# result: result
|
|
80
|
+
# })
|
|
81
|
+
# rescue TaskerCore::Errors::RetryableError => e
|
|
82
|
+
# bridge.publish_step_completion({
|
|
83
|
+
# event_id: event_id,
|
|
84
|
+
# task_uuid: task_uuid,
|
|
85
|
+
# step_uuid: step_uuid,
|
|
86
|
+
# success: false,
|
|
87
|
+
# error_message: e.message,
|
|
88
|
+
# error_class: e.class.name,
|
|
89
|
+
# retryable: true,
|
|
90
|
+
# retry_after: e.retry_after
|
|
91
|
+
# })
|
|
92
|
+
# rescue TaskerCore::Errors::PermanentError => e
|
|
93
|
+
# bridge.publish_step_completion({
|
|
94
|
+
# event_id: event_id,
|
|
95
|
+
# task_uuid: task_uuid,
|
|
96
|
+
# step_uuid: step_uuid,
|
|
97
|
+
# success: false,
|
|
98
|
+
# error_message: e.message,
|
|
99
|
+
# error_class: e.class.name,
|
|
100
|
+
# retryable: false
|
|
101
|
+
# })
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
# Event Flow Diagram:
|
|
105
|
+
#
|
|
106
|
+
# ```
|
|
107
|
+
# Rust Orchestration EventBridge Ruby Handler
|
|
108
|
+
# ----------------- ----------- ------------
|
|
109
|
+
# | | |
|
|
110
|
+
# | 1. Step ready for execution | |
|
|
111
|
+
# |------------------------------->| |
|
|
112
|
+
# | publish_step_execution | |
|
|
113
|
+
# | | 2. Publish event |
|
|
114
|
+
# | |-------------------------->|
|
|
115
|
+
# | | step.execution.received|
|
|
116
|
+
# | | |
|
|
117
|
+
# | | 3. Execute handler
|
|
118
|
+
# | | |
|
|
119
|
+
# | | 4. Completion |
|
|
120
|
+
# | |<--------------------------|
|
|
121
|
+
# | 5. FFI completion | publish_step_completion|
|
|
122
|
+
# |<-------------------------------| |
|
|
123
|
+
# | send_step_completion_event | |
|
|
124
|
+
# ```
|
|
125
|
+
#
|
|
126
|
+
# Registered Events:
|
|
127
|
+
# - **step.execution.received**: Step ready for execution (Rust → Ruby)
|
|
128
|
+
# - **step.completion.sent**: Step execution completed (Ruby → Rust)
|
|
129
|
+
# - **bridge.error**: Error in event processing
|
|
130
|
+
#
|
|
131
|
+
# Completion Data Validation:
|
|
132
|
+
# The bridge validates completion data before sending to Rust:
|
|
133
|
+
# - **event_id**: Required, UUID of the original execution event
|
|
134
|
+
# - **task_uuid**: Required, UUID of the task
|
|
135
|
+
# - **step_uuid**: Required, UUID of the step
|
|
136
|
+
# - **success**: Required, boolean indicating success/failure
|
|
137
|
+
# - **metadata**: Optional, hash with additional context
|
|
138
|
+
# - **completed_at**: Optional, ISO 8601 timestamp (auto-generated if missing)
|
|
139
|
+
#
|
|
140
|
+
# @see TaskerCore::Subscriber For event subscription implementation
|
|
141
|
+
# @see TaskerCore::Worker::EventPoller For polling mechanism
|
|
142
|
+
# @see TaskerCore::FFI For Rust FFI operations
|
|
143
|
+
# @see TaskerCore::Models::TaskSequenceStepWrapper For data wrappers
|
|
144
|
+
class EventBridge
|
|
145
|
+
include Singleton
|
|
146
|
+
include Dry::Events::Publisher[:tasker_core]
|
|
147
|
+
|
|
148
|
+
attr_reader :logger, :active
|
|
149
|
+
|
|
150
|
+
def initialize
|
|
151
|
+
@logger = TaskerCore::Logger.instance
|
|
152
|
+
@active = true
|
|
153
|
+
|
|
154
|
+
setup_event_schema!
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Check if bridge is active
|
|
158
|
+
def active?
|
|
159
|
+
@active
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Stop the event bridge
|
|
163
|
+
def stop!
|
|
164
|
+
@active = false
|
|
165
|
+
logger.info 'Event bridge stopped'
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Called by Rust FFI when StepExecutionEvent is received
|
|
169
|
+
# This is the entry point for events from Rust
|
|
170
|
+
def publish_step_execution(event_data)
|
|
171
|
+
return unless active?
|
|
172
|
+
|
|
173
|
+
event_data = event_data.to_h.deep_symbolize_keys
|
|
174
|
+
logger.debug "Publishing step execution event: #{event_data[:event_id]}"
|
|
175
|
+
|
|
176
|
+
# Wrap the raw data in accessor objects for easier use
|
|
177
|
+
wrapped_event = wrap_step_execution_event(event_data)
|
|
178
|
+
|
|
179
|
+
# Publish to dry-events subscribers (Ruby handlers)
|
|
180
|
+
publish('step.execution.received', wrapped_event)
|
|
181
|
+
|
|
182
|
+
logger.debug 'Step execution event published successfully'
|
|
183
|
+
true
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
logger.error "Failed to publish step execution: #{e.message}"
|
|
186
|
+
logger.error e.backtrace.join("\n")
|
|
187
|
+
raise
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Subscribe to step execution events (used by StepExecutionSubscriber)
|
|
191
|
+
def subscribe_to_step_execution(&)
|
|
192
|
+
subscribe('step.execution.received', &)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Send completion event back to Rust
|
|
196
|
+
# Called by StepExecutionSubscriber after handler execution
|
|
197
|
+
def publish_step_completion(completion_data)
|
|
198
|
+
return unless active?
|
|
199
|
+
|
|
200
|
+
logger.debug "Sending step completion to Rust: #{completion_data[:event_id]}"
|
|
201
|
+
|
|
202
|
+
# Validate completion data
|
|
203
|
+
validate_completion!(completion_data)
|
|
204
|
+
|
|
205
|
+
# Send to Rust via FFI (TAS-67: complete_step_event takes event_id and completion_data)
|
|
206
|
+
TaskerCore::FFI.complete_step_event(completion_data[:event_id].to_s, completion_data)
|
|
207
|
+
|
|
208
|
+
# Also publish locally for monitoring/debugging
|
|
209
|
+
publish('step.completion.sent', completion_data)
|
|
210
|
+
|
|
211
|
+
logger.debug 'Step completion sent to Rust'
|
|
212
|
+
rescue StandardError => e
|
|
213
|
+
logger.error "Failed to send step completion: #{e.message}"
|
|
214
|
+
logger.error e.backtrace.join("\n")
|
|
215
|
+
raise
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# TAS-125: Send checkpoint yield back to Rust for batch processing
|
|
219
|
+
#
|
|
220
|
+
# Called by batch processing handlers when they want to persist progress
|
|
221
|
+
# and be re-dispatched for continuation. Unlike publish_step_completion,
|
|
222
|
+
# this does NOT complete the step - instead it persists checkpoint data
|
|
223
|
+
# and causes the step to be re-dispatched for continued processing.
|
|
224
|
+
#
|
|
225
|
+
# @param checkpoint_data [Hash] Checkpoint data to persist:
|
|
226
|
+
# - :event_id [String] Required, UUID of the original execution event
|
|
227
|
+
# - :step_uuid [String] Required, UUID of the step
|
|
228
|
+
# - :cursor [Object] Required, position to resume from (Integer, String, or Hash)
|
|
229
|
+
# - :items_processed [Integer] Required, count of items processed so far
|
|
230
|
+
# - :accumulated_results [Hash] Optional, partial results to carry forward
|
|
231
|
+
# @return [Boolean] true if checkpoint was persisted and step re-dispatched
|
|
232
|
+
#
|
|
233
|
+
# @example Yield checkpoint in batch processing
|
|
234
|
+
# bridge.publish_step_checkpoint_yield({
|
|
235
|
+
# event_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
236
|
+
# step_uuid: "123e4567-e89b-12d3-a456-426614174000",
|
|
237
|
+
# cursor: 1000,
|
|
238
|
+
# items_processed: 1000,
|
|
239
|
+
# accumulated_results: { total_amount: 50000.00, processed_count: 1000 }
|
|
240
|
+
# })
|
|
241
|
+
def publish_step_checkpoint_yield(checkpoint_data)
|
|
242
|
+
return false unless active?
|
|
243
|
+
|
|
244
|
+
logger.debug "Sending checkpoint yield to Rust: #{checkpoint_data[:event_id]}"
|
|
245
|
+
|
|
246
|
+
# Validate checkpoint data
|
|
247
|
+
validate_checkpoint_yield!(checkpoint_data)
|
|
248
|
+
|
|
249
|
+
# Send to Rust via FFI (TAS-125)
|
|
250
|
+
success = TaskerCore::FFI.checkpoint_yield_step_event(
|
|
251
|
+
checkpoint_data[:event_id].to_s,
|
|
252
|
+
checkpoint_data
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if success
|
|
256
|
+
# Publish locally for monitoring/debugging
|
|
257
|
+
publish('step.checkpoint_yield.sent', checkpoint_data)
|
|
258
|
+
logger.debug 'Checkpoint yield sent to Rust - step will be re-dispatched'
|
|
259
|
+
else
|
|
260
|
+
logger.warn 'Checkpoint yield failed - checkpoint support may not be configured'
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
success
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
logger.error "Failed to send checkpoint yield: #{e.message}"
|
|
266
|
+
logger.error e.backtrace.join("\n")
|
|
267
|
+
raise
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
def setup_event_schema!
|
|
273
|
+
# Register event types
|
|
274
|
+
register_event('step.execution.received')
|
|
275
|
+
register_event('step.completion.sent')
|
|
276
|
+
register_event('step.checkpoint_yield.sent') # TAS-125
|
|
277
|
+
register_event('bridge.error')
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def wrap_step_execution_event(event_data)
|
|
281
|
+
wrapped = {
|
|
282
|
+
event_id: event_data[:event_id],
|
|
283
|
+
task_uuid: event_data[:task_uuid],
|
|
284
|
+
step_uuid: event_data[:step_uuid],
|
|
285
|
+
task_sequence_step: TaskerCore::Models::TaskSequenceStepWrapper.new(event_data[:task_sequence_step])
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# TAS-29: Expose correlation_id at top level for easy access
|
|
289
|
+
wrapped[:correlation_id] = event_data[:correlation_id] if event_data[:correlation_id]
|
|
290
|
+
wrapped[:parent_correlation_id] = event_data[:parent_correlation_id] if event_data[:parent_correlation_id]
|
|
291
|
+
|
|
292
|
+
# TAS-65 Phase 1.5b: Expose trace_id and span_id for distributed tracing
|
|
293
|
+
wrapped[:trace_id] = event_data[:trace_id] if event_data[:trace_id]
|
|
294
|
+
wrapped[:span_id] = event_data[:span_id] if event_data[:span_id]
|
|
295
|
+
|
|
296
|
+
wrapped
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def validate_completion!(completion_data)
|
|
300
|
+
required_fields = %i[event_id task_uuid step_uuid success]
|
|
301
|
+
missing_fields = required_fields - completion_data.keys
|
|
302
|
+
|
|
303
|
+
if missing_fields.any?
|
|
304
|
+
raise ArgumentError, "Missing required fields in completion: #{missing_fields.join(', ')}"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Ensure metadata is a hash
|
|
308
|
+
completion_data[:metadata] ||= {}
|
|
309
|
+
|
|
310
|
+
# Ensure timestamps
|
|
311
|
+
completion_data[:completed_at] ||= Time.now.utc.iso8601
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# TAS-125: Validate checkpoint yield data before sending to Rust
|
|
315
|
+
def validate_checkpoint_yield!(checkpoint_data)
|
|
316
|
+
required_fields = %i[event_id step_uuid cursor items_processed]
|
|
317
|
+
missing_fields = required_fields - checkpoint_data.keys
|
|
318
|
+
|
|
319
|
+
if missing_fields.any?
|
|
320
|
+
raise ArgumentError, "Missing required fields in checkpoint yield: #{missing_fields.join(', ')}"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Validate items_processed is a non-negative integer
|
|
324
|
+
return if checkpoint_data[:items_processed].is_a?(Integer) && checkpoint_data[:items_processed] >= 0
|
|
325
|
+
|
|
326
|
+
raise ArgumentError, 'items_processed must be a non-negative integer'
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|