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,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
module StepHandler
|
|
5
|
+
module Mixins
|
|
6
|
+
# Decision mixin for TAS-53 Dynamic Workflow Decision Points
|
|
7
|
+
#
|
|
8
|
+
# ## TAS-112: Composition Pattern
|
|
9
|
+
#
|
|
10
|
+
# This module follows the composition-over-inheritance pattern. Instead of
|
|
11
|
+
# inheriting from a specialized Decision handler class, include this mixin
|
|
12
|
+
# in your Base handler.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# ```ruby
|
|
17
|
+
# class MyDecisionHandler < TaskerCore::StepHandler::Base
|
|
18
|
+
# include TaskerCore::StepHandler::Mixins::Decision
|
|
19
|
+
#
|
|
20
|
+
# def call(context)
|
|
21
|
+
# amount = context.get_task_field('amount')
|
|
22
|
+
#
|
|
23
|
+
# if amount < 1000
|
|
24
|
+
# decision_success(
|
|
25
|
+
# steps: ['auto_approve'],
|
|
26
|
+
# result_data: { route_type: 'auto', amount: amount }
|
|
27
|
+
# )
|
|
28
|
+
# else
|
|
29
|
+
# decision_success(
|
|
30
|
+
# steps: ['manager_approval', 'finance_review'],
|
|
31
|
+
# result_data: { route_type: 'dual', amount: amount }
|
|
32
|
+
# )
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
# ```
|
|
37
|
+
#
|
|
38
|
+
# ## No-Branch Pattern
|
|
39
|
+
#
|
|
40
|
+
# ```ruby
|
|
41
|
+
# def call(context)
|
|
42
|
+
# if context.get_task_field('skip_approval')
|
|
43
|
+
# decision_no_branches(result_data: { reason: 'skipped' })
|
|
44
|
+
# else
|
|
45
|
+
# decision_success(steps: ['standard_approval'])
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
# ```
|
|
49
|
+
module Decision
|
|
50
|
+
# Hook called when module is included
|
|
51
|
+
def self.included(base)
|
|
52
|
+
base.extend(ClassMethods)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Class methods added to including class
|
|
56
|
+
module ClassMethods
|
|
57
|
+
# No class methods needed for now
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Override capabilities to include decision-specific features
|
|
61
|
+
def capabilities
|
|
62
|
+
super + %w[decision_point dynamic_workflow step_creation]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Enhanced configuration schema for decision handlers
|
|
66
|
+
def config_schema
|
|
67
|
+
super.merge({
|
|
68
|
+
properties: super[:properties].merge(
|
|
69
|
+
decision_thresholds: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
description: 'Thresholds for decision routing logic'
|
|
72
|
+
},
|
|
73
|
+
decision_metadata: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
description: 'Additional metadata for decision logging'
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# ========================================================================
|
|
82
|
+
# DECISION OUTCOME HELPER METHODS
|
|
83
|
+
# ========================================================================
|
|
84
|
+
|
|
85
|
+
# Return a successful decision outcome that creates specified steps
|
|
86
|
+
#
|
|
87
|
+
# @param steps [Array<String>, String] Step name(s) to create dynamically
|
|
88
|
+
# @param result_data [Hash] Additional result data (route_type, amounts, etc.)
|
|
89
|
+
# @param metadata [Hash] Optional metadata for observability
|
|
90
|
+
# @return [TaskerCore::Types::StepHandlerCallResult] Success result with decision outcome
|
|
91
|
+
#
|
|
92
|
+
# @example Single step
|
|
93
|
+
# decision_success(
|
|
94
|
+
# steps: 'approval_required',
|
|
95
|
+
# result_data: { route_type: 'standard' }
|
|
96
|
+
# )
|
|
97
|
+
#
|
|
98
|
+
# @example Multiple steps
|
|
99
|
+
# decision_success(
|
|
100
|
+
# steps: ['manager_approval', 'finance_review'],
|
|
101
|
+
# result_data: { route_type: 'dual_approval', amount: 10000 }
|
|
102
|
+
# )
|
|
103
|
+
def decision_success(steps:, result_data: {}, metadata: {})
|
|
104
|
+
# Normalize steps to array
|
|
105
|
+
step_names = Array(steps)
|
|
106
|
+
|
|
107
|
+
# Validate step names
|
|
108
|
+
validate_step_names!(step_names)
|
|
109
|
+
|
|
110
|
+
# Create decision outcome using type-safe factory
|
|
111
|
+
outcome = TaskerCore::Types::DecisionPointOutcome.create_steps(step_names)
|
|
112
|
+
|
|
113
|
+
# Build result with decision outcome embedded
|
|
114
|
+
result = result_data.merge(
|
|
115
|
+
decision_point_outcome: outcome.to_h
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Create success result
|
|
119
|
+
TaskerCore::Types::StepHandlerCallResult.success(
|
|
120
|
+
result: result,
|
|
121
|
+
metadata: build_decision_metadata(metadata, outcome)
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Return a decision outcome indicating no additional steps needed
|
|
126
|
+
#
|
|
127
|
+
# Use this when the decision point determines that workflow can proceed
|
|
128
|
+
# without creating any dynamic steps.
|
|
129
|
+
#
|
|
130
|
+
# @param result_data [Hash] Result data explaining why no branches needed
|
|
131
|
+
# @param metadata [Hash] Optional metadata for observability
|
|
132
|
+
# @return [TaskerCore::Types::StepHandlerCallResult] Success result with no-branches outcome
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# decision_no_branches(
|
|
136
|
+
# result_data: { reason: 'amount_below_threshold', amount: 50 }
|
|
137
|
+
# )
|
|
138
|
+
def decision_no_branches(result_data: {}, metadata: {})
|
|
139
|
+
# Create no-branches outcome
|
|
140
|
+
outcome = TaskerCore::Types::DecisionPointOutcome.no_branches
|
|
141
|
+
|
|
142
|
+
# Build result with outcome embedded
|
|
143
|
+
result = result_data.merge(
|
|
144
|
+
decision_point_outcome: outcome.to_h
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Create success result
|
|
148
|
+
TaskerCore::Types::StepHandlerCallResult.success(
|
|
149
|
+
result: result,
|
|
150
|
+
metadata: build_decision_metadata(metadata, outcome)
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Alias for decision_no_branches for cross-language consistency
|
|
155
|
+
alias skip_branches decision_no_branches
|
|
156
|
+
|
|
157
|
+
# Validate that a decision point outcome is properly structured
|
|
158
|
+
#
|
|
159
|
+
# @param outcome [Hash, DecisionPointOutcome] Outcome to validate
|
|
160
|
+
# @raise [TaskerCore::PermanentError] if outcome is invalid
|
|
161
|
+
def validate_decision_outcome!(outcome)
|
|
162
|
+
# Convert to hash if it's a DecisionPointOutcome type
|
|
163
|
+
outcome_hash = if outcome.respond_to?(:to_h)
|
|
164
|
+
outcome.to_h
|
|
165
|
+
elsif outcome.is_a?(Hash)
|
|
166
|
+
outcome
|
|
167
|
+
else
|
|
168
|
+
raise_invalid_outcome!('Outcome must be Hash or DecisionPointOutcome')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Validate type field exists
|
|
172
|
+
outcome_type = outcome_hash[:type] || outcome_hash['type'] ||
|
|
173
|
+
outcome_hash[:outcome_type] || outcome_hash['outcome_type']
|
|
174
|
+
unless %w[NoBranches CreateSteps no_branches create_steps].include?(outcome_type)
|
|
175
|
+
raise_invalid_outcome!("Invalid outcome_type: #{outcome_type}")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Validate CreateSteps has step_names
|
|
179
|
+
normalized_type = outcome_type.downcase.gsub('_', '')
|
|
180
|
+
if normalized_type == 'createsteps'
|
|
181
|
+
step_names = outcome_hash[:step_names] || outcome_hash['step_names']
|
|
182
|
+
validate_step_names!(step_names)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
outcome_hash
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Build a decision result with a custom outcome
|
|
189
|
+
#
|
|
190
|
+
# Use this for advanced scenarios where you need full control over the outcome
|
|
191
|
+
# structure. Most handlers should use decision_success or decision_no_branches.
|
|
192
|
+
#
|
|
193
|
+
# @param outcome [DecisionPointOutcome, Hash] The decision outcome
|
|
194
|
+
# @param result_data [Hash] Additional result data
|
|
195
|
+
# @param metadata [Hash] Optional metadata
|
|
196
|
+
# @return [TaskerCore::Types::StepHandlerCallResult] Success result
|
|
197
|
+
def decision_with_custom_outcome(outcome:, result_data: {}, metadata: {})
|
|
198
|
+
# Validate outcome structure
|
|
199
|
+
validated_outcome = validate_decision_outcome!(outcome)
|
|
200
|
+
|
|
201
|
+
# Build result
|
|
202
|
+
result = result_data.merge(
|
|
203
|
+
decision_point_outcome: validated_outcome
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Create success result
|
|
207
|
+
TaskerCore::Types::StepHandlerCallResult.success(
|
|
208
|
+
result: result,
|
|
209
|
+
metadata: build_decision_metadata(metadata, outcome)
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
# Validate step names for decision outcomes
|
|
216
|
+
def validate_step_names!(step_names)
|
|
217
|
+
unless step_names.is_a?(Array) && !step_names.empty?
|
|
218
|
+
raise_invalid_outcome!('step_names must be non-empty array')
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
unless step_names.all? { |name| name.is_a?(String) && !name.empty? }
|
|
222
|
+
raise_invalid_outcome!('All step names must be non-empty strings')
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
true
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Build metadata for decision outcomes
|
|
229
|
+
def build_decision_metadata(custom_metadata, outcome)
|
|
230
|
+
base_metadata = {
|
|
231
|
+
decision_point: true,
|
|
232
|
+
outcome_type: outcome.type,
|
|
233
|
+
branches_created: outcome.step_names.size,
|
|
234
|
+
processed_at: Time.now.utc.iso8601,
|
|
235
|
+
processed_by: handler_name
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
base_metadata.merge(custom_metadata)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Raise a permanent error for invalid decision outcomes
|
|
242
|
+
def raise_invalid_outcome!(message)
|
|
243
|
+
raise TaskerCore::Errors::PermanentError.new(
|
|
244
|
+
"Invalid decision point outcome: #{message}",
|
|
245
|
+
error_code: 'INVALID_DECISION_OUTCOME',
|
|
246
|
+
context: { error_category: 'validation' }
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# TAS-112: Composition Pattern - Mixin Modules
|
|
4
|
+
#
|
|
5
|
+
# This file exports all step handler mixins. Mixins follow the composition-over-inheritance
|
|
6
|
+
# pattern where specialized functionality is added via `include` rather than subclassing.
|
|
7
|
+
#
|
|
8
|
+
# ## Usage
|
|
9
|
+
#
|
|
10
|
+
# ```ruby
|
|
11
|
+
# class MyApiHandler < TaskerCore::StepHandler::Base
|
|
12
|
+
# include TaskerCore::StepHandler::Mixins::API
|
|
13
|
+
#
|
|
14
|
+
# def call(context)
|
|
15
|
+
# response = get('/users')
|
|
16
|
+
# success(result: response.body)
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# class MyDecisionHandler < TaskerCore::StepHandler::Base
|
|
21
|
+
# include TaskerCore::StepHandler::Mixins::Decision
|
|
22
|
+
#
|
|
23
|
+
# def call(context)
|
|
24
|
+
# decision_success(steps: ['next_step'])
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# class MyBatchHandler < TaskerCore::StepHandler::Base
|
|
29
|
+
# include TaskerCore::StepHandler::Mixins::Batchable
|
|
30
|
+
#
|
|
31
|
+
# def call(context)
|
|
32
|
+
# configs = create_cursor_configs(1000, 5)
|
|
33
|
+
# create_batches_outcome(worker_template_name: 'worker', cursor_configs: configs, total_items: 1000)
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
# ```
|
|
37
|
+
#
|
|
38
|
+
# ## Combining Mixins
|
|
39
|
+
#
|
|
40
|
+
# Mixins can be combined when a handler needs multiple capabilities:
|
|
41
|
+
#
|
|
42
|
+
# ```ruby
|
|
43
|
+
# class ApiBatchHandler < TaskerCore::StepHandler::Base
|
|
44
|
+
# include TaskerCore::StepHandler::Mixins::API
|
|
45
|
+
# include TaskerCore::StepHandler::Mixins::Batchable
|
|
46
|
+
#
|
|
47
|
+
# def call(context)
|
|
48
|
+
# # Can use both API and Batchable helpers
|
|
49
|
+
# end
|
|
50
|
+
# end
|
|
51
|
+
# ```
|
|
52
|
+
|
|
53
|
+
require_relative 'mixins/api'
|
|
54
|
+
require_relative 'mixins/decision'
|
|
55
|
+
require_relative 'mixins/batchable'
|
|
56
|
+
|
|
57
|
+
module TaskerCore
|
|
58
|
+
module StepHandler
|
|
59
|
+
module Mixins
|
|
60
|
+
# Re-export for convenient access
|
|
61
|
+
# API = TaskerCore::StepHandler::Mixins::API
|
|
62
|
+
# Decision = TaskerCore::StepHandler::Mixins::Decision
|
|
63
|
+
# Batchable = TaskerCore::StepHandler::Mixins::Batchable
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
module Worker
|
|
5
|
+
# Subscribes to step execution events and routes to handlers
|
|
6
|
+
class StepExecutionSubscriber
|
|
7
|
+
attr_reader :logger, :handler_registry, :stats
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@logger = TaskerCore::Logger.instance
|
|
11
|
+
@handler_registry = TaskerCore::Registry::HandlerRegistry.instance
|
|
12
|
+
@stats = { processed: 0, succeeded: 0, failed: 0 }
|
|
13
|
+
@active = true
|
|
14
|
+
@subscribed = false
|
|
15
|
+
|
|
16
|
+
subscribe_to_events
|
|
17
|
+
logger.info 'Step execution subscriber initialized'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Check if subscriber is active
|
|
21
|
+
def active?
|
|
22
|
+
@active
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Stop the subscriber
|
|
26
|
+
def stop!
|
|
27
|
+
@active = false
|
|
28
|
+
logger.info 'Step execution subscriber stopped'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Called by dry-events when step execution is requested
|
|
32
|
+
def call(event)
|
|
33
|
+
event_data = event.payload
|
|
34
|
+
step_data = event_data[:task_sequence_step]
|
|
35
|
+
|
|
36
|
+
logger.info 'Processing step execution request'
|
|
37
|
+
logger.info("Event ID: #{event_data[:event_id]}")
|
|
38
|
+
logger.info("Step: #{step_data.workflow_step.name}")
|
|
39
|
+
logger.info("Handler: #{step_data.step_definition.handler.callable}")
|
|
40
|
+
|
|
41
|
+
@stats[:processed] += 1
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
# TAS-93: Resolve step handler from registry using full handler definition
|
|
45
|
+
# This enables method dispatch (handler_method) and resolver hints (resolver)
|
|
46
|
+
handler = @handler_registry.resolve_handler(step_data.step_definition.handler)
|
|
47
|
+
|
|
48
|
+
unless handler
|
|
49
|
+
raise Errors::ConfigurationError,
|
|
50
|
+
"No handler found for #{step_data.step_definition.handler.callable}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Execute handler with unified context (TAS-96: cross-language standard)
|
|
54
|
+
context = TaskerCore::Types::StepContext.new(step_data)
|
|
55
|
+
result = handler.call(context)
|
|
56
|
+
|
|
57
|
+
# Convert handler output to standardized result
|
|
58
|
+
standardized_result = TaskerCore::Types::StepHandlerCallResult.from_handler_output(result)
|
|
59
|
+
|
|
60
|
+
# TAS-125: Check for checkpoint yield - this is a special case where
|
|
61
|
+
# we persist progress and re-dispatch instead of completing the step
|
|
62
|
+
if standardized_result.checkpoint?
|
|
63
|
+
publish_checkpoint_yield(
|
|
64
|
+
event_data: event_data,
|
|
65
|
+
checkpoint_result: standardized_result
|
|
66
|
+
)
|
|
67
|
+
@stats[:succeeded] += 1
|
|
68
|
+
logger.info('ā
Checkpoint yield submitted - step will be re-dispatched')
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Handle handler-returned error results directly (preserves retryable field)
|
|
73
|
+
# TAS-125: Handlers like inject_failure return StepHandlerCallResult.error()
|
|
74
|
+
# with explicit retryable: false for permanent failures. We must preserve this.
|
|
75
|
+
unless standardized_result.success?
|
|
76
|
+
logger.error("š„ Handler returned failure: #{standardized_result.message}")
|
|
77
|
+
logger.error("š„ Error type: #{standardized_result.error_type}, retryable: #{standardized_result.retryable}")
|
|
78
|
+
|
|
79
|
+
publish_step_completion(
|
|
80
|
+
event_data: event_data,
|
|
81
|
+
success: false,
|
|
82
|
+
result: nil,
|
|
83
|
+
error_message: standardized_result.message,
|
|
84
|
+
metadata: {
|
|
85
|
+
execution_time_ms: standardized_result.metadata&.dig('duration_ms') || 0,
|
|
86
|
+
handler_version: nil,
|
|
87
|
+
retryable: standardized_result.retryable, # CRITICAL: Preserve handler's retryable setting
|
|
88
|
+
completed_at: Time.now.utc.iso8601,
|
|
89
|
+
worker_id: 'ruby_worker',
|
|
90
|
+
worker_hostname: nil,
|
|
91
|
+
started_at: Time.now.utc.iso8601,
|
|
92
|
+
custom: {
|
|
93
|
+
failed_at: Time.now.utc.iso8601,
|
|
94
|
+
failed_by: 'ruby_worker',
|
|
95
|
+
error_type: standardized_result.error_type,
|
|
96
|
+
error_code: standardized_result.error_code,
|
|
97
|
+
handler_class: step_data.step_definition.handler.callable,
|
|
98
|
+
retryable: standardized_result.retryable
|
|
99
|
+
}.merge(standardized_result.metadata || {})
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@stats[:failed] += 1
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Publish successful completion with properly structured metadata
|
|
108
|
+
# Match StepExecutionMetadata struct from Rust
|
|
109
|
+
publish_step_completion(
|
|
110
|
+
event_data: event_data,
|
|
111
|
+
success: true,
|
|
112
|
+
result: standardized_result.result,
|
|
113
|
+
metadata: {
|
|
114
|
+
# StepExecutionMetadata required fields
|
|
115
|
+
execution_time_ms: standardized_result.metadata&.dig('duration_ms') || 0,
|
|
116
|
+
handler_version: nil,
|
|
117
|
+
retryable: true, # Success is always retryable if it fails later
|
|
118
|
+
completed_at: Time.now.utc.iso8601,
|
|
119
|
+
worker_id: 'ruby_worker',
|
|
120
|
+
worker_hostname: nil,
|
|
121
|
+
started_at: Time.now.utc.iso8601,
|
|
122
|
+
# Additional metadata in custom field
|
|
123
|
+
custom: {
|
|
124
|
+
processed_at: Time.now.utc.iso8601,
|
|
125
|
+
processed_by: 'ruby_worker',
|
|
126
|
+
handler_class: step_data.step_definition.handler.callable
|
|
127
|
+
}.merge(standardized_result.metadata || {})
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@stats[:succeeded] += 1
|
|
132
|
+
logger.info('ā
Step execution completed successfully')
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
logger.error("š„ Step execution failed: #{e.message}")
|
|
135
|
+
logger.error("š„ #{e.backtrace.first(5).join("\nš„ ")}")
|
|
136
|
+
|
|
137
|
+
# Classify error retryability using systematic classifier
|
|
138
|
+
retryable = Errors::ErrorClassifier.retryable?(e)
|
|
139
|
+
|
|
140
|
+
# Publish failure completion with properly structured metadata
|
|
141
|
+
# Match StepExecutionMetadata struct from Rust - retryable field is critical
|
|
142
|
+
publish_step_completion(
|
|
143
|
+
event_data: event_data,
|
|
144
|
+
success: false,
|
|
145
|
+
result: nil,
|
|
146
|
+
error_message: e.message,
|
|
147
|
+
metadata: {
|
|
148
|
+
# StepExecutionMetadata required fields
|
|
149
|
+
execution_time_ms: 0,
|
|
150
|
+
handler_version: nil,
|
|
151
|
+
retryable: retryable, # CRITICAL: This is what Rust reads at metadata.retryable
|
|
152
|
+
completed_at: Time.now.utc.iso8601,
|
|
153
|
+
worker_id: 'ruby_worker',
|
|
154
|
+
worker_hostname: nil,
|
|
155
|
+
started_at: Time.now.utc.iso8601,
|
|
156
|
+
# Additional error context in custom field
|
|
157
|
+
custom: {
|
|
158
|
+
failed_at: Time.now.utc.iso8601,
|
|
159
|
+
failed_by: 'ruby_worker',
|
|
160
|
+
error_class: e.class.name,
|
|
161
|
+
handler_class: step_data.step_definition.handler.callable,
|
|
162
|
+
retryable: retryable # Also in custom for debugging/redundancy
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@stats[:failed] += 1
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
def subscribe_to_events
|
|
174
|
+
return if @subscribed # Guard against double subscription
|
|
175
|
+
|
|
176
|
+
TaskerCore::Worker::EventBridge.instance.subscribe_to_step_execution do |event|
|
|
177
|
+
call(event)
|
|
178
|
+
end
|
|
179
|
+
@subscribed = true
|
|
180
|
+
logger.info 'Subscribed to step execution events'
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def publish_step_completion(event_data:, success:, result: nil, error_message: nil, metadata: nil)
|
|
184
|
+
completion_payload = {
|
|
185
|
+
event_id: event_data[:event_id],
|
|
186
|
+
task_uuid: event_data[:task_uuid],
|
|
187
|
+
step_uuid: event_data[:step_uuid],
|
|
188
|
+
success: success,
|
|
189
|
+
result: result,
|
|
190
|
+
metadata: metadata,
|
|
191
|
+
error_message: error_message
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# TAS-65 Phase 1.5b: Propagate trace context back to Rust for distributed tracing
|
|
195
|
+
completion_payload[:trace_id] = event_data[:trace_id] if event_data[:trace_id]
|
|
196
|
+
completion_payload[:span_id] = event_data[:span_id] if event_data[:span_id]
|
|
197
|
+
|
|
198
|
+
TaskerCore::Worker::EventBridge.instance.publish_step_completion(completion_payload)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# TAS-125: Publish checkpoint yield to persist progress and trigger re-dispatch
|
|
202
|
+
def publish_checkpoint_yield(event_data:, checkpoint_result:)
|
|
203
|
+
checkpoint_data = checkpoint_result.to_checkpoint_data(
|
|
204
|
+
event_id: event_data[:event_id],
|
|
205
|
+
step_uuid: event_data[:step_uuid]
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
TaskerCore::Worker::EventBridge.instance.publish_step_checkpoint_yield(checkpoint_data)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|