tasker-rb 0.1.3-x86_64-linux
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.so +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,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
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
# Clean Handlers Domain API
|
|
5
|
+
#
|
|
6
|
+
# This module provides the primary public interface for working with TaskerCore handlers.
|
|
7
|
+
# It's organized into two main namespaces: Steps and Tasks, mirroring the Rails engine
|
|
8
|
+
# architecture while providing a clean Ruby interface with enhanced type safety.
|
|
9
|
+
#
|
|
10
|
+
# The Handlers namespace serves as the recommended entry point for handler operations,
|
|
11
|
+
# abstracting the underlying implementation details while preserving method signatures
|
|
12
|
+
# that developers familiar with the Rails engine will recognize.
|
|
13
|
+
#
|
|
14
|
+
# @example Creating and using a step handler
|
|
15
|
+
# # Define a handler class
|
|
16
|
+
# class ProcessPaymentHandler < TaskerCore::Handlers::Steps::Base
|
|
17
|
+
# def call(context)
|
|
18
|
+
# # Access task context
|
|
19
|
+
# amount = context.get_task_field('amount')
|
|
20
|
+
# currency = context.get_task_field('currency')
|
|
21
|
+
#
|
|
22
|
+
# # Process payment logic
|
|
23
|
+
# result = charge_payment(amount, currency)
|
|
24
|
+
#
|
|
25
|
+
# # Return results
|
|
26
|
+
# success(result: { payment_id: result.id, status: "succeeded" })
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# # Create instance with configuration
|
|
31
|
+
# handler = TaskerCore::Handlers::Steps.create(
|
|
32
|
+
# ProcessPaymentHandler,
|
|
33
|
+
# config: { timeout: 30, max_attempts: 3 }
|
|
34
|
+
# )
|
|
35
|
+
#
|
|
36
|
+
# @example Validating handler implementation
|
|
37
|
+
# validation = TaskerCore::Handlers::Steps.validate(ProcessPaymentHandler)
|
|
38
|
+
# # => {
|
|
39
|
+
# # valid: true,
|
|
40
|
+
# # missing_required: [],
|
|
41
|
+
# # optional_implemented: [:process_results],
|
|
42
|
+
# # handler_class: "ProcessPaymentHandler"
|
|
43
|
+
# # }
|
|
44
|
+
#
|
|
45
|
+
# if validation[:valid]
|
|
46
|
+
# puts "Handler implements all required methods"
|
|
47
|
+
# else
|
|
48
|
+
# puts "Missing: #{validation[:missing_required].join(', ')}"
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# @example Using API handlers for HTTP operations
|
|
52
|
+
# class FetchUserHandler < TaskerCore::Handlers::Steps::API
|
|
53
|
+
# def call(context)
|
|
54
|
+
# user_id = context.get_task_field('user_id')
|
|
55
|
+
#
|
|
56
|
+
# # Automatic error classification and retry logic
|
|
57
|
+
# response = get("/users/#{user_id}")
|
|
58
|
+
#
|
|
59
|
+
# success(result: response.body)
|
|
60
|
+
# end
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# @example Task-level handler for workflow coordination
|
|
64
|
+
# # Task handlers coordinate multiple steps
|
|
65
|
+
# result = TaskerCore::Handlers::Tasks.handle(task_uuid: "123-456")
|
|
66
|
+
# # => Orchestrates all steps for the task
|
|
67
|
+
#
|
|
68
|
+
# Architecture:
|
|
69
|
+
# - **Steps**: Individual business logic units (payment processing, API calls, etc.)
|
|
70
|
+
# - **Tasks**: Workflow orchestration and step coordination
|
|
71
|
+
# - **API**: Specialized step handlers for HTTP operations with automatic retry
|
|
72
|
+
#
|
|
73
|
+
# Method Signature:
|
|
74
|
+
# Cross-language standard handler signature (TAS-96):
|
|
75
|
+
# - `call(context)` - Primary handler execution with unified StepContext
|
|
76
|
+
# - `context.task` - Task wrapper with context data
|
|
77
|
+
# - `context.workflow_step` - Workflow step wrapper with execution state
|
|
78
|
+
# - `context.dependency_results` - Results from parent steps
|
|
79
|
+
#
|
|
80
|
+
# @see TaskerCore::Handlers::Steps For step-level business logic
|
|
81
|
+
# @see TaskerCore::Handlers::Tasks For task-level orchestration
|
|
82
|
+
# @see TaskerCore::StepHandler::Base For low-level step handler implementation
|
|
83
|
+
# @see TaskerCore::StepHandler::Api For HTTP-based handlers
|
|
84
|
+
module Handlers
|
|
85
|
+
# Step Handler API with preserved method signatures
|
|
86
|
+
module Steps
|
|
87
|
+
# Import the existing base step handler with preserved signatures
|
|
88
|
+
require_relative 'step_handler/base'
|
|
89
|
+
require_relative 'step_handler/mixins' # TAS-112: Composition pattern mixins
|
|
90
|
+
require_relative 'step_handler/api'
|
|
91
|
+
require_relative 'step_handler/decision' # TAS-53: Decision point handlers
|
|
92
|
+
require_relative 'step_handler/batchable' # TAS-59: Batch processing handlers
|
|
93
|
+
|
|
94
|
+
# Re-export with clean namespace
|
|
95
|
+
Base = TaskerCore::StepHandler::Base
|
|
96
|
+
|
|
97
|
+
# TAS-112: Composition Pattern - Mixin modules (preferred)
|
|
98
|
+
# Use these with `include` in handlers that inherit from Base
|
|
99
|
+
Mixins = TaskerCore::StepHandler::Mixins
|
|
100
|
+
|
|
101
|
+
# Legacy class aliases (deprecated - use Mixins instead)
|
|
102
|
+
API = TaskerCore::StepHandler::Api
|
|
103
|
+
Decision = TaskerCore::StepHandler::Decision
|
|
104
|
+
Batchable = TaskerCore::StepHandler::Batchable
|
|
105
|
+
|
|
106
|
+
class << self
|
|
107
|
+
# Create a new step handler instance
|
|
108
|
+
# @param handler_class [Class] Handler class to instantiate
|
|
109
|
+
# @param config [Hash] Handler configuration
|
|
110
|
+
# @return [Object] Handler instance
|
|
111
|
+
def create(handler_class, config: {})
|
|
112
|
+
handler_class.new(config: config)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Validate step handler implementation
|
|
116
|
+
# @param handler_class [Class] Handler class to validate
|
|
117
|
+
# @return [Hash] Validation result
|
|
118
|
+
def validate(handler_class)
|
|
119
|
+
required_methods = %i[process]
|
|
120
|
+
optional_methods = %i[process_results handle]
|
|
121
|
+
|
|
122
|
+
missing = required_methods.reject { |method| handler_class.method_defined?(method) }
|
|
123
|
+
present_optional = optional_methods.select { |method| handler_class.method_defined?(method) }
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
valid: missing.empty?,
|
|
127
|
+
missing_required: missing,
|
|
128
|
+
optional_implemented: present_optional,
|
|
129
|
+
handler_class: handler_class.name
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Task Handler API
|
|
136
|
+
module Tasks
|
|
137
|
+
require_relative 'task_handler/base'
|
|
138
|
+
|
|
139
|
+
# Re-export with clean namespace
|
|
140
|
+
Base = TaskHandler
|
|
141
|
+
|
|
142
|
+
class << self
|
|
143
|
+
# Handle task with preserved signature
|
|
144
|
+
# @param task_uuid [Integer] Task ID to handle
|
|
145
|
+
# @return [Object] Task handle result
|
|
146
|
+
def handle(task_uuid)
|
|
147
|
+
Base.new.handle(task_uuid)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Create task handler instance
|
|
151
|
+
# @param config [Hash] Handler configuration
|
|
152
|
+
# @return [TaskHandler] Handler instance
|
|
153
|
+
def create(config: {})
|
|
154
|
+
Base.new(config: config)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
|
|
5
|
+
module TaskerCore
|
|
6
|
+
module Internal
|
|
7
|
+
# Simple stub for OrchestrationManager to satisfy StepHandler::Base dependencies
|
|
8
|
+
# In the FFI worker context, we don't need the full Rails engine infrastructure
|
|
9
|
+
class OrchestrationManager
|
|
10
|
+
include Singleton
|
|
11
|
+
|
|
12
|
+
def orchestration_system
|
|
13
|
+
# Return a simple stub that satisfies the interface expected by handlers
|
|
14
|
+
NullOrchestrationSystem.new
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Minimal orchestration system stub for FFI worker context
|
|
19
|
+
class NullOrchestrationSystem
|
|
20
|
+
def method_missing(_method_name, *_args, **_kwargs)
|
|
21
|
+
# Log that the method was called but return nil
|
|
22
|
+
# This allows handlers to work without crashing
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|