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,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
require 'dry-types'
|
|
5
|
+
|
|
6
|
+
module TaskerCore
|
|
7
|
+
module Types
|
|
8
|
+
# BatchProcessingOutcome - TAS-59 Batch Processing Implementation
|
|
9
|
+
#
|
|
10
|
+
# Represents the outcome of a batch processing handler execution.
|
|
11
|
+
# Batch processing handlers determine whether to create worker batches
|
|
12
|
+
# based on business logic and data volume.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage Patterns
|
|
15
|
+
#
|
|
16
|
+
# ### No Batches (No worker batches needed)
|
|
17
|
+
# ```ruby
|
|
18
|
+
# BatchProcessingOutcome.no_batches
|
|
19
|
+
# ```
|
|
20
|
+
#
|
|
21
|
+
# ### Create Worker Batches
|
|
22
|
+
# ```ruby
|
|
23
|
+
# # Create batches for processing large datasets
|
|
24
|
+
# cursor_configs = [
|
|
25
|
+
# { 'batch_id' => '001', 'start_cursor' => 1, 'end_cursor' => 200 },
|
|
26
|
+
# { 'batch_id' => '002', 'start_cursor' => 201, 'end_cursor' => 400 },
|
|
27
|
+
# { 'batch_id' => '003', 'start_cursor' => 401, 'end_cursor' => 600 }
|
|
28
|
+
# ]
|
|
29
|
+
#
|
|
30
|
+
# BatchProcessingOutcome.create_batches(
|
|
31
|
+
# worker_template_name: 'process_batch_worker',
|
|
32
|
+
# worker_count: 3,
|
|
33
|
+
# cursor_configs: cursor_configs,
|
|
34
|
+
# total_items: 250
|
|
35
|
+
# )
|
|
36
|
+
# ```
|
|
37
|
+
#
|
|
38
|
+
# ## Integration with Step Handlers
|
|
39
|
+
#
|
|
40
|
+
# Batch processing handlers should return this outcome in their result:
|
|
41
|
+
#
|
|
42
|
+
# ```ruby
|
|
43
|
+
# def call(context)
|
|
44
|
+
# # Business logic to determine if batching is needed
|
|
45
|
+
# total_records = context.get_task_field('total_records')
|
|
46
|
+
#
|
|
47
|
+
# outcome = if total_records > 1000
|
|
48
|
+
# # Create batches for parallel processing
|
|
49
|
+
# cursor_configs = calculate_cursor_configs(total_records)
|
|
50
|
+
# BatchProcessingOutcome.create_batches(
|
|
51
|
+
# worker_template_name: 'batch_processor',
|
|
52
|
+
# worker_count: cursor_configs.size,
|
|
53
|
+
# cursor_configs: cursor_configs,
|
|
54
|
+
# total_items: total_records
|
|
55
|
+
# )
|
|
56
|
+
# else
|
|
57
|
+
# # Process inline, no batches needed
|
|
58
|
+
# BatchProcessingOutcome.no_batches
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# StepHandlerCallResult.success(
|
|
62
|
+
# result: {
|
|
63
|
+
# batch_processing_outcome: outcome.to_h
|
|
64
|
+
# },
|
|
65
|
+
# metadata: {
|
|
66
|
+
# operation: 'batch_decision',
|
|
67
|
+
# batches_created: outcome.requires_batch_creation? ? cursor_configs.size : 0
|
|
68
|
+
# }
|
|
69
|
+
# )
|
|
70
|
+
# end
|
|
71
|
+
# ```
|
|
72
|
+
module BatchProcessingOutcome
|
|
73
|
+
module Types
|
|
74
|
+
include Dry.Types()
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# NoBatches outcome - no worker batches needed
|
|
78
|
+
class NoBatches < Dry::Struct
|
|
79
|
+
# Type discriminator (matches Rust serde tag field)
|
|
80
|
+
attribute :type, Types::String.default('no_batches')
|
|
81
|
+
|
|
82
|
+
# Convert to hash for serialization to Rust
|
|
83
|
+
# Note: Rust expects lowercase snake_case due to #[serde(rename_all = "snake_case")]
|
|
84
|
+
def to_h
|
|
85
|
+
{ 'type' => 'no_batches' }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if this outcome requires batch creation
|
|
89
|
+
def requires_batch_creation?
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# CreateBatches outcome - create worker batches for parallel processing
|
|
95
|
+
class CreateBatches < Dry::Struct
|
|
96
|
+
# Type discriminator (matches Rust serde tag field)
|
|
97
|
+
attribute :type, Types::String.default('create_batches')
|
|
98
|
+
|
|
99
|
+
# Name of the worker template to use for batch processing
|
|
100
|
+
attribute :worker_template_name, Types::Strict::String
|
|
101
|
+
|
|
102
|
+
# Number of worker batches to create
|
|
103
|
+
attribute :worker_count, Types::Strict::Integer.constrained(gteq: 0)
|
|
104
|
+
|
|
105
|
+
# Array of cursor configurations for batch processing
|
|
106
|
+
# Each config is a flexible hash (e.g., batch_id, start_cursor, end_cursor, batch_size)
|
|
107
|
+
attribute :cursor_configs, Types::Array.of(Types::Hash).constrained(min_size: 1)
|
|
108
|
+
|
|
109
|
+
# Total number of items to be processed across all batches
|
|
110
|
+
attribute :total_items, Types::Strict::Integer.constrained(gteq: 0)
|
|
111
|
+
|
|
112
|
+
# Convert to hash for serialization to Rust
|
|
113
|
+
# Note: Rust expects lowercase snake_case due to #[serde(rename_all = "snake_case")]
|
|
114
|
+
def to_h
|
|
115
|
+
{
|
|
116
|
+
'type' => 'create_batches',
|
|
117
|
+
'worker_template_name' => worker_template_name,
|
|
118
|
+
'worker_count' => worker_count,
|
|
119
|
+
'cursor_configs' => cursor_configs,
|
|
120
|
+
'total_items' => total_items
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if this outcome requires batch creation
|
|
125
|
+
def requires_batch_creation?
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Factory methods for creating outcomes
|
|
131
|
+
class << self
|
|
132
|
+
# Create a NoBatches outcome
|
|
133
|
+
#
|
|
134
|
+
# Use when the batch processing handler determines that no worker
|
|
135
|
+
# batches are needed (e.g., small dataset, inline processing).
|
|
136
|
+
#
|
|
137
|
+
# @return [NoBatches] A no-batches outcome instance
|
|
138
|
+
#
|
|
139
|
+
# @example
|
|
140
|
+
# BatchProcessingOutcome.no_batches
|
|
141
|
+
def no_batches
|
|
142
|
+
NoBatches.new
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Create a CreateBatches outcome with specified batch configuration
|
|
146
|
+
#
|
|
147
|
+
# Use when the batch processing handler determines that worker batches
|
|
148
|
+
# should be created for parallel processing.
|
|
149
|
+
#
|
|
150
|
+
# @param worker_template_name [String] Name of worker template to use
|
|
151
|
+
# @param worker_count [Integer] Number of worker batches to create (must be >= 0)
|
|
152
|
+
# @param cursor_configs [Array<Hash>] Array of cursor configurations
|
|
153
|
+
# @param total_items [Integer] Total number of items to process (must be >= 0)
|
|
154
|
+
# @return [CreateBatches] A create-batches outcome instance
|
|
155
|
+
# @raise [ArgumentError] if required parameters are invalid
|
|
156
|
+
#
|
|
157
|
+
# @example Create batches for CSV row processing
|
|
158
|
+
# cursor_configs = [
|
|
159
|
+
# { 'batch_id' => '001', 'start_cursor' => 1, 'end_cursor' => 200, 'batch_size' => 200 },
|
|
160
|
+
# { 'batch_id' => '002', 'start_cursor' => 201, 'end_cursor' => 400, 'batch_size' => 200 }
|
|
161
|
+
# ]
|
|
162
|
+
# BatchProcessingOutcome.create_batches(
|
|
163
|
+
# worker_template_name: 'process_csv_batch',
|
|
164
|
+
# worker_count: 2,
|
|
165
|
+
# cursor_configs: cursor_configs,
|
|
166
|
+
# total_items: 400
|
|
167
|
+
# )
|
|
168
|
+
def create_batches(worker_template_name:, worker_count:, cursor_configs:, total_items:)
|
|
169
|
+
if worker_template_name.nil? || worker_template_name.empty?
|
|
170
|
+
raise ArgumentError,
|
|
171
|
+
'worker_template_name cannot be empty'
|
|
172
|
+
end
|
|
173
|
+
raise ArgumentError, 'cursor_configs cannot be empty' if cursor_configs.nil? || cursor_configs.empty?
|
|
174
|
+
|
|
175
|
+
# Validate cursor_configs count matches worker_count
|
|
176
|
+
if cursor_configs.size != worker_count
|
|
177
|
+
raise ArgumentError,
|
|
178
|
+
"cursor_configs length (#{cursor_configs.size}) must equal worker_count (#{worker_count})"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Validate batch_size consistency for numeric cursors
|
|
182
|
+
# Only validate when both cursors are integers (flexible cursors like UUIDs/timestamps don't have batch_size)
|
|
183
|
+
cursor_configs.each_with_index do |config, index|
|
|
184
|
+
next unless config.key?('batch_size') || config.key?(:batch_size)
|
|
185
|
+
|
|
186
|
+
# Extract values supporting both string and symbol keys
|
|
187
|
+
start_cursor = config['start_cursor'] || config[:start_cursor]
|
|
188
|
+
end_cursor = config['end_cursor'] || config[:end_cursor]
|
|
189
|
+
batch_size = config['batch_size'] || config[:batch_size]
|
|
190
|
+
|
|
191
|
+
# Only validate for numeric cursors
|
|
192
|
+
next unless start_cursor.is_a?(Integer) && end_cursor.is_a?(Integer)
|
|
193
|
+
|
|
194
|
+
expected_batch_size = end_cursor - start_cursor
|
|
195
|
+
|
|
196
|
+
next unless batch_size != expected_batch_size
|
|
197
|
+
|
|
198
|
+
raise ArgumentError,
|
|
199
|
+
"cursor_configs[#{index}] batch_size (#{batch_size}) must equal " \
|
|
200
|
+
"end_cursor - start_cursor (#{end_cursor} - #{start_cursor} = #{expected_batch_size})"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
CreateBatches.new(
|
|
204
|
+
worker_template_name: worker_template_name,
|
|
205
|
+
worker_count: worker_count,
|
|
206
|
+
cursor_configs: cursor_configs,
|
|
207
|
+
total_items: total_items
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Parse a BatchProcessingOutcome from a hash
|
|
212
|
+
#
|
|
213
|
+
# Used to deserialize outcomes from step handler results.
|
|
214
|
+
# Supports both symbol and string keys for maximum flexibility.
|
|
215
|
+
#
|
|
216
|
+
# @param hash [Hash] The hash representation of an outcome
|
|
217
|
+
# @return [NoBatches, CreateBatches] The parsed outcome
|
|
218
|
+
# @raise [ArgumentError] If hash is invalid or missing required fields
|
|
219
|
+
#
|
|
220
|
+
# @example Parse no-batches outcome
|
|
221
|
+
# outcome = BatchProcessingOutcome.from_hash({
|
|
222
|
+
# type: 'no_batches'
|
|
223
|
+
# })
|
|
224
|
+
#
|
|
225
|
+
# @example Parse create-batches outcome
|
|
226
|
+
# outcome = BatchProcessingOutcome.from_hash({
|
|
227
|
+
# 'type' => 'create_batches',
|
|
228
|
+
# 'worker_template_name' => 'batch_worker',
|
|
229
|
+
# 'worker_count' => 3,
|
|
230
|
+
# 'cursor_configs' => [
|
|
231
|
+
# { 'cursor_key' => 'offset', 'cursor_value' => 0 }
|
|
232
|
+
# ],
|
|
233
|
+
# 'total_items' => 300
|
|
234
|
+
# })
|
|
235
|
+
def from_hash(hash)
|
|
236
|
+
raise ArgumentError, 'Expected Hash, got nil' if hash.nil?
|
|
237
|
+
raise ArgumentError, "Expected Hash, got #{hash.class}" unless hash.is_a?(Hash)
|
|
238
|
+
|
|
239
|
+
# Support both symbol and string keys
|
|
240
|
+
# Note: Rust sends lowercase snake_case due to #[serde(rename_all = "snake_case")]
|
|
241
|
+
hash = deep_symbolize_keys(hash)
|
|
242
|
+
outcome_type = hash[:type]
|
|
243
|
+
|
|
244
|
+
case outcome_type
|
|
245
|
+
when 'no_batches'
|
|
246
|
+
NoBatches.new
|
|
247
|
+
when 'create_batches'
|
|
248
|
+
worker_template_name = hash[:worker_template_name]
|
|
249
|
+
worker_count = hash[:worker_count]
|
|
250
|
+
cursor_configs = hash[:cursor_configs]
|
|
251
|
+
total_items = hash[:total_items]
|
|
252
|
+
|
|
253
|
+
if worker_template_name.nil? || worker_template_name.empty?
|
|
254
|
+
raise ArgumentError, 'worker_template_name is required for create_batches outcome'
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if worker_count.nil? || worker_count.negative?
|
|
258
|
+
raise ArgumentError,
|
|
259
|
+
'worker_count is required and must be >= 0'
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
unless cursor_configs.is_a?(Array) && !cursor_configs.empty?
|
|
263
|
+
raise ArgumentError, 'cursor_configs must be a non-empty array'
|
|
264
|
+
end
|
|
265
|
+
raise ArgumentError, 'total_items is required and must be >= 0' if total_items.nil? || total_items.negative?
|
|
266
|
+
|
|
267
|
+
CreateBatches.new(
|
|
268
|
+
worker_template_name: worker_template_name,
|
|
269
|
+
worker_count: worker_count,
|
|
270
|
+
cursor_configs: cursor_configs,
|
|
271
|
+
total_items: total_items
|
|
272
|
+
)
|
|
273
|
+
else
|
|
274
|
+
raise ArgumentError, "Unknown outcome type: #{outcome_type.inspect}"
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
|
|
280
|
+
# Deep symbolize hash keys for consistent access
|
|
281
|
+
# Handles nested hashes and arrays of hashes
|
|
282
|
+
def deep_symbolize_keys(hash)
|
|
283
|
+
return hash unless hash.is_a?(Hash)
|
|
284
|
+
|
|
285
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
286
|
+
new_key = key.to_sym
|
|
287
|
+
new_value = case value
|
|
288
|
+
when Hash
|
|
289
|
+
deep_symbolize_keys(value)
|
|
290
|
+
when Array
|
|
291
|
+
value.map { |v| v.is_a?(Hash) ? deep_symbolize_keys(v) : v }
|
|
292
|
+
else
|
|
293
|
+
value
|
|
294
|
+
end
|
|
295
|
+
result[new_key] = new_value
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
require 'dry-types'
|
|
5
|
+
|
|
6
|
+
module TaskerCore
|
|
7
|
+
module Types
|
|
8
|
+
# TAS-231: Client API response types for orchestration client FFI.
|
|
9
|
+
#
|
|
10
|
+
# These types match the JSON responses from the orchestration API,
|
|
11
|
+
# as returned by the client FFI functions (client_create_task, etc.).
|
|
12
|
+
module ClientTypes
|
|
13
|
+
module Types
|
|
14
|
+
include Dry.Types()
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Step readiness status within a task response
|
|
18
|
+
class StepReadiness < Dry::Struct
|
|
19
|
+
attribute :workflow_step_uuid, Types::String
|
|
20
|
+
attribute :task_uuid, Types::String
|
|
21
|
+
attribute :named_step_uuid, Types::String
|
|
22
|
+
attribute :name, Types::String
|
|
23
|
+
attribute :current_state, Types::String
|
|
24
|
+
attribute :dependencies_satisfied, Types::Bool
|
|
25
|
+
attribute :retry_eligible, Types::Bool
|
|
26
|
+
attribute :ready_for_execution, Types::Bool
|
|
27
|
+
attribute? :last_failure_at, Types::String.optional
|
|
28
|
+
attribute? :next_retry_at, Types::String.optional
|
|
29
|
+
attribute :total_parents, Types::Integer
|
|
30
|
+
attribute :completed_parents, Types::Integer
|
|
31
|
+
attribute :attempts, Types::Integer
|
|
32
|
+
attribute :max_attempts, Types::Integer
|
|
33
|
+
attribute? :backoff_request_seconds, Types::Integer.optional
|
|
34
|
+
attribute? :last_attempted_at, Types::String.optional
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Task response from the orchestration API.
|
|
38
|
+
# Returned by client_create_task and client_get_task.
|
|
39
|
+
class TaskResponse < Dry::Struct
|
|
40
|
+
attribute :task_uuid, Types::String
|
|
41
|
+
attribute :name, Types::String
|
|
42
|
+
attribute :namespace, Types::String
|
|
43
|
+
attribute :version, Types::String
|
|
44
|
+
attribute :status, Types::String
|
|
45
|
+
attribute :created_at, Types::String
|
|
46
|
+
attribute :updated_at, Types::String
|
|
47
|
+
attribute? :completed_at, Types::String.optional
|
|
48
|
+
attribute :context, Types::Hash
|
|
49
|
+
attribute :initiator, Types::String
|
|
50
|
+
attribute :source_system, Types::String
|
|
51
|
+
attribute :reason, Types::String
|
|
52
|
+
attribute? :priority, Types::Integer.optional
|
|
53
|
+
attribute? :tags, Types::Array.of(Types::String).optional
|
|
54
|
+
attribute :correlation_id, Types::String
|
|
55
|
+
attribute? :parent_correlation_id, Types::String.optional
|
|
56
|
+
|
|
57
|
+
# Execution context
|
|
58
|
+
attribute :total_steps, Types::Integer
|
|
59
|
+
attribute :pending_steps, Types::Integer
|
|
60
|
+
attribute :in_progress_steps, Types::Integer
|
|
61
|
+
attribute :completed_steps, Types::Integer
|
|
62
|
+
attribute :failed_steps, Types::Integer
|
|
63
|
+
attribute :ready_steps, Types::Integer
|
|
64
|
+
attribute :execution_status, Types::String
|
|
65
|
+
attribute :recommended_action, Types::String
|
|
66
|
+
attribute :completion_percentage, Types::Float
|
|
67
|
+
attribute :health_status, Types::String
|
|
68
|
+
|
|
69
|
+
# Step readiness
|
|
70
|
+
attribute :steps, Types::Array.default([].freeze)
|
|
71
|
+
|
|
72
|
+
def to_s
|
|
73
|
+
"#<ClientTaskResponse #{namespace}/#{name}:#{version} status=#{status}>"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Pagination information in list responses
|
|
78
|
+
class PaginationInfo < Dry::Struct
|
|
79
|
+
attribute :page, Types::Integer
|
|
80
|
+
attribute :per_page, Types::Integer
|
|
81
|
+
attribute :total_count, Types::Integer
|
|
82
|
+
attribute :total_pages, Types::Integer
|
|
83
|
+
attribute :has_next, Types::Bool
|
|
84
|
+
attribute :has_previous, Types::Bool
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Task list response with pagination.
|
|
88
|
+
# Returned by client_list_tasks.
|
|
89
|
+
class TaskListResponse < Dry::Struct
|
|
90
|
+
attribute :tasks, Types::Array.default([].freeze)
|
|
91
|
+
attribute :pagination, Types::Hash
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Step response from the orchestration API.
|
|
95
|
+
# Returned by client_get_step.
|
|
96
|
+
class StepResponse < Dry::Struct
|
|
97
|
+
attribute :step_uuid, Types::String
|
|
98
|
+
attribute :task_uuid, Types::String
|
|
99
|
+
attribute :name, Types::String
|
|
100
|
+
attribute :created_at, Types::String
|
|
101
|
+
attribute :updated_at, Types::String
|
|
102
|
+
attribute? :completed_at, Types::String.optional
|
|
103
|
+
attribute? :results, Types::Hash.optional
|
|
104
|
+
|
|
105
|
+
# Readiness fields
|
|
106
|
+
attribute :current_state, Types::String
|
|
107
|
+
attribute :dependencies_satisfied, Types::Bool
|
|
108
|
+
attribute :retry_eligible, Types::Bool
|
|
109
|
+
attribute :ready_for_execution, Types::Bool
|
|
110
|
+
attribute :total_parents, Types::Integer
|
|
111
|
+
attribute :completed_parents, Types::Integer
|
|
112
|
+
attribute :attempts, Types::Integer
|
|
113
|
+
attribute :max_attempts, Types::Integer
|
|
114
|
+
attribute? :last_failure_at, Types::String.optional
|
|
115
|
+
attribute? :next_retry_at, Types::String.optional
|
|
116
|
+
attribute? :last_attempted_at, Types::String.optional
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Step audit history response (SOC2 compliance).
|
|
120
|
+
# Returned by client_get_step_audit_history.
|
|
121
|
+
class StepAuditResponse < Dry::Struct
|
|
122
|
+
attribute :audit_uuid, Types::String
|
|
123
|
+
attribute :workflow_step_uuid, Types::String
|
|
124
|
+
attribute :transition_uuid, Types::String
|
|
125
|
+
attribute :task_uuid, Types::String
|
|
126
|
+
attribute :recorded_at, Types::String
|
|
127
|
+
attribute? :worker_uuid, Types::String.optional
|
|
128
|
+
attribute? :correlation_id, Types::String.optional
|
|
129
|
+
attribute :success, Types::Bool
|
|
130
|
+
attribute? :execution_time_ms, Types::Integer.optional
|
|
131
|
+
attribute? :result, Types::Hash.optional
|
|
132
|
+
attribute :step_name, Types::String
|
|
133
|
+
attribute? :from_state, Types::String.optional
|
|
134
|
+
attribute :to_state, Types::String
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Health check response from the orchestration API.
|
|
138
|
+
# Returned by client_health_check.
|
|
139
|
+
class HealthResponse < Dry::Struct
|
|
140
|
+
attribute :status, Types::String
|
|
141
|
+
attribute :timestamp, Types::String
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
require 'dry-types'
|
|
5
|
+
|
|
6
|
+
module TaskerCore
|
|
7
|
+
module Types
|
|
8
|
+
# DecisionPointOutcome - TAS-53 Dynamic Workflow Decision Points
|
|
9
|
+
#
|
|
10
|
+
# Represents the outcome of a decision point handler execution.
|
|
11
|
+
# Decision point handlers make runtime decisions about which workflow steps
|
|
12
|
+
# to create dynamically based on business logic.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage Patterns
|
|
15
|
+
#
|
|
16
|
+
# ### No Branches (No additional steps needed)
|
|
17
|
+
# ```ruby
|
|
18
|
+
# DecisionPointOutcome.no_branches
|
|
19
|
+
# ```
|
|
20
|
+
#
|
|
21
|
+
# ### Create Specific Steps
|
|
22
|
+
# ```ruby
|
|
23
|
+
# # Simple: create one or more steps by name
|
|
24
|
+
# DecisionPointOutcome.create_steps(["approval_required"])
|
|
25
|
+
#
|
|
26
|
+
# # Multiple branches based on condition
|
|
27
|
+
# if high_value?
|
|
28
|
+
# DecisionPointOutcome.create_steps(["manager_approval", "finance_review"])
|
|
29
|
+
# else
|
|
30
|
+
# DecisionPointOutcome.create_steps(["auto_approve"])
|
|
31
|
+
# end
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
34
|
+
# ## Integration with Step Handlers
|
|
35
|
+
#
|
|
36
|
+
# Decision point handlers should return this outcome in their result:
|
|
37
|
+
#
|
|
38
|
+
# ```ruby
|
|
39
|
+
# def call(context)
|
|
40
|
+
# # Business logic to determine which steps to create
|
|
41
|
+
# steps_to_create = if context.get_task_field('amount') > 1000
|
|
42
|
+
# ['manager_approval', 'finance_review']
|
|
43
|
+
# else
|
|
44
|
+
# ['auto_approve']
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# outcome = DecisionPointOutcome.create_steps(steps_to_create)
|
|
48
|
+
#
|
|
49
|
+
# StepHandlerCallResult.success(
|
|
50
|
+
# result: {
|
|
51
|
+
# decision_point_outcome: outcome.to_h
|
|
52
|
+
# },
|
|
53
|
+
# metadata: {
|
|
54
|
+
# operation: 'routing_decision',
|
|
55
|
+
# branches_created: steps_to_create.size
|
|
56
|
+
# }
|
|
57
|
+
# )
|
|
58
|
+
# end
|
|
59
|
+
# ```
|
|
60
|
+
module DecisionPointOutcome
|
|
61
|
+
module Types
|
|
62
|
+
include Dry.Types()
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# NoBranches outcome - no additional steps needed
|
|
66
|
+
class NoBranches < Dry::Struct
|
|
67
|
+
# Type discriminator (matches Rust serde tag field)
|
|
68
|
+
attribute :type, Types::String.default('no_branches')
|
|
69
|
+
|
|
70
|
+
# Convert to hash for serialization to Rust
|
|
71
|
+
# Note: Rust expects lowercase snake_case due to #[serde(rename_all = "snake_case")]
|
|
72
|
+
def to_h
|
|
73
|
+
{ type: 'no_branches' }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if this outcome requires step creation
|
|
77
|
+
def requires_step_creation?
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get step names (empty for NoBranches)
|
|
82
|
+
def step_names
|
|
83
|
+
[]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# CreateSteps outcome - dynamically create specified workflow steps
|
|
88
|
+
class CreateSteps < Dry::Struct
|
|
89
|
+
# Type discriminator (matches Rust serde tag field)
|
|
90
|
+
attribute :type, Types::String.default('create_steps')
|
|
91
|
+
|
|
92
|
+
# Array of step names to create
|
|
93
|
+
attribute :step_names, Types::Array.of(Types::Strict::String).constrained(min_size: 1)
|
|
94
|
+
|
|
95
|
+
# Convert to hash for serialization to Rust
|
|
96
|
+
# Note: Rust expects lowercase snake_case due to #[serde(rename_all = "snake_case")]
|
|
97
|
+
def to_h
|
|
98
|
+
{
|
|
99
|
+
type: 'create_steps',
|
|
100
|
+
step_names: step_names
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if this outcome requires step creation
|
|
105
|
+
def requires_step_creation?
|
|
106
|
+
true
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Factory methods for creating outcomes
|
|
111
|
+
class << self
|
|
112
|
+
# Create a NoBranches outcome
|
|
113
|
+
#
|
|
114
|
+
# Use when the decision point determines that no additional workflow
|
|
115
|
+
# steps are needed.
|
|
116
|
+
#
|
|
117
|
+
# @return [NoBranches] A no-branches outcome instance
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
# DecisionPointOutcome.no_branches
|
|
121
|
+
def no_branches
|
|
122
|
+
NoBranches.new
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Create a CreateSteps outcome with specified step names
|
|
126
|
+
#
|
|
127
|
+
# Use when the decision point determines that one or more workflow
|
|
128
|
+
# steps should be dynamically created.
|
|
129
|
+
#
|
|
130
|
+
# @param step_names [Array<String>] Names of steps to create (must be valid step names from template)
|
|
131
|
+
# @return [CreateSteps] A create-steps outcome instance
|
|
132
|
+
# @raise [ArgumentError] if step_names is empty
|
|
133
|
+
#
|
|
134
|
+
# @example Create single step
|
|
135
|
+
# DecisionPointOutcome.create_steps(['approval_required'])
|
|
136
|
+
#
|
|
137
|
+
# @example Create multiple steps
|
|
138
|
+
# DecisionPointOutcome.create_steps(['manager_approval', 'finance_review'])
|
|
139
|
+
def create_steps(step_names)
|
|
140
|
+
raise ArgumentError, 'step_names cannot be empty' if step_names.nil? || step_names.empty?
|
|
141
|
+
|
|
142
|
+
CreateSteps.new(step_names: step_names)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Parse a DecisionPointOutcome from a hash
|
|
146
|
+
#
|
|
147
|
+
# Used to deserialize outcomes from step handler results.
|
|
148
|
+
#
|
|
149
|
+
# @param hash [Hash] The hash representation of an outcome
|
|
150
|
+
# @return [NoBranches, CreateSteps, nil] The parsed outcome or nil if invalid
|
|
151
|
+
#
|
|
152
|
+
# @example
|
|
153
|
+
# outcome = DecisionPointOutcome.from_hash({
|
|
154
|
+
# type: 'create_steps',
|
|
155
|
+
# step_names: ['approval_required']
|
|
156
|
+
# })
|
|
157
|
+
def from_hash(hash)
|
|
158
|
+
return nil unless hash.is_a?(Hash)
|
|
159
|
+
|
|
160
|
+
# Support both symbol and string keys
|
|
161
|
+
# Note: Rust sends lowercase snake_case due to #[serde(rename_all = "snake_case")]
|
|
162
|
+
outcome_type = hash[:type] || hash['type']
|
|
163
|
+
|
|
164
|
+
case outcome_type
|
|
165
|
+
when 'no_branches'
|
|
166
|
+
NoBranches.new
|
|
167
|
+
when 'create_steps'
|
|
168
|
+
step_names = hash[:step_names] || hash['step_names']
|
|
169
|
+
return nil unless step_names.is_a?(Array) && !step_names.empty?
|
|
170
|
+
|
|
171
|
+
CreateSteps.new(step_names: step_names)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
module Types
|
|
5
|
+
# Standard error type constants for cross-language consistency.
|
|
6
|
+
#
|
|
7
|
+
# These error types are used across Ruby, Python, and Rust workers
|
|
8
|
+
# to categorize step execution failures consistently.
|
|
9
|
+
#
|
|
10
|
+
# Note: Values use PascalCase to match StepHandlerCallResult::Error constraints.
|
|
11
|
+
#
|
|
12
|
+
# @example Using error types in a handler
|
|
13
|
+
# failure(
|
|
14
|
+
# message: "Invalid order total",
|
|
15
|
+
# error_type: TaskerCore::Types::ErrorTypes::VALIDATION_ERROR,
|
|
16
|
+
# retryable: false
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# @example Checking if an error type is valid
|
|
20
|
+
# TaskerCore::Types::ErrorTypes.valid?('ValidationError') # => true
|
|
21
|
+
# TaskerCore::Types::ErrorTypes.valid?('unknown_type') # => false
|
|
22
|
+
module ErrorTypes
|
|
23
|
+
# Permanent error that should not be retried
|
|
24
|
+
PERMANENT_ERROR = 'PermanentError'
|
|
25
|
+
|
|
26
|
+
# Retryable error that may succeed on subsequent attempts
|
|
27
|
+
RETRYABLE_ERROR = 'RetryableError'
|
|
28
|
+
|
|
29
|
+
# Validation error due to invalid input data
|
|
30
|
+
VALIDATION_ERROR = 'ValidationError'
|
|
31
|
+
|
|
32
|
+
# Unexpected error from unknown causes
|
|
33
|
+
UNEXPECTED_ERROR = 'UnexpectedError'
|
|
34
|
+
|
|
35
|
+
# Step completion error
|
|
36
|
+
STEP_COMPLETION_ERROR = 'StepCompletionError'
|
|
37
|
+
|
|
38
|
+
# All valid error types (matching StepHandlerCallResult::Error constraints)
|
|
39
|
+
ALL = [
|
|
40
|
+
PERMANENT_ERROR,
|
|
41
|
+
RETRYABLE_ERROR,
|
|
42
|
+
VALIDATION_ERROR,
|
|
43
|
+
UNEXPECTED_ERROR,
|
|
44
|
+
STEP_COMPLETION_ERROR
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
# Check if an error type is a recognized standard type
|
|
48
|
+
#
|
|
49
|
+
# @param error_type [String] The error type to check
|
|
50
|
+
# @return [Boolean] True if the error type is recognized
|
|
51
|
+
def self.valid?(error_type)
|
|
52
|
+
ALL.include?(error_type)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if an error type is typically retryable
|
|
56
|
+
#
|
|
57
|
+
# @param error_type [String] The error type to check
|
|
58
|
+
# @return [Boolean] True if the error type typically allows retry
|
|
59
|
+
def self.typically_retryable?(error_type)
|
|
60
|
+
[RETRYABLE_ERROR, UNEXPECTED_ERROR].include?(error_type)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check if an error type is typically permanent (not retryable)
|
|
64
|
+
#
|
|
65
|
+
# @param error_type [String] The error type to check
|
|
66
|
+
# @return [Boolean] True if the error type is typically permanent
|
|
67
|
+
def self.typically_permanent?(error_type)
|
|
68
|
+
[PERMANENT_ERROR, VALIDATION_ERROR].include?(error_type)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|