tasker-rb 0.1.5-x86_64-linux → 0.1.6-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 +4 -4
- data/lib/tasker_core/handlers.rb +1 -33
- data/lib/tasker_core/registry/handler_registry.rb +24 -2
- data/lib/tasker_core/step_handler/base.rb +4 -1
- data/lib/tasker_core/step_handler/functional.rb +452 -0
- data/lib/tasker_core/step_handler/mixins.rb +1 -0
- data/lib/tasker_core/tasker_rb.so +0 -0
- data/lib/tasker_core/template_discovery.rb +0 -5
- data/lib/tasker_core/test_environment.rb +14 -1
- data/lib/tasker_core/types/task_template.rb +0 -8
- data/lib/tasker_core/version.rb +1 -1
- metadata +2 -2
- data/lib/tasker_core/task_handler/base.rb +0 -254
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ad2e7070082b3ce68f86829a83781eaf94b1fbcecfe32bcbbd3f3c668d47d25
|
|
4
|
+
data.tar.gz: 05ac453ea270917bfafc5558e430d6129b2abba4aaffda3ec58467c60c5725d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c8796a036d3ed7e9cfd5818c09ebd8677b7a841721e6e36cce81d787280390d1a8dcd5e312e688f2602afc40d44fc5c560ad5c74039d327f4456a355d8ec779
|
|
7
|
+
data.tar.gz: 5a8b5d080096c7ba5c106e59056132ba8e72319bb0775a2de096c10acb1185bde3cbda0672d800a8d71fe9c5e1c11c4a4802adcb0cd9a640dab8e7b38634af75
|
data/lib/tasker_core/handlers.rb
CHANGED
|
@@ -4,8 +4,7 @@ module TaskerCore
|
|
|
4
4
|
# Clean Handlers Domain API
|
|
5
5
|
#
|
|
6
6
|
# This module provides the primary public interface for working with TaskerCore handlers.
|
|
7
|
-
# It
|
|
8
|
-
# architecture while providing a clean Ruby interface with enhanced type safety.
|
|
7
|
+
# It provides a clean Ruby interface with enhanced type safety for step handlers.
|
|
9
8
|
#
|
|
10
9
|
# The Handlers namespace serves as the recommended entry point for handler operations,
|
|
11
10
|
# abstracting the underlying implementation details while preserving method signatures
|
|
@@ -60,14 +59,8 @@ module TaskerCore
|
|
|
60
59
|
# end
|
|
61
60
|
# end
|
|
62
61
|
#
|
|
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
62
|
# Architecture:
|
|
69
63
|
# - **Steps**: Individual business logic units (payment processing, API calls, etc.)
|
|
70
|
-
# - **Tasks**: Workflow orchestration and step coordination
|
|
71
64
|
# - **API**: Specialized step handlers for HTTP operations with automatic retry
|
|
72
65
|
#
|
|
73
66
|
# Method Signature:
|
|
@@ -78,7 +71,6 @@ module TaskerCore
|
|
|
78
71
|
# - `context.dependency_results` - Results from parent steps
|
|
79
72
|
#
|
|
80
73
|
# @see TaskerCore::Handlers::Steps For step-level business logic
|
|
81
|
-
# @see TaskerCore::Handlers::Tasks For task-level orchestration
|
|
82
74
|
# @see TaskerCore::StepHandler::Base For low-level step handler implementation
|
|
83
75
|
# @see TaskerCore::StepHandler::Api For HTTP-based handlers
|
|
84
76
|
module Handlers
|
|
@@ -131,29 +123,5 @@ module TaskerCore
|
|
|
131
123
|
end
|
|
132
124
|
end
|
|
133
125
|
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
126
|
end
|
|
159
127
|
end
|
|
@@ -298,7 +298,8 @@ module TaskerCore
|
|
|
298
298
|
return ENV['TASKER_TEMPLATE_PATH'] if ENV['TASKER_TEMPLATE_PATH']
|
|
299
299
|
|
|
300
300
|
# 2. Test environment: default to spec/fixtures/templates
|
|
301
|
-
|
|
301
|
+
# (skip when TASKER_SKIP_EXAMPLE_HANDLERS is set — the app provides its own templates)
|
|
302
|
+
if test_environment? && ENV['TASKER_SKIP_EXAMPLE_HANDLERS'] != 'true'
|
|
302
303
|
fixtures_path = File.expand_path('../../../spec/fixtures/templates', __dir__)
|
|
303
304
|
return fixtures_path if Dir.exist?(fixtures_path)
|
|
304
305
|
end
|
|
@@ -375,7 +376,8 @@ module TaskerCore
|
|
|
375
376
|
end
|
|
376
377
|
|
|
377
378
|
# In test environment, add spec/handlers/examples for test fixtures
|
|
378
|
-
|
|
379
|
+
# (skip when TASKER_SKIP_EXAMPLE_HANDLERS is set — the app provides its own handlers)
|
|
380
|
+
if test_environment? && ENV['TASKER_SKIP_EXAMPLE_HANDLERS'] != 'true'
|
|
379
381
|
spec_dir = File.expand_path('../../../spec/handlers/examples', __dir__)
|
|
380
382
|
Dir.glob("#{spec_dir}/**/").each { |dir| paths << dir } if Dir.exist?(spec_dir)
|
|
381
383
|
end
|
|
@@ -423,6 +425,26 @@ module TaskerCore
|
|
|
423
425
|
# Register with FULL class name so templates can reference it properly
|
|
424
426
|
register_handler(handler_class_name, handler_class)
|
|
425
427
|
registered_count += 1
|
|
428
|
+
|
|
429
|
+
# TAS-294: Also register under the DSL handler_name if it differs from the class name.
|
|
430
|
+
# DSL handlers created via step_handler('dot.notation.name') { ... } have a handler_name
|
|
431
|
+
# instance method that returns the dot-notation callable (e.g., 'linear_workflow_dsl.step_handlers.linear_step_1')
|
|
432
|
+
# but are registered above under their Ruby class name (e.g., 'LinearStep1DslHandler').
|
|
433
|
+
# Templates reference the dot-notation name, so we need both keys in the registry.
|
|
434
|
+
begin
|
|
435
|
+
instance = handler_class.new
|
|
436
|
+
if instance.respond_to?(:handler_name)
|
|
437
|
+
dsl_name = instance.handler_name
|
|
438
|
+
if dsl_name && dsl_name != handler_class_name
|
|
439
|
+
register_handler(dsl_name, handler_class)
|
|
440
|
+
registered_count += 1
|
|
441
|
+
logger.debug("✅ Registered DSL handler alias: #{dsl_name}")
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
rescue StandardError
|
|
445
|
+
# Some handlers require config args - skip DSL name registration for those
|
|
446
|
+
end
|
|
447
|
+
|
|
426
448
|
logger.debug("✅ Registered preloaded test handler: #{handler_class_name}")
|
|
427
449
|
else
|
|
428
450
|
logger.debug("⚠️ Preloaded handler class not found: #{handler_class_name}")
|
|
@@ -80,7 +80,10 @@ module TaskerCore
|
|
|
80
80
|
class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
# Get handler metadata for monitoring and introspection
|
|
83
|
+
# Get handler metadata for monitoring and introspection.
|
|
84
|
+
# Note: VERSION is currently a no-op — not yet wired through FFI bridges
|
|
85
|
+
# to StepExecutionMetadata. Reserved for future observability and
|
|
86
|
+
# compatibility tracking.
|
|
84
87
|
# @return [Hash] Handler metadata
|
|
85
88
|
def metadata
|
|
86
89
|
{
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Functional/block DSL for step handlers (TAS-294).
|
|
4
|
+
#
|
|
5
|
+
# This module provides block-based alternatives to the class-based handler API.
|
|
6
|
+
# It reduces boilerplate for common handler patterns while preserving full access
|
|
7
|
+
# to the underlying StepContext for advanced use cases.
|
|
8
|
+
#
|
|
9
|
+
# The DSL methods auto-wrap return values and classify exceptions:
|
|
10
|
+
# - Hash return -> StepHandlerCallResult.success(result: hash)
|
|
11
|
+
# - StepHandlerCallResult return -> pass through unchanged
|
|
12
|
+
# - PermanentError raised -> StepHandlerCallResult.error(retryable: false)
|
|
13
|
+
# - RetryableError raised -> StepHandlerCallResult.error(retryable: true)
|
|
14
|
+
# - Other errors -> StepHandlerCallResult.error(retryable: true)
|
|
15
|
+
#
|
|
16
|
+
# @example Basic handler
|
|
17
|
+
# include TaskerCore::StepHandler::Functional
|
|
18
|
+
#
|
|
19
|
+
# step_handler "process_payment",
|
|
20
|
+
# depends_on: { cart: "validate_cart" },
|
|
21
|
+
# inputs: [:payment_info] do |cart:, payment_info:, context:|
|
|
22
|
+
#
|
|
23
|
+
# raise TaskerCore::Errors::PermanentError, "Payment info required" unless payment_info
|
|
24
|
+
# result = charge_card(payment_info, cart["total"])
|
|
25
|
+
# { payment_id: result["id"], amount: cart["total"] }
|
|
26
|
+
# end
|
|
27
|
+
|
|
28
|
+
module TaskerCore
|
|
29
|
+
module StepHandler
|
|
30
|
+
module Functional
|
|
31
|
+
# ========================================================================
|
|
32
|
+
# Helper: Decision
|
|
33
|
+
# ========================================================================
|
|
34
|
+
|
|
35
|
+
# Helper for decision handler return values.
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# decision_handler "route_order",
|
|
39
|
+
# depends_on: { order: "validate_order" } do |order:, context:|
|
|
40
|
+
# if order["tier"] == "premium"
|
|
41
|
+
# Decision.route(["process_premium"], tier: "premium")
|
|
42
|
+
# else
|
|
43
|
+
# Decision.route(["process_standard"])
|
|
44
|
+
# end
|
|
45
|
+
# end
|
|
46
|
+
class Decision
|
|
47
|
+
attr_reader :type, :steps, :reason, :routing_context
|
|
48
|
+
|
|
49
|
+
def initialize(type, steps, reason, routing_context)
|
|
50
|
+
@type = type
|
|
51
|
+
@steps = steps
|
|
52
|
+
@reason = reason
|
|
53
|
+
@routing_context = routing_context
|
|
54
|
+
freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Route to the specified steps.
|
|
58
|
+
#
|
|
59
|
+
# @param steps [Array<String>] Step names to route to
|
|
60
|
+
# @param routing_context [Hash] Additional routing context
|
|
61
|
+
# @return [Decision]
|
|
62
|
+
def self.route(steps, **routing_context)
|
|
63
|
+
new('create_steps', Array(steps), nil, routing_context)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Skip all branches.
|
|
67
|
+
#
|
|
68
|
+
# @param reason [String] Reason for skipping
|
|
69
|
+
# @param routing_context [Hash] Additional routing context
|
|
70
|
+
# @return [Decision]
|
|
71
|
+
def self.skip(reason, **routing_context)
|
|
72
|
+
new('no_branches', [], reason, routing_context)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# ========================================================================
|
|
77
|
+
# Helper: BatchConfig
|
|
78
|
+
# ========================================================================
|
|
79
|
+
|
|
80
|
+
# Configuration returned by batch analyzer handlers.
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# batch_analyzer "analyze_orders", worker_template: "process_batch" do |context:|
|
|
84
|
+
# BatchConfig.new(total_items: 250, batch_size: 100)
|
|
85
|
+
# end
|
|
86
|
+
class BatchConfig
|
|
87
|
+
attr_reader :total_items, :batch_size, :metadata
|
|
88
|
+
|
|
89
|
+
# @param total_items [Integer] Total number of items to process
|
|
90
|
+
# @param batch_size [Integer] Size of each batch
|
|
91
|
+
# @param metadata [Hash] Optional metadata
|
|
92
|
+
def initialize(total_items:, batch_size:, metadata: {})
|
|
93
|
+
@total_items = total_items
|
|
94
|
+
@batch_size = batch_size
|
|
95
|
+
@metadata = metadata
|
|
96
|
+
freeze
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ========================================================================
|
|
101
|
+
# Internal: Auto-wrapping
|
|
102
|
+
# ========================================================================
|
|
103
|
+
|
|
104
|
+
module_function
|
|
105
|
+
|
|
106
|
+
# @api private
|
|
107
|
+
def _wrap_result(result)
|
|
108
|
+
case result
|
|
109
|
+
when Types::StepHandlerCallResult::Success,
|
|
110
|
+
Types::StepHandlerCallResult::Error,
|
|
111
|
+
Types::StepHandlerCallResult::CheckpointYield
|
|
112
|
+
result
|
|
113
|
+
when Hash
|
|
114
|
+
Types::StepHandlerCallResult.success(result: result)
|
|
115
|
+
when nil
|
|
116
|
+
Types::StepHandlerCallResult.success(result: {})
|
|
117
|
+
else
|
|
118
|
+
Types::StepHandlerCallResult.success(result: result.respond_to?(:to_h) ? result.to_h : {})
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @api private
|
|
123
|
+
def _wrap_exception(exception)
|
|
124
|
+
Types::StepHandlerCallResult.from_exception(exception)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @api private
|
|
128
|
+
def _inject_args(context, depends_on, inputs)
|
|
129
|
+
args = { context: context }
|
|
130
|
+
|
|
131
|
+
depends_on.each do |param_name, value|
|
|
132
|
+
if value.is_a?(Array) && value.length == 2
|
|
133
|
+
step_name, model_cls = value
|
|
134
|
+
raw = context.get_dependency_result(step_name.to_s)
|
|
135
|
+
args[param_name.to_sym] = if raw.is_a?(Hash)
|
|
136
|
+
# Convert to plain Hash first — ActiveSupport::HashWithIndifferentAccess
|
|
137
|
+
# stores keys as strings internally even after transform_keys(&:to_sym),
|
|
138
|
+
# which causes Dry::Struct to silently ignore all attributes.
|
|
139
|
+
symbolized = Hash(raw).transform_keys(&:to_sym)
|
|
140
|
+
known = model_cls.attribute_names
|
|
141
|
+
model_cls.new(**symbolized.slice(*known))
|
|
142
|
+
else
|
|
143
|
+
raw
|
|
144
|
+
end
|
|
145
|
+
else
|
|
146
|
+
args[param_name.to_sym] = context.get_dependency_result(value.to_s)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if inputs.is_a?(Class)
|
|
151
|
+
model_data = {}
|
|
152
|
+
inputs.attribute_names.each do |attr_name|
|
|
153
|
+
model_data[attr_name] = context.get_input(attr_name.to_s)
|
|
154
|
+
end
|
|
155
|
+
args[:inputs] = inputs.new(**model_data.compact)
|
|
156
|
+
args[:inputs].validate! if args[:inputs].respond_to?(:validate!)
|
|
157
|
+
else
|
|
158
|
+
Array(inputs).each do |input_key|
|
|
159
|
+
args[input_key.to_sym] = context.get_input(input_key.to_s)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
args
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# ========================================================================
|
|
167
|
+
# Public DSL Methods
|
|
168
|
+
# ========================================================================
|
|
169
|
+
|
|
170
|
+
# Define a step handler from a block.
|
|
171
|
+
#
|
|
172
|
+
# Returns a StepHandler::Base subclass that can be registered with HandlerRegistry.
|
|
173
|
+
# The block receives injected dependencies, inputs, and context as keyword arguments.
|
|
174
|
+
# Return values are auto-wrapped as success results, and exceptions are
|
|
175
|
+
# auto-classified as failure results.
|
|
176
|
+
#
|
|
177
|
+
# @param name [String] Handler name (must match step definition)
|
|
178
|
+
# @param depends_on [Hash] Mapping of parameter names to dependency step names
|
|
179
|
+
# @param inputs [Array<Symbol, String>] Task context input keys to inject
|
|
180
|
+
# @param version [String] Handler version (default: "1.0.0")
|
|
181
|
+
# @yield [**args] Block receiving dependencies, inputs, and context as keyword args
|
|
182
|
+
# @return [Class] StepHandler::Base subclass
|
|
183
|
+
#
|
|
184
|
+
# @example
|
|
185
|
+
# ProcessPayment = step_handler "process_payment",
|
|
186
|
+
# depends_on: { cart: "validate_cart" },
|
|
187
|
+
# inputs: [:payment_info] do |cart:, payment_info:, context:|
|
|
188
|
+
#
|
|
189
|
+
# raise TaskerCore::Errors::PermanentError, "Payment info required" unless payment_info
|
|
190
|
+
# result = charge_card(payment_info, cart["total"])
|
|
191
|
+
# { payment_id: result["id"], amount: cart["total"] }
|
|
192
|
+
# end
|
|
193
|
+
def step_handler(name, depends_on: {}, inputs: [], version: '1.0.0', &block)
|
|
194
|
+
raise ArgumentError, 'block required' unless block
|
|
195
|
+
|
|
196
|
+
handler_depends = depends_on
|
|
197
|
+
handler_inputs = inputs
|
|
198
|
+
handler_block = block
|
|
199
|
+
|
|
200
|
+
Class.new(Base) do
|
|
201
|
+
const_set(:VERSION, version)
|
|
202
|
+
|
|
203
|
+
define_method(:handler_name) { name }
|
|
204
|
+
|
|
205
|
+
define_method(:call) do |context|
|
|
206
|
+
args = Functional._inject_args(context, handler_depends, handler_inputs)
|
|
207
|
+
result = handler_block.call(**args)
|
|
208
|
+
Functional._wrap_result(result)
|
|
209
|
+
rescue StandardError => e
|
|
210
|
+
Functional._wrap_exception(e)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Define a decision handler from a block.
|
|
216
|
+
#
|
|
217
|
+
# The block should return a `Decision.route(...)` or `Decision.skip(...)`.
|
|
218
|
+
#
|
|
219
|
+
# @param name [String] Handler name
|
|
220
|
+
# @param depends_on [Hash] Mapping of parameter names to dependency step names
|
|
221
|
+
# @param inputs [Array<Symbol, String>] Task context input keys to inject
|
|
222
|
+
# @param version [String] Handler version (default: "1.0.0")
|
|
223
|
+
# @yield [**args] Block returning a Decision
|
|
224
|
+
# @return [Class] StepHandler::Base subclass with decision capabilities
|
|
225
|
+
#
|
|
226
|
+
# @example
|
|
227
|
+
# RouteOrder = decision_handler "route_order",
|
|
228
|
+
# depends_on: { order: "validate_order" } do |order:, context:|
|
|
229
|
+
# if order["tier"] == "premium"
|
|
230
|
+
# Decision.route(["process_premium"], tier: "premium")
|
|
231
|
+
# else
|
|
232
|
+
# Decision.route(["process_standard"])
|
|
233
|
+
# end
|
|
234
|
+
# end
|
|
235
|
+
def decision_handler(name, depends_on: {}, inputs: [], version: '1.0.0', &block)
|
|
236
|
+
raise ArgumentError, 'block required' unless block
|
|
237
|
+
|
|
238
|
+
handler_depends = depends_on
|
|
239
|
+
handler_inputs = inputs
|
|
240
|
+
handler_block = block
|
|
241
|
+
|
|
242
|
+
Class.new(Base) do
|
|
243
|
+
include Mixins::Decision
|
|
244
|
+
|
|
245
|
+
const_set(:VERSION, version)
|
|
246
|
+
|
|
247
|
+
define_method(:handler_name) { name }
|
|
248
|
+
|
|
249
|
+
define_method(:call) do |context|
|
|
250
|
+
args = Functional._inject_args(context, handler_depends, handler_inputs)
|
|
251
|
+
raw_result = handler_block.call(**args)
|
|
252
|
+
|
|
253
|
+
case raw_result
|
|
254
|
+
when Types::StepHandlerCallResult::Success,
|
|
255
|
+
Types::StepHandlerCallResult::Error
|
|
256
|
+
raw_result
|
|
257
|
+
when Decision
|
|
258
|
+
result_data = {}
|
|
259
|
+
if raw_result.type == 'create_steps'
|
|
260
|
+
result_data[:routing_context] = raw_result.routing_context unless raw_result.routing_context.empty?
|
|
261
|
+
decision_success(steps: raw_result.steps, result_data: result_data)
|
|
262
|
+
else
|
|
263
|
+
result_data[:reason] = raw_result.reason if raw_result.reason
|
|
264
|
+
result_data[:routing_context] = raw_result.routing_context unless raw_result.routing_context.empty?
|
|
265
|
+
decision_no_branches(result_data: result_data)
|
|
266
|
+
end
|
|
267
|
+
else
|
|
268
|
+
Functional._wrap_result(raw_result)
|
|
269
|
+
end
|
|
270
|
+
rescue StandardError => e
|
|
271
|
+
Functional._wrap_exception(e)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Define a batch analyzer handler.
|
|
277
|
+
#
|
|
278
|
+
# The block should return a `BatchConfig` with `total_items` and `batch_size`.
|
|
279
|
+
# Cursor configs are generated automatically.
|
|
280
|
+
#
|
|
281
|
+
# @param name [String] Handler name
|
|
282
|
+
# @param worker_template [String] Name of the worker template step
|
|
283
|
+
# @param depends_on [Hash] Mapping of parameter names to dependency step names
|
|
284
|
+
# @param inputs [Array<Symbol, String>] Task context input keys to inject
|
|
285
|
+
# @param version [String] Handler version (default: "1.0.0")
|
|
286
|
+
# @yield [**args] Block returning a BatchConfig
|
|
287
|
+
# @return [Class] StepHandler::Base subclass
|
|
288
|
+
#
|
|
289
|
+
# @example
|
|
290
|
+
# AnalyzeOrders = batch_analyzer "analyze_orders",
|
|
291
|
+
# worker_template: "process_batch" do |context:|
|
|
292
|
+
# BatchConfig.new(total_items: 250, batch_size: 100)
|
|
293
|
+
# end
|
|
294
|
+
def batch_analyzer(name, worker_template:, depends_on: {}, inputs: [], version: '1.0.0', &block)
|
|
295
|
+
raise ArgumentError, 'block required' unless block
|
|
296
|
+
|
|
297
|
+
handler_depends = depends_on
|
|
298
|
+
handler_inputs = inputs
|
|
299
|
+
handler_block = block
|
|
300
|
+
handler_worker_template = worker_template
|
|
301
|
+
|
|
302
|
+
Class.new(Base) do
|
|
303
|
+
include Mixins::Batchable
|
|
304
|
+
|
|
305
|
+
const_set(:VERSION, version)
|
|
306
|
+
|
|
307
|
+
define_method(:handler_name) { name }
|
|
308
|
+
|
|
309
|
+
define_method(:call) do |context|
|
|
310
|
+
args = Functional._inject_args(context, handler_depends, handler_inputs)
|
|
311
|
+
raw_result = handler_block.call(**args)
|
|
312
|
+
|
|
313
|
+
case raw_result
|
|
314
|
+
when Types::StepHandlerCallResult::Success,
|
|
315
|
+
Types::StepHandlerCallResult::Error
|
|
316
|
+
raw_result
|
|
317
|
+
when BatchConfig
|
|
318
|
+
total_items = raw_result.total_items
|
|
319
|
+
batch_size = raw_result.batch_size
|
|
320
|
+
worker_count = (total_items.to_f / batch_size).ceil
|
|
321
|
+
|
|
322
|
+
cursor_configs = create_cursor_configs(total_items, worker_count)
|
|
323
|
+
|
|
324
|
+
create_batches_outcome(
|
|
325
|
+
worker_template_name: handler_worker_template,
|
|
326
|
+
cursor_configs: cursor_configs,
|
|
327
|
+
total_items: total_items,
|
|
328
|
+
metadata: raw_result.metadata
|
|
329
|
+
)
|
|
330
|
+
else
|
|
331
|
+
Functional._wrap_result(raw_result)
|
|
332
|
+
end
|
|
333
|
+
rescue StandardError => e
|
|
334
|
+
Functional._wrap_exception(e)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Define a batch worker handler.
|
|
340
|
+
#
|
|
341
|
+
# The block receives a `batch_context` keyword argument extracted from the
|
|
342
|
+
# step context, containing cursor configuration for this worker's partition.
|
|
343
|
+
#
|
|
344
|
+
# @param name [String] Handler name
|
|
345
|
+
# @param depends_on [Hash] Mapping of parameter names to dependency step names
|
|
346
|
+
# @param inputs [Array<Symbol, String>] Task context input keys to inject
|
|
347
|
+
# @param version [String] Handler version (default: "1.0.0")
|
|
348
|
+
# @yield [**args] Block receiving batch_context and other injected args
|
|
349
|
+
# @return [Class] StepHandler::Base subclass
|
|
350
|
+
#
|
|
351
|
+
# @example
|
|
352
|
+
# ProcessBatch = batch_worker "process_batch" do |batch_context:, context:|
|
|
353
|
+
# cursor = batch_context&.dig(:cursor_config)
|
|
354
|
+
# # process items from cursor[:start_cursor] to cursor[:end_cursor]
|
|
355
|
+
# { processed: true }
|
|
356
|
+
# end
|
|
357
|
+
def batch_worker(name, depends_on: {}, inputs: [], version: '1.0.0', &block)
|
|
358
|
+
raise ArgumentError, 'block required' unless block
|
|
359
|
+
|
|
360
|
+
handler_depends = depends_on
|
|
361
|
+
handler_inputs = inputs
|
|
362
|
+
handler_block = block
|
|
363
|
+
|
|
364
|
+
Class.new(Base) do
|
|
365
|
+
include Mixins::Batchable
|
|
366
|
+
|
|
367
|
+
const_set(:VERSION, version)
|
|
368
|
+
|
|
369
|
+
define_method(:handler_name) { name }
|
|
370
|
+
|
|
371
|
+
define_method(:call) do |context|
|
|
372
|
+
args = Functional._inject_args(context, handler_depends, handler_inputs)
|
|
373
|
+
|
|
374
|
+
# Delegate batch context extraction to Batchable mixin
|
|
375
|
+
args[:batch_context] = get_batch_context(context)
|
|
376
|
+
|
|
377
|
+
result = handler_block.call(**args)
|
|
378
|
+
Functional._wrap_result(result)
|
|
379
|
+
rescue StandardError => e
|
|
380
|
+
Functional._wrap_exception(e)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Define an API handler from a block.
|
|
386
|
+
#
|
|
387
|
+
# The block receives an `api` keyword argument that provides pre-configured
|
|
388
|
+
# HTTP methods (get, post, put, delete) and result helpers (api_success,
|
|
389
|
+
# api_failure) from the API mixin, in addition to any declared dependencies,
|
|
390
|
+
# inputs, and context.
|
|
391
|
+
#
|
|
392
|
+
# @param name [String] Handler name (must match step definition)
|
|
393
|
+
# @param base_url [String] Base URL for API calls
|
|
394
|
+
# @param depends_on [Hash] Mapping of parameter names to dependency step names
|
|
395
|
+
# @param inputs [Array<Symbol, String>] Task context input keys to inject
|
|
396
|
+
# @param version [String] Handler version (default: "1.0.0")
|
|
397
|
+
# @param timeout [Integer, nil] Request timeout in seconds
|
|
398
|
+
# @param open_timeout [Integer, nil] Connection open timeout in seconds
|
|
399
|
+
# @param headers [Hash] Default headers for all requests
|
|
400
|
+
# @yield [**args] Block receiving api, dependencies, inputs, and context as keyword args
|
|
401
|
+
# @return [Class] StepHandler::Base subclass with API capabilities
|
|
402
|
+
#
|
|
403
|
+
# @example
|
|
404
|
+
# FetchUser = api_handler "fetch_user",
|
|
405
|
+
# base_url: "https://api.example.com",
|
|
406
|
+
# depends_on: { user_id: "validate_user" } do |user_id:, api:, context:|
|
|
407
|
+
#
|
|
408
|
+
# response = api.get("/users/#{user_id}")
|
|
409
|
+
# body = JSON.parse(response.body)
|
|
410
|
+
# api.api_success(data: body, status: response.status)
|
|
411
|
+
# end
|
|
412
|
+
def api_handler(name, base_url:, depends_on: {}, inputs: [], version: '1.0.0',
|
|
413
|
+
timeout: nil, open_timeout: nil, headers: {}, &block)
|
|
414
|
+
raise ArgumentError, 'block required' unless block
|
|
415
|
+
|
|
416
|
+
handler_depends = depends_on
|
|
417
|
+
handler_inputs = inputs
|
|
418
|
+
handler_block = block
|
|
419
|
+
handler_base_url = base_url
|
|
420
|
+
handler_timeout = timeout
|
|
421
|
+
handler_open_timeout = open_timeout
|
|
422
|
+
handler_headers = headers
|
|
423
|
+
|
|
424
|
+
Class.new(Base) do
|
|
425
|
+
include Mixins::API
|
|
426
|
+
|
|
427
|
+
const_set(:VERSION, version)
|
|
428
|
+
|
|
429
|
+
define_method(:handler_name) { name }
|
|
430
|
+
|
|
431
|
+
define_method(:initialize) do
|
|
432
|
+
super(config: {
|
|
433
|
+
url: handler_base_url,
|
|
434
|
+
timeout: handler_timeout,
|
|
435
|
+
open_timeout: handler_open_timeout,
|
|
436
|
+
headers: handler_headers
|
|
437
|
+
}.compact)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
define_method(:call) do |context|
|
|
441
|
+
args = Functional._inject_args(context, handler_depends, handler_inputs)
|
|
442
|
+
args[:api] = self
|
|
443
|
+
result = handler_block.call(**args)
|
|
444
|
+
Functional._wrap_result(result)
|
|
445
|
+
rescue StandardError => e
|
|
446
|
+
Functional._wrap_exception(e)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
Binary file
|
|
@@ -94,11 +94,6 @@ module TaskerCore
|
|
|
94
94
|
def extract_handlers_from_template(template_data)
|
|
95
95
|
handlers = []
|
|
96
96
|
|
|
97
|
-
# Add task handler if present
|
|
98
|
-
if template_data['task_handler'] && template_data['task_handler']['callable']
|
|
99
|
-
handlers << template_data['task_handler']['callable']
|
|
100
|
-
end
|
|
101
|
-
|
|
102
97
|
# Add step handlers
|
|
103
98
|
if template_data['steps'].is_a?(Array)
|
|
104
99
|
template_data['steps'].each do |step|
|
|
@@ -16,8 +16,13 @@ module TaskerCore
|
|
|
16
16
|
@template_fixtures_path = File.expand_path('../../spec/fixtures/templates', __dir__)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Check if we should load test environment components
|
|
19
|
+
# Check if we should load test environment components.
|
|
20
|
+
# Apps using tasker-rb via a local path: dependency can set
|
|
21
|
+
# TASKER_SKIP_EXAMPLE_HANDLERS=true to prevent the gem's spec
|
|
22
|
+
# handlers from shadowing the app's own handlers.
|
|
20
23
|
def should_load_test_environment?
|
|
24
|
+
return false if ENV['TASKER_SKIP_EXAMPLE_HANDLERS'] == 'true'
|
|
25
|
+
|
|
21
26
|
test_env = ENV['TASKER_ENV']&.downcase == 'test'
|
|
22
27
|
rails_test_env = ENV['RAILS_ENV']&.downcase == 'test'
|
|
23
28
|
force_examples = ENV['TASKER_FORCE_EXAMPLE_HANDLERS'] == 'true'
|
|
@@ -176,6 +181,14 @@ module TaskerCore
|
|
|
176
181
|
handler_files = Dir.glob("#{@example_handlers_path}/**/*_handler.rb")
|
|
177
182
|
log_debug("🔍 Found #{handler_files.count} example handler files")
|
|
178
183
|
|
|
184
|
+
# TAS-294: Also load DSL example handlers (functional API mirrors)
|
|
185
|
+
dsl_examples_path = @example_handlers_path.sub(%r{/examples$}, '/dsl_examples')
|
|
186
|
+
if Dir.exist?(dsl_examples_path)
|
|
187
|
+
dsl_files = Dir.glob("#{dsl_examples_path}/**/*.rb")
|
|
188
|
+
log_debug("🔍 Found #{dsl_files.count} DSL example handler files")
|
|
189
|
+
handler_files += dsl_files
|
|
190
|
+
end
|
|
191
|
+
|
|
179
192
|
loaded_count = 0
|
|
180
193
|
handler_files.each do |handler_file|
|
|
181
194
|
# Use require instead of require_relative to avoid duplicate loading
|
|
@@ -139,7 +139,6 @@ module TaskerCore
|
|
|
139
139
|
|
|
140
140
|
# Environment-specific overrides
|
|
141
141
|
class EnvironmentOverride < Dry::Struct
|
|
142
|
-
attribute :task_handler, HandlerOverride.optional.default(nil)
|
|
143
142
|
attribute :steps, Types::Array.of(StepOverride).default([].freeze)
|
|
144
143
|
end
|
|
145
144
|
|
|
@@ -156,7 +155,6 @@ module TaskerCore
|
|
|
156
155
|
# Self-describing structure
|
|
157
156
|
attribute :description, Types::String.optional.default(nil)
|
|
158
157
|
attribute :metadata, TemplateMetadata.optional.default(nil)
|
|
159
|
-
attribute :task_handler, HandlerDefinition.optional.default(nil)
|
|
160
158
|
attribute(:system_dependencies, SystemDependencies.default { SystemDependencies.new })
|
|
161
159
|
attribute :domain_events, Types::Array.of(DomainEventDefinition).default([].freeze)
|
|
162
160
|
attribute :input_schema, Types::Hash.optional.default(nil) # JSON Schema
|
|
@@ -174,7 +172,6 @@ module TaskerCore
|
|
|
174
172
|
# Extract all callable references
|
|
175
173
|
def all_callables
|
|
176
174
|
callables = []
|
|
177
|
-
callables << task_handler.callable if task_handler
|
|
178
175
|
steps.each { |step| callables << step.handler.callable }
|
|
179
176
|
callables
|
|
180
177
|
end
|
|
@@ -197,11 +194,6 @@ module TaskerCore
|
|
|
197
194
|
if environments[environment_name]
|
|
198
195
|
env_override = environments[environment_name]
|
|
199
196
|
|
|
200
|
-
# Apply task handler overrides
|
|
201
|
-
if env_override.task_handler && resolved_template.task_handler && env_override.task_handler.initialization
|
|
202
|
-
resolved_template.task_handler.initialization.merge!(env_override.task_handler.initialization)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
197
|
# Apply step overrides
|
|
206
198
|
env_override.steps.each do |step_override|
|
|
207
199
|
if step_override.name == 'ALL'
|
data/lib/tasker_core/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tasker-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Pete Taylor
|
|
@@ -231,12 +231,12 @@ files:
|
|
|
231
231
|
- lib/tasker_core/step_handler/base.rb
|
|
232
232
|
- lib/tasker_core/step_handler/batchable.rb
|
|
233
233
|
- lib/tasker_core/step_handler/decision.rb
|
|
234
|
+
- lib/tasker_core/step_handler/functional.rb
|
|
234
235
|
- lib/tasker_core/step_handler/mixins.rb
|
|
235
236
|
- lib/tasker_core/step_handler/mixins/api.rb
|
|
236
237
|
- lib/tasker_core/step_handler/mixins/batchable.rb
|
|
237
238
|
- lib/tasker_core/step_handler/mixins/decision.rb
|
|
238
239
|
- lib/tasker_core/subscriber.rb
|
|
239
|
-
- lib/tasker_core/task_handler/base.rb
|
|
240
240
|
- lib/tasker_core/tasker_rb.so
|
|
241
241
|
- lib/tasker_core/template_discovery.rb
|
|
242
242
|
- lib/tasker_core/test_environment.rb
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'yaml'
|
|
4
|
-
require 'logger'
|
|
5
|
-
require 'securerandom'
|
|
6
|
-
|
|
7
|
-
module TaskerCore
|
|
8
|
-
module TaskHandler
|
|
9
|
-
class Base
|
|
10
|
-
# Ruby task handler for pgmq-based orchestration
|
|
11
|
-
#
|
|
12
|
-
# This class provides a simplified interface for task processing using the new
|
|
13
|
-
# pgmq architecture. Instead of TCP commands, it uses direct pgmq communication
|
|
14
|
-
# and the embedded orchestrator for step enqueueing.
|
|
15
|
-
#
|
|
16
|
-
# Key changes from TCP architecture:
|
|
17
|
-
# - No command_client dependencies
|
|
18
|
-
# - Direct pgmq messaging for task initialization
|
|
19
|
-
# - Embedded orchestrator for step enqueueing
|
|
20
|
-
# - Simplified error handling and validation
|
|
21
|
-
|
|
22
|
-
attr_reader :logger, :task_config
|
|
23
|
-
|
|
24
|
-
def initialize(task_config_path: nil, task_config: nil)
|
|
25
|
-
@logger = TaskerCore::Logger.instance
|
|
26
|
-
@task_config = task_config || (task_config_path ? load_task_config_from_path(task_config_path) : {})
|
|
27
|
-
@pgmq_client = nil # Lazy initialization to avoid database connection during setup
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Get or create pgmq client (lazy initialization)
|
|
31
|
-
# @return [TaskerCore::Messaging::PgmqClient] pgmq client instance
|
|
32
|
-
def pgmq_client
|
|
33
|
-
@pgmq_client ||= TaskerCore::Messaging::PgmqClient.new
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Main task processing method - Rails engine signature: handle(task_uuid)
|
|
37
|
-
#
|
|
38
|
-
# Mode-aware processing: Uses embedded FFI orchestrator in embedded mode,
|
|
39
|
-
# or pure pgmq communication in distributed mode.
|
|
40
|
-
#
|
|
41
|
-
# @param task_uuid [Integer] ID of the task to process
|
|
42
|
-
# @return [Hash] Result of step enqueueing operation
|
|
43
|
-
def handle(task_uuid)
|
|
44
|
-
unless task_uuid.is_a?(Integer)
|
|
45
|
-
raise TaskerCore::Errors::ValidationError.new('task_uuid is required and must be an integer', :task_uuid)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
mode = orchestration_mode
|
|
49
|
-
logger.info "🚀 Processing task #{task_uuid} with pgmq orchestration (#{mode} mode)"
|
|
50
|
-
|
|
51
|
-
case mode
|
|
52
|
-
when 'embedded'
|
|
53
|
-
handle_embedded_mode(task_uuid)
|
|
54
|
-
when 'distributed'
|
|
55
|
-
handle_distributed_mode(task_uuid)
|
|
56
|
-
else
|
|
57
|
-
raise TaskerCore::Errors::OrchestrationError,
|
|
58
|
-
"Unknown orchestration mode: #{mode}. Expected 'embedded' or 'distributed'"
|
|
59
|
-
end
|
|
60
|
-
rescue TaskerCore::Errors::OrchestrationError => e
|
|
61
|
-
logger.error "❌ Orchestration error for task #{task_uuid}: #{e.message}"
|
|
62
|
-
{
|
|
63
|
-
success: false,
|
|
64
|
-
task_uuid: task_uuid,
|
|
65
|
-
error: e.message,
|
|
66
|
-
error_type: 'OrchestrationError',
|
|
67
|
-
architecture: 'pgmq',
|
|
68
|
-
processed_at: Time.now.utc.iso8601
|
|
69
|
-
}
|
|
70
|
-
rescue StandardError => e
|
|
71
|
-
logger.error "❌ Unexpected error processing task #{task_uuid}: #{e.class.name}: #{e.message}"
|
|
72
|
-
{
|
|
73
|
-
success: false,
|
|
74
|
-
task_uuid: task_uuid,
|
|
75
|
-
error: e.message,
|
|
76
|
-
error_type: e.class.name,
|
|
77
|
-
architecture: 'pgmq',
|
|
78
|
-
processed_at: Time.now.utc.iso8601
|
|
79
|
-
}
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Initialize a new task with workflow steps
|
|
83
|
-
#
|
|
84
|
-
# In the pgmq architecture, this sends a task request message to the orchestration
|
|
85
|
-
# core monitored task_requests_queue, which will be processed by the Rust orchestrator
|
|
86
|
-
# to create the task record and enqueue initial steps.
|
|
87
|
-
#
|
|
88
|
-
# @param task_request [Hash] Task initialization data
|
|
89
|
-
# @return [void] No return value - operation is async via pgmq
|
|
90
|
-
def initialize_task(task_request)
|
|
91
|
-
logger.info '🚀 Initializing task with pgmq architecture'
|
|
92
|
-
|
|
93
|
-
task_request = TaskerCore::Types::TaskTypes::TaskRequest.from_hash(task_request)
|
|
94
|
-
|
|
95
|
-
# Prepare task request message for pgmq
|
|
96
|
-
task_request_message = {
|
|
97
|
-
message_type: 'task_request',
|
|
98
|
-
task_request: task_request.to_ffi_hash,
|
|
99
|
-
enqueued_at: Time.now.utc.iso8601,
|
|
100
|
-
message_id: SecureRandom.uuid
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
# Send message to task_requests_queue for orchestration core processing
|
|
104
|
-
begin
|
|
105
|
-
pgmq_client.send_message('task_requests_queue', task_request_message)
|
|
106
|
-
logger.info "✅ Task request sent to orchestration queue: #{task_request.namespace}/#{task_request.name}"
|
|
107
|
-
|
|
108
|
-
# Return void - this is now an async operation
|
|
109
|
-
nil
|
|
110
|
-
rescue StandardError => e
|
|
111
|
-
logger.error "❌ Failed to send task request to orchestration queue: #{e.message}"
|
|
112
|
-
raise TaskerCore::Errors::OrchestrationError, "Failed to send task request: #{e.message}"
|
|
113
|
-
end
|
|
114
|
-
rescue TaskerCore::Errors::ValidationError => e
|
|
115
|
-
logger.error "❌ Validation error initializing task: #{e.message}"
|
|
116
|
-
raise e
|
|
117
|
-
rescue StandardError => e
|
|
118
|
-
logger.error "❌ Unexpected error initializing task: #{e.class.name}: #{e.message}"
|
|
119
|
-
raise TaskerCore::Errors::OrchestrationError, "Task initialization failed: #{e.message}"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Check if the pgmq orchestration system is available and ready
|
|
123
|
-
# @return [Boolean] true if system is ready for task processing
|
|
124
|
-
def orchestration_ready?
|
|
125
|
-
orchestrator = TaskerCore.embedded_orchestrator
|
|
126
|
-
orchestrator.running?
|
|
127
|
-
rescue StandardError => e
|
|
128
|
-
logger.warn "⚠️ Failed to check orchestration status: #{e.message}"
|
|
129
|
-
false
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Get status information for this task handler
|
|
133
|
-
# @return [Hash] Status information including mode and pgmq connectivity
|
|
134
|
-
def status
|
|
135
|
-
mode = orchestration_mode
|
|
136
|
-
|
|
137
|
-
# Check pgmq availability without forcing connection
|
|
138
|
-
pgmq_available = begin
|
|
139
|
-
!pgmq_client.nil?
|
|
140
|
-
rescue TaskerCore::Errors::Error => e
|
|
141
|
-
logger.debug "🔍 PGMQ not available: #{e.message}"
|
|
142
|
-
false
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
status_info = {
|
|
146
|
-
handler_type: 'TaskHandler::Base',
|
|
147
|
-
architecture: 'pgmq',
|
|
148
|
-
orchestration_mode: mode,
|
|
149
|
-
orchestration_ready: orchestration_ready?,
|
|
150
|
-
pgmq_available: pgmq_available,
|
|
151
|
-
task_config_loaded: !task_config.empty?,
|
|
152
|
-
checked_at: Time.now.utc.iso8601
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
# Include embedded orchestrator status only in embedded mode
|
|
156
|
-
status_info[:embedded_orchestrator] = embedded_orchestrator_status if mode == 'embedded'
|
|
157
|
-
|
|
158
|
-
status_info
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
private
|
|
162
|
-
|
|
163
|
-
def load_task_config_from_path(path)
|
|
164
|
-
return {} unless path && File.exist?(path)
|
|
165
|
-
|
|
166
|
-
YAML.load_file(path)
|
|
167
|
-
rescue StandardError => e
|
|
168
|
-
logger.warn "Error loading task configuration: #{e.message}"
|
|
169
|
-
{}
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def embedded_orchestrator_status
|
|
173
|
-
orchestrator = TaskerCore.embedded_orchestrator
|
|
174
|
-
{
|
|
175
|
-
running: orchestrator.running?,
|
|
176
|
-
namespaces: orchestrator.namespaces,
|
|
177
|
-
started_at: orchestrator.started_at&.iso8601
|
|
178
|
-
}
|
|
179
|
-
rescue StandardError => e
|
|
180
|
-
logger.warn "⚠️ Failed to get embedded orchestrator status: #{e.message}"
|
|
181
|
-
{ running: false, error: e.message }
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Determine orchestration mode from configuration
|
|
185
|
-
# @return [String] 'embedded' or 'distributed'
|
|
186
|
-
def orchestration_mode
|
|
187
|
-
config = TaskerCore::Config.instance
|
|
188
|
-
mode = config.orchestration_config.mode
|
|
189
|
-
|
|
190
|
-
# Default to embedded mode if not specified or in test environment
|
|
191
|
-
if mode.nil?
|
|
192
|
-
if config.test_environment?
|
|
193
|
-
'embedded'
|
|
194
|
-
else
|
|
195
|
-
'distributed'
|
|
196
|
-
end
|
|
197
|
-
else
|
|
198
|
-
mode
|
|
199
|
-
end
|
|
200
|
-
rescue StandardError => e
|
|
201
|
-
logger.warn "⚠️ Failed to determine orchestration mode: #{e.message}, defaulting to distributed"
|
|
202
|
-
'distributed'
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Handle task processing in embedded mode using FFI orchestrator
|
|
206
|
-
# @param task_uuid [Integer] ID of the task to process
|
|
207
|
-
# @return [Hash] Result of step enqueueing operation
|
|
208
|
-
def handle_embedded_mode(task_uuid)
|
|
209
|
-
# Use embedded orchestrator to enqueue ready steps for the task
|
|
210
|
-
orchestrator = TaskerCore.embedded_orchestrator
|
|
211
|
-
|
|
212
|
-
unless orchestrator.running?
|
|
213
|
-
raise TaskerCore::Errors::OrchestrationError,
|
|
214
|
-
'Embedded orchestration system not running. Call TaskerCore.start_embedded_orchestration! first.'
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# Enqueue steps for the task - this will publish step messages to appropriate queues
|
|
218
|
-
result = orchestrator.enqueue_steps(task_uuid)
|
|
219
|
-
|
|
220
|
-
logger.info "✅ Task #{task_uuid} step enqueueing completed (embedded): #{result}"
|
|
221
|
-
|
|
222
|
-
{
|
|
223
|
-
success: true,
|
|
224
|
-
task_uuid: task_uuid,
|
|
225
|
-
message: result,
|
|
226
|
-
mode: 'embedded',
|
|
227
|
-
architecture: 'pgmq',
|
|
228
|
-
processed_at: Time.now.utc.iso8601
|
|
229
|
-
}
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Handle task processing in distributed mode using pure pgmq
|
|
233
|
-
# @param task_uuid [Integer] ID of the task to process
|
|
234
|
-
# @return [Hash] Result of step enqueueing operation
|
|
235
|
-
def handle_distributed_mode(task_uuid)
|
|
236
|
-
# In distributed mode, we don't directly enqueue steps via FFI
|
|
237
|
-
# Instead, we could publish a task processing request to a queue
|
|
238
|
-
# For now, return a message indicating distributed mode handling
|
|
239
|
-
|
|
240
|
-
logger.info "✅ Task #{task_uuid} queued for distributed processing"
|
|
241
|
-
|
|
242
|
-
{
|
|
243
|
-
success: true,
|
|
244
|
-
task_uuid: task_uuid,
|
|
245
|
-
message: 'Task queued for distributed orchestration processing',
|
|
246
|
-
mode: 'distributed',
|
|
247
|
-
architecture: 'pgmq',
|
|
248
|
-
processed_at: Time.now.utc.iso8601,
|
|
249
|
-
note: 'Phase 4.5 will complete distributed orchestration integration'
|
|
250
|
-
}
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
end
|