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,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
require 'dry-types'
|
|
5
|
+
|
|
6
|
+
module TaskerCore
|
|
7
|
+
module Types
|
|
8
|
+
# Standardized result structure for step handler call methods
|
|
9
|
+
#
|
|
10
|
+
# This provides a consistent interface for all step handlers to return
|
|
11
|
+
# structured results with proper metadata for observability.
|
|
12
|
+
#
|
|
13
|
+
# Success example:
|
|
14
|
+
# StepHandlerCallResult.success(
|
|
15
|
+
# result: { validated_items: [...], total: 100.00 },
|
|
16
|
+
# metadata: {
|
|
17
|
+
# operation: "validate_order",
|
|
18
|
+
# item_count: 3,
|
|
19
|
+
# processing_time_ms: 125
|
|
20
|
+
# }
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# Error example (typically raised as exception):
|
|
24
|
+
# raise TaskerCore::Errors::PermanentError.new(
|
|
25
|
+
# "Invalid order total",
|
|
26
|
+
# error_code: "INVALID_TOTAL",
|
|
27
|
+
# context: { total: -50, min_allowed: 0 }
|
|
28
|
+
# )
|
|
29
|
+
module StepHandlerCallResult
|
|
30
|
+
class Success < Dry::Struct
|
|
31
|
+
# Always true for success results
|
|
32
|
+
attribute :success, Types::Bool.default(true)
|
|
33
|
+
|
|
34
|
+
# The actual result data from the step handler
|
|
35
|
+
# Can be any JSON-serializable value (Hash, Array, String, Number, etc.)
|
|
36
|
+
attribute :result, Types::Any
|
|
37
|
+
|
|
38
|
+
# Optional metadata for observability
|
|
39
|
+
attribute(:metadata, Types::Hash.default { {} })
|
|
40
|
+
|
|
41
|
+
# Predicate method for checking if result is successful
|
|
42
|
+
def success?
|
|
43
|
+
success
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# TAS-125: Not a checkpoint - this is a final success result
|
|
47
|
+
def checkpoint?
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Error < Dry::Struct
|
|
53
|
+
# Always false for error results
|
|
54
|
+
attribute :success, Types::Bool.default(false)
|
|
55
|
+
|
|
56
|
+
# Error type (matches our error class names)
|
|
57
|
+
attribute :error_type,
|
|
58
|
+
Types::String.enum('PermanentError', 'RetryableError', 'ValidationError', 'UnexpectedError',
|
|
59
|
+
'StepCompletionError')
|
|
60
|
+
|
|
61
|
+
# Human-readable error message
|
|
62
|
+
attribute :message, Types::String
|
|
63
|
+
|
|
64
|
+
# Optional error code for categorization
|
|
65
|
+
attribute :error_code, Types::String.optional
|
|
66
|
+
|
|
67
|
+
# Whether this error should trigger a retry
|
|
68
|
+
attribute :retryable, Types::Bool.default(false)
|
|
69
|
+
|
|
70
|
+
# Additional error context and metadata
|
|
71
|
+
attribute(:metadata, Types::Hash.default { {} })
|
|
72
|
+
|
|
73
|
+
# Predicate method for checking if result is successful
|
|
74
|
+
def success?
|
|
75
|
+
success
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# TAS-125: Not a checkpoint - this is a final error result
|
|
79
|
+
def checkpoint?
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# TAS-125: Checkpoint yield result for batch processing
|
|
85
|
+
#
|
|
86
|
+
# This result type signals that a batch processing handler wants to
|
|
87
|
+
# persist its progress (checkpoint) and be re-dispatched for continued
|
|
88
|
+
# processing. Unlike Success or Error, this is an intermediate state
|
|
89
|
+
# that doesn't complete the step.
|
|
90
|
+
#
|
|
91
|
+
# Example:
|
|
92
|
+
# StepHandlerCallResult.checkpoint_yield(
|
|
93
|
+
# cursor: 1000,
|
|
94
|
+
# items_processed: 1000,
|
|
95
|
+
# accumulated_results: { total_amount: 50000.00 }
|
|
96
|
+
# )
|
|
97
|
+
class CheckpointYield < Dry::Struct
|
|
98
|
+
# Position to resume from (Integer, String, or Hash)
|
|
99
|
+
# For offset-based pagination: Integer (row number)
|
|
100
|
+
# For cursor-based pagination: String or Hash (opaque cursor)
|
|
101
|
+
attribute :cursor, Types::Any
|
|
102
|
+
|
|
103
|
+
# Total number of items processed so far (cumulative across all yields)
|
|
104
|
+
attribute :items_processed, Types::Integer.constrained(gteq: 0)
|
|
105
|
+
|
|
106
|
+
# Optional partial aggregations to carry forward to next iteration
|
|
107
|
+
# Useful for running totals, counters, or intermediate calculations
|
|
108
|
+
attribute :accumulated_results, Types::Any.optional
|
|
109
|
+
|
|
110
|
+
# This is a checkpoint, not a final result
|
|
111
|
+
def checkpoint?
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Not successful yet - still in progress
|
|
116
|
+
def success?
|
|
117
|
+
false
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Convert to hash for FFI transport
|
|
121
|
+
def to_checkpoint_data(event_id:, step_uuid:)
|
|
122
|
+
{
|
|
123
|
+
event_id: event_id,
|
|
124
|
+
step_uuid: step_uuid,
|
|
125
|
+
cursor: cursor,
|
|
126
|
+
items_processed: items_processed,
|
|
127
|
+
accumulated_results: accumulated_results
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Factory methods for creating results
|
|
133
|
+
class << self
|
|
134
|
+
# Create a success result
|
|
135
|
+
#
|
|
136
|
+
# @param result [Object] The result data (must be JSON-serializable)
|
|
137
|
+
# @param metadata [Hash] Optional metadata for observability
|
|
138
|
+
# @return [Success] A success result instance
|
|
139
|
+
def success(result:, metadata: {})
|
|
140
|
+
Success.new(
|
|
141
|
+
success: true,
|
|
142
|
+
result: result,
|
|
143
|
+
metadata: metadata
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Create an error result
|
|
148
|
+
#
|
|
149
|
+
# @param error_type [String] Type of error (PermanentError, RetryableError, etc.)
|
|
150
|
+
# @param message [String] Human-readable error message
|
|
151
|
+
# @param error_code [String, nil] Optional error code
|
|
152
|
+
# @param retryable [Boolean] Whether to retry this error
|
|
153
|
+
# @param metadata [Hash] Additional error context
|
|
154
|
+
# @return [Error] An error result instance
|
|
155
|
+
def error(error_type:, message:, error_code: nil, retryable: false, metadata: {})
|
|
156
|
+
Error.new(
|
|
157
|
+
success: false,
|
|
158
|
+
error_type: error_type,
|
|
159
|
+
message: message,
|
|
160
|
+
error_code: error_code,
|
|
161
|
+
retryable: retryable,
|
|
162
|
+
metadata: metadata
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# TAS-125: Create a checkpoint yield result for batch processing
|
|
167
|
+
#
|
|
168
|
+
# Use this when your batch processing handler wants to persist progress
|
|
169
|
+
# and be re-dispatched for continued processing.
|
|
170
|
+
#
|
|
171
|
+
# @param cursor [Integer, String, Hash] Position to resume from
|
|
172
|
+
# @param items_processed [Integer] Total items processed so far (cumulative)
|
|
173
|
+
# @param accumulated_results [Hash, nil] Optional partial aggregations
|
|
174
|
+
# @return [CheckpointYield] A checkpoint yield result instance
|
|
175
|
+
#
|
|
176
|
+
# @example Yield checkpoint with offset cursor
|
|
177
|
+
# StepHandlerCallResult.checkpoint_yield(
|
|
178
|
+
# cursor: 1000,
|
|
179
|
+
# items_processed: 1000,
|
|
180
|
+
# accumulated_results: { total_amount: 50000.00, row_count: 1000 }
|
|
181
|
+
# )
|
|
182
|
+
#
|
|
183
|
+
# @example Yield checkpoint with opaque cursor
|
|
184
|
+
# StepHandlerCallResult.checkpoint_yield(
|
|
185
|
+
# cursor: { page_token: "eyJsYXN0X2lkIjo5OTl9" },
|
|
186
|
+
# items_processed: 500
|
|
187
|
+
# )
|
|
188
|
+
def checkpoint_yield(cursor:, items_processed:, accumulated_results: nil)
|
|
189
|
+
CheckpointYield.new(
|
|
190
|
+
cursor: cursor,
|
|
191
|
+
items_processed: items_processed,
|
|
192
|
+
accumulated_results: accumulated_results
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Convert arbitrary handler output to a StepHandlerCallResult
|
|
197
|
+
#
|
|
198
|
+
# @param output [Object] The output from a handler's call method
|
|
199
|
+
# @return [Success, Error, CheckpointYield] A properly structured result
|
|
200
|
+
def from_handler_output(output)
|
|
201
|
+
case output
|
|
202
|
+
when Success, Error, CheckpointYield
|
|
203
|
+
# Already a proper result
|
|
204
|
+
output
|
|
205
|
+
when Hash
|
|
206
|
+
# Check if it looks like a result structure
|
|
207
|
+
if output[:success] == true || output['success'] == true
|
|
208
|
+
# Looks like a success result
|
|
209
|
+
Success.new(
|
|
210
|
+
success: true,
|
|
211
|
+
result: output[:result] || output['result'] || output,
|
|
212
|
+
metadata: output[:metadata] || output['metadata'] || {}
|
|
213
|
+
)
|
|
214
|
+
elsif output[:success] == false || output['success'] == false
|
|
215
|
+
# Looks like an error result
|
|
216
|
+
Error.new(
|
|
217
|
+
success: false,
|
|
218
|
+
error_type: output[:error_type] || output['error_type'] || 'UnexpectedError',
|
|
219
|
+
message: output[:message] || output['message'] || 'Unknown error',
|
|
220
|
+
error_code: output[:error_code] || output['error_code'],
|
|
221
|
+
retryable: output[:retryable] || output['retryable'] || false,
|
|
222
|
+
metadata: output[:metadata] || output['metadata'] || {}
|
|
223
|
+
)
|
|
224
|
+
else
|
|
225
|
+
# Just a hash of results - wrap it as success
|
|
226
|
+
Success.new(
|
|
227
|
+
success: true,
|
|
228
|
+
result: output,
|
|
229
|
+
metadata: {
|
|
230
|
+
wrapped: true,
|
|
231
|
+
original_type: 'Hash'
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
else
|
|
236
|
+
# Any other type - wrap as success
|
|
237
|
+
Success.new(
|
|
238
|
+
success: true,
|
|
239
|
+
result: output,
|
|
240
|
+
metadata: {
|
|
241
|
+
wrapped: true,
|
|
242
|
+
original_type: output.class.name
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Create error result from an exception
|
|
249
|
+
#
|
|
250
|
+
# @param exception [Exception] The exception that was raised
|
|
251
|
+
# @return [Error] An error result instance
|
|
252
|
+
def from_exception(exception)
|
|
253
|
+
case exception
|
|
254
|
+
when TaskerCore::Errors::PermanentError
|
|
255
|
+
Error.new(
|
|
256
|
+
success: false,
|
|
257
|
+
error_type: 'PermanentError',
|
|
258
|
+
message: exception.message,
|
|
259
|
+
error_code: exception.error_code,
|
|
260
|
+
retryable: false,
|
|
261
|
+
metadata: {
|
|
262
|
+
context: exception.context || {}
|
|
263
|
+
}.compact
|
|
264
|
+
)
|
|
265
|
+
when TaskerCore::Errors::RetryableError
|
|
266
|
+
Error.new(
|
|
267
|
+
success: false,
|
|
268
|
+
error_type: 'RetryableError',
|
|
269
|
+
message: exception.message,
|
|
270
|
+
error_code: nil, # RetryableError doesn't have error_code
|
|
271
|
+
retryable: true,
|
|
272
|
+
metadata: {
|
|
273
|
+
context: exception.context || {},
|
|
274
|
+
retry_after: exception.retry_after
|
|
275
|
+
}.compact
|
|
276
|
+
)
|
|
277
|
+
when TaskerCore::Errors::ValidationError
|
|
278
|
+
Error.new(
|
|
279
|
+
success: false,
|
|
280
|
+
error_type: 'ValidationError',
|
|
281
|
+
message: exception.message,
|
|
282
|
+
error_code: exception.error_code,
|
|
283
|
+
retryable: false,
|
|
284
|
+
metadata: {
|
|
285
|
+
context: exception.context || {},
|
|
286
|
+
field: exception.field
|
|
287
|
+
}.compact
|
|
288
|
+
)
|
|
289
|
+
else
|
|
290
|
+
# Generic exception
|
|
291
|
+
Error.new(
|
|
292
|
+
success: false,
|
|
293
|
+
error_type: 'UnexpectedError',
|
|
294
|
+
message: exception.message,
|
|
295
|
+
error_code: nil,
|
|
296
|
+
retryable: true,
|
|
297
|
+
metadata: {
|
|
298
|
+
exception_class: exception.class.name,
|
|
299
|
+
stack_trace: exception.backtrace&.first(10)&.join("\n")
|
|
300
|
+
}.compact
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
require 'dry-types'
|
|
5
|
+
|
|
6
|
+
module TaskerCore
|
|
7
|
+
module Types
|
|
8
|
+
# Define custom types for the system
|
|
9
|
+
include Dry.Types()
|
|
10
|
+
|
|
11
|
+
# Custom types for TaskerCore - Essential validation types
|
|
12
|
+
Namespace = Types::String.constrained(filled: true)
|
|
13
|
+
StepName = Types::String.constrained(filled: true)
|
|
14
|
+
TaskName = Types::String # Allow empty strings for task names
|
|
15
|
+
TaskVersion = Types::String.constrained(filled: true)
|
|
16
|
+
Priority = Types::Integer.constrained(gteq: 1, lteq: 10)
|
|
17
|
+
RetryCount = Types::Integer.constrained(gteq: 0)
|
|
18
|
+
TimeoutMs = Types::Integer.constrained(gt: 0)
|
|
19
|
+
|
|
20
|
+
# Lightweight struct for execution context (replaces OpenStruct)
|
|
21
|
+
ExecutionContext = Struct.new(:step, :task, keyword_init: true)
|
|
22
|
+
|
|
23
|
+
# Lightweight struct for queue message envelope (replaces OpenStruct)
|
|
24
|
+
QueueMessageEnvelope = Struct.new(:msg_id, :read_ct, :enqueued_at, :vt, :message, keyword_init: true)
|
|
25
|
+
|
|
26
|
+
# Minimal StepMessage stub for registry compatibility only
|
|
27
|
+
# This is a lightweight replacement for the complex 324-line StepMessage class
|
|
28
|
+
# Used only for temporary registry lookup objects in queue_worker.rb
|
|
29
|
+
class StepMessage
|
|
30
|
+
attr_reader :step_name, :namespace, :task_name, :task_version, :step_id, :task_uuid, :step_uuid
|
|
31
|
+
|
|
32
|
+
def initialize(attrs = {})
|
|
33
|
+
@step_name = attrs[:step_name]
|
|
34
|
+
@namespace = attrs[:namespace]
|
|
35
|
+
@task_name = attrs[:task_name]
|
|
36
|
+
@task_version = attrs[:task_version]
|
|
37
|
+
@step_id = attrs[:step_id]
|
|
38
|
+
@task_uuid = attrs[:task_uuid]
|
|
39
|
+
@step_uuid = attrs[:step_uuid]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Minimal interface for registry compatibility
|
|
43
|
+
def execution_context
|
|
44
|
+
ExecutionContext.new(
|
|
45
|
+
step: {
|
|
46
|
+
step_name: @step_name,
|
|
47
|
+
workflow_step_id: @step_id,
|
|
48
|
+
step_uuid: @step_uuid
|
|
49
|
+
},
|
|
50
|
+
task: {
|
|
51
|
+
task_uuid: @task_uuid,
|
|
52
|
+
namespace: @namespace,
|
|
53
|
+
task_name: @task_name,
|
|
54
|
+
task_version: @task_version
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Simple queue message data for new UUID-based architecture
|
|
61
|
+
# This is the core message structure for the simplified messaging system
|
|
62
|
+
# Used extensively in pgmq_client.rb and queue_worker.rb
|
|
63
|
+
class SimpleQueueMessageData < Dry::Struct
|
|
64
|
+
transform_keys(&:to_sym)
|
|
65
|
+
|
|
66
|
+
# pgmq envelope data
|
|
67
|
+
attribute :msg_id, Types::Integer
|
|
68
|
+
attribute :read_ct, Types::Integer
|
|
69
|
+
# Accept either String or Time for timestamps (PostgreSQL may return either)
|
|
70
|
+
attribute :enqueued_at, Types::String | Types::Instance(Time)
|
|
71
|
+
attribute :vt, Types::String | Types::Instance(Time)
|
|
72
|
+
|
|
73
|
+
# Simple message content (UUID-based)
|
|
74
|
+
attribute :message, Types::Hash.schema(
|
|
75
|
+
task_uuid: Types::String,
|
|
76
|
+
step_uuid: Types::String,
|
|
77
|
+
ready_dependency_step_uuids: Types::Array.of(Types::String)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Convenience accessor for the simple message
|
|
81
|
+
def step_message
|
|
82
|
+
@step_message ||= SimpleStepMessage.new(message)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def queue_message
|
|
86
|
+
@queue_message ||= QueueMessageEnvelope.new(
|
|
87
|
+
msg_id: msg_id,
|
|
88
|
+
read_ct: read_ct,
|
|
89
|
+
enqueued_at: enqueued_at,
|
|
90
|
+
vt: vt,
|
|
91
|
+
message: message
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Delegate UUID accessors to the message hash for convenience
|
|
96
|
+
def task_uuid
|
|
97
|
+
message[:task_uuid]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def step_uuid
|
|
101
|
+
message[:step_uuid]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def ready_dependency_step_uuids
|
|
105
|
+
message[:ready_dependency_step_uuids]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# SimpleStepMessage is defined in simple_message.rb with full UUID validation
|
|
110
|
+
# This avoids duplicate class definitions
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
require 'dry-types'
|
|
5
|
+
require 'socket'
|
|
6
|
+
|
|
7
|
+
module TaskerCore
|
|
8
|
+
module Types
|
|
9
|
+
# Step-related type definitions for workflow execution
|
|
10
|
+
module StepTypes
|
|
11
|
+
module Types
|
|
12
|
+
include Dry.Types()
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Step execution status enum
|
|
16
|
+
class StepExecutionStatus < Dry::Struct
|
|
17
|
+
attribute :status, Types::String.enum('success', 'failed', 'in_progress', 'cancelled', 'timeout')
|
|
18
|
+
|
|
19
|
+
def success?
|
|
20
|
+
status == 'success'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def completed?
|
|
24
|
+
status == 'success'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def failed?
|
|
28
|
+
status == 'failed'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def in_progress?
|
|
32
|
+
status == 'in_progress'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Step execution error details
|
|
37
|
+
class StepExecutionError < Dry::Struct
|
|
38
|
+
attribute :error_type,
|
|
39
|
+
Types::String.enum(
|
|
40
|
+
'HandlerNotFound',
|
|
41
|
+
'HandlerException',
|
|
42
|
+
'ProcessingError',
|
|
43
|
+
'MaxRetriesExceeded',
|
|
44
|
+
'RecordNotFound',
|
|
45
|
+
'UnexpectedError',
|
|
46
|
+
'PermanentError',
|
|
47
|
+
'RetryableError'
|
|
48
|
+
)
|
|
49
|
+
attribute :message, Types::String
|
|
50
|
+
attribute :retryable, Types::Bool.default(true)
|
|
51
|
+
attribute? :error_code, Types::String.optional
|
|
52
|
+
attribute? :stack_trace, Types::String.optional
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
{
|
|
56
|
+
message: message,
|
|
57
|
+
error_type: error_type,
|
|
58
|
+
status_code: nil,
|
|
59
|
+
context: {
|
|
60
|
+
error_code: error_code,
|
|
61
|
+
stack_trace: stack_trace
|
|
62
|
+
}.compact,
|
|
63
|
+
retryable: retryable
|
|
64
|
+
}.compact
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Step execution result from pgmq worker processing
|
|
69
|
+
class StepResult < Dry::Struct
|
|
70
|
+
attribute :task_uuid, Types::String
|
|
71
|
+
attribute :step_uuid, Types::String
|
|
72
|
+
attribute :status, StepExecutionStatus
|
|
73
|
+
attribute :execution_time_ms, Types::Integer
|
|
74
|
+
attribute :completed_at, Types::Constructor(Time) { |value| value.is_a?(Time) ? value : Time.parse(value.to_s) }
|
|
75
|
+
attribute? :result_data, Types::Any.optional
|
|
76
|
+
attribute? :error, StepExecutionError.optional
|
|
77
|
+
attribute? :orchestration_metadata, Types::Hash.optional
|
|
78
|
+
|
|
79
|
+
# Factory methods for creating results
|
|
80
|
+
def self.success(step_uuid:, task_uuid:, result_data: nil, execution_time_ms: 0)
|
|
81
|
+
new(
|
|
82
|
+
step_uuid: step_uuid,
|
|
83
|
+
task_uuid: task_uuid,
|
|
84
|
+
status: StepExecutionStatus.new(status: 'success'),
|
|
85
|
+
execution_time_ms: execution_time_ms,
|
|
86
|
+
completed_at: Time.now,
|
|
87
|
+
result_data: result_data
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.in_progress(step_uuid:, task_uuid:, result_data: nil, execution_time_ms: 0)
|
|
92
|
+
new(
|
|
93
|
+
step_uuid: step_uuid,
|
|
94
|
+
task_uuid: task_uuid,
|
|
95
|
+
status: StepExecutionStatus.new(status: 'in_progress'),
|
|
96
|
+
execution_time_ms: execution_time_ms,
|
|
97
|
+
completed_at: Time.now,
|
|
98
|
+
result_data: result_data
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.failure(step_uuid:, task_uuid:, error:, execution_time_ms: 0)
|
|
103
|
+
new(
|
|
104
|
+
step_uuid: step_uuid,
|
|
105
|
+
task_uuid: task_uuid,
|
|
106
|
+
status: StepExecutionStatus.new(status: 'failed'),
|
|
107
|
+
execution_time_ms: execution_time_ms,
|
|
108
|
+
completed_at: Time.now,
|
|
109
|
+
error: error
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Convenience methods
|
|
114
|
+
def success?
|
|
115
|
+
status.success?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def failed?
|
|
119
|
+
status.failed?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Convert to hash for message serialization matching Rust StepResultMessage structure
|
|
123
|
+
def to_h
|
|
124
|
+
{
|
|
125
|
+
step_uuid: step_uuid,
|
|
126
|
+
task_uuid: task_uuid,
|
|
127
|
+
status: map_status_to_rust_enum(status.status),
|
|
128
|
+
results: result_data,
|
|
129
|
+
error: error&.to_h,
|
|
130
|
+
execution_time_ms: execution_time_ms,
|
|
131
|
+
orchestration_metadata: orchestration_metadata,
|
|
132
|
+
metadata: {
|
|
133
|
+
worker_id: "ruby_worker_#{Process.pid}",
|
|
134
|
+
worker_hostname: Socket.gethostname,
|
|
135
|
+
started_at: (completed_at - (execution_time_ms / 1000.0)).utc.iso8601,
|
|
136
|
+
completed_at: completed_at.utc.iso8601,
|
|
137
|
+
custom: {}
|
|
138
|
+
}
|
|
139
|
+
}.compact
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
# Map Ruby status strings to Rust enum variants
|
|
145
|
+
def map_status_to_rust_enum(status_string)
|
|
146
|
+
case status_string
|
|
147
|
+
when 'success'
|
|
148
|
+
'Success'
|
|
149
|
+
when 'failed'
|
|
150
|
+
'Failed'
|
|
151
|
+
when 'cancelled'
|
|
152
|
+
'Cancelled'
|
|
153
|
+
when 'timeout'
|
|
154
|
+
'Timeout'
|
|
155
|
+
when 'in_progress'
|
|
156
|
+
'InProgress'
|
|
157
|
+
else # rubocop:disable Lint/DuplicateBranch
|
|
158
|
+
'Failed' # fallback
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Step completion struct for workflow step results
|
|
164
|
+
class StepCompletion < Dry::Struct
|
|
165
|
+
attribute :step_name, Types::Coercible::String
|
|
166
|
+
attribute :status, Types::Coercible::String.enum('complete', 'failed', 'pending')
|
|
167
|
+
attribute :results, Types::Hash.default({}.freeze)
|
|
168
|
+
attribute :duration_ms, Types::Integer.optional
|
|
169
|
+
attribute :completed_at, Types::Constructor(Time).optional
|
|
170
|
+
attribute :error_message, Types::Coercible::String.optional
|
|
171
|
+
|
|
172
|
+
# Validation for step completion data
|
|
173
|
+
def valid?
|
|
174
|
+
!step_name.empty? &&
|
|
175
|
+
%w[complete failed pending].include?(status) &&
|
|
176
|
+
results.is_a?(Hash)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Check if step completed successfully
|
|
180
|
+
def completed?
|
|
181
|
+
status == 'complete'
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Check if step failed
|
|
185
|
+
def failed?
|
|
186
|
+
status == 'failed'
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Check if step is still pending
|
|
190
|
+
def pending?
|
|
191
|
+
status == 'pending'
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Get execution duration in seconds
|
|
195
|
+
def duration_seconds
|
|
196
|
+
return nil unless duration_ms
|
|
197
|
+
|
|
198
|
+
duration_ms / 1000.0
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def to_s
|
|
202
|
+
"#<StepCompletion #{step_name} status=#{status} duration=#{duration_seconds}s>"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|