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,254 @@
|
|
|
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
|
|
Binary file
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module TaskerCore
|
|
6
|
+
module TemplateDiscovery
|
|
7
|
+
# Template path discovery following the same patterns as Rust TaskTemplateManager
|
|
8
|
+
# Finds task template configuration directory using environment variables and workspace detection
|
|
9
|
+
class TemplatePath
|
|
10
|
+
class << self
|
|
11
|
+
# Find the template configuration directory using the same logic as Rust
|
|
12
|
+
# Returns path to directory containing .yaml template files
|
|
13
|
+
def find_template_config_directory
|
|
14
|
+
# First try TASKER_TEMPLATE_PATH environment variable
|
|
15
|
+
return ENV['TASKER_TEMPLATE_PATH'] if ENV['TASKER_TEMPLATE_PATH'] && Dir.exist?(ENV['TASKER_TEMPLATE_PATH'])
|
|
16
|
+
|
|
17
|
+
# Try WORKSPACE_PATH environment variable (for compatibility)
|
|
18
|
+
if ENV['WORKSPACE_PATH']
|
|
19
|
+
template_dir = File.join(ENV['WORKSPACE_PATH'], 'config', 'tasks')
|
|
20
|
+
return template_dir if Dir.exist?(template_dir)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Try workspace detection from current directory
|
|
24
|
+
workspace_root = find_workspace_root
|
|
25
|
+
if workspace_root
|
|
26
|
+
template_dir = File.join(workspace_root, 'config', 'tasks')
|
|
27
|
+
return template_dir if Dir.exist?(template_dir)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Default fallback to current directory
|
|
31
|
+
default_dir = 'config/tasks'
|
|
32
|
+
return default_dir if Dir.exist?(default_dir)
|
|
33
|
+
|
|
34
|
+
# If nothing found, return nil
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Discover all YAML template files in the template directory
|
|
39
|
+
def discover_template_files(template_dir = nil)
|
|
40
|
+
template_dir ||= find_template_config_directory
|
|
41
|
+
return [] unless template_dir
|
|
42
|
+
|
|
43
|
+
# Look for both .yaml and .yml file extensions
|
|
44
|
+
yaml_files = Dir.glob(File.join(template_dir, '**', '*.yaml'))
|
|
45
|
+
yml_files = Dir.glob(File.join(template_dir, '**', '*.yml'))
|
|
46
|
+
(yaml_files + yml_files).sort.uniq
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Find workspace root by looking for Cargo.toml or other workspace indicators
|
|
52
|
+
def find_workspace_root(start_dir = Dir.pwd)
|
|
53
|
+
current_dir = File.expand_path(start_dir)
|
|
54
|
+
|
|
55
|
+
loop do
|
|
56
|
+
# Look for workspace indicators
|
|
57
|
+
return current_dir if workspace_indicators.any? do |indicator|
|
|
58
|
+
File.exist?(File.join(current_dir, indicator))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
parent_dir = File.dirname(current_dir)
|
|
62
|
+
break if parent_dir == current_dir # Reached filesystem root
|
|
63
|
+
|
|
64
|
+
current_dir = parent_dir
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def workspace_indicators
|
|
71
|
+
['Cargo.toml', '.git', 'Gemfile', 'package.json', 'tasker-core.code-workspace']
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# YAML template parser that extracts handler information
|
|
77
|
+
class TemplateParser
|
|
78
|
+
class << self
|
|
79
|
+
# Parse a YAML template file and extract handler callables
|
|
80
|
+
# Returns array of handler class names found in the template
|
|
81
|
+
def extract_handler_callables(template_file)
|
|
82
|
+
return [] unless File.exist?(template_file)
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
template_data = YAML.load_file(template_file)
|
|
86
|
+
extract_handlers_from_template(template_data)
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
TaskerCore::Logger.instance.warn("Failed to parse template #{template_file}: #{e.message}")
|
|
89
|
+
[]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Extract handler callables from parsed template data
|
|
94
|
+
def extract_handlers_from_template(template_data)
|
|
95
|
+
handlers = []
|
|
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
|
+
# Add step handlers
|
|
103
|
+
if template_data['steps'].is_a?(Array)
|
|
104
|
+
template_data['steps'].each do |step|
|
|
105
|
+
handlers << step['handler']['callable'] if step['handler'] && step['handler']['callable']
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
handlers.uniq
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get template metadata for debugging/introspection
|
|
113
|
+
def extract_template_metadata(template_file)
|
|
114
|
+
return nil unless File.exist?(template_file)
|
|
115
|
+
|
|
116
|
+
begin
|
|
117
|
+
template_data = YAML.load_file(template_file)
|
|
118
|
+
{
|
|
119
|
+
name: template_data['name'],
|
|
120
|
+
namespace_name: template_data['namespace_name'],
|
|
121
|
+
version: template_data['version'],
|
|
122
|
+
description: template_data['description'],
|
|
123
|
+
file_path: template_file,
|
|
124
|
+
handlers: extract_handlers_from_template(template_data)
|
|
125
|
+
}
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
TaskerCore::Logger.instance.warn("Failed to extract metadata from #{template_file}: #{e.message}")
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Main discovery coordinator that combines path and parsing logic
|
|
135
|
+
class HandlerDiscovery
|
|
136
|
+
class << self
|
|
137
|
+
# Discover all handlers from templates in the configured template directory
|
|
138
|
+
# Returns array of unique handler class names
|
|
139
|
+
def discover_all_handlers(template_dir = nil)
|
|
140
|
+
template_files = TemplatePath.discover_template_files(template_dir)
|
|
141
|
+
|
|
142
|
+
handlers = template_files.flat_map do |file|
|
|
143
|
+
TemplateParser.extract_handler_callables(file)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
handlers.uniq.sort
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get detailed information about discovered templates
|
|
150
|
+
# Returns array of template metadata hashes
|
|
151
|
+
def discover_template_metadata(template_dir = nil)
|
|
152
|
+
template_files = TemplatePath.discover_template_files(template_dir)
|
|
153
|
+
|
|
154
|
+
template_files.filter_map do |file|
|
|
155
|
+
TemplateParser.extract_template_metadata(file)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Get handlers grouped by namespace
|
|
160
|
+
# Returns hash of namespace => [handler_names]
|
|
161
|
+
def discover_handlers_by_namespace(template_dir = nil)
|
|
162
|
+
metadata = discover_template_metadata(template_dir)
|
|
163
|
+
|
|
164
|
+
handlers_by_namespace = {}
|
|
165
|
+
metadata.each do |meta|
|
|
166
|
+
namespace = meta[:namespace_name] || 'default'
|
|
167
|
+
handlers_by_namespace[namespace] ||= []
|
|
168
|
+
handlers_by_namespace[namespace].concat(meta[:handlers])
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Remove duplicates and sort
|
|
172
|
+
handlers_by_namespace.each do |namespace, handlers|
|
|
173
|
+
handlers_by_namespace[namespace] = handlers.uniq.sort
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
handlers_by_namespace
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
|
|
5
|
+
module TaskerCore
|
|
6
|
+
# Unified structured logging via Rust tracing FFI
|
|
7
|
+
#
|
|
8
|
+
# TAS-29 Phase 6: Replace Ruby's Logger with FFI calls to Rust's tracing
|
|
9
|
+
# infrastructure, enabling unified structured logging across Ruby and Rust.
|
|
10
|
+
#
|
|
11
|
+
# ## Architecture
|
|
12
|
+
#
|
|
13
|
+
# Ruby Handler → Tracing.info() → FFI Bridge → Rust tracing → OpenTelemetry
|
|
14
|
+
#
|
|
15
|
+
# ## Usage
|
|
16
|
+
#
|
|
17
|
+
# # Simple message
|
|
18
|
+
# Tracing.info("Task initialized")
|
|
19
|
+
#
|
|
20
|
+
# # With structured fields
|
|
21
|
+
# Tracing.info("Step completed", {
|
|
22
|
+
# correlation_id: correlation_id,
|
|
23
|
+
# task_uuid: task.uuid,
|
|
24
|
+
# step_uuid: step.uuid,
|
|
25
|
+
# namespace: "order_fulfillment",
|
|
26
|
+
# operation: "validate_inventory",
|
|
27
|
+
# duration_ms: elapsed_ms
|
|
28
|
+
# })
|
|
29
|
+
#
|
|
30
|
+
# # Error logging
|
|
31
|
+
# Tracing.error("Payment processing failed", {
|
|
32
|
+
# correlation_id: correlation_id,
|
|
33
|
+
# error_class: error.class.name,
|
|
34
|
+
# error_message: error.message
|
|
35
|
+
# })
|
|
36
|
+
#
|
|
37
|
+
# ## Structured Field Conventions (TAS-29 Phase 6.2)
|
|
38
|
+
#
|
|
39
|
+
# **Required fields** (when applicable):
|
|
40
|
+
# - correlation_id: Always include for distributed tracing
|
|
41
|
+
# - task_uuid: Include for task-level operations
|
|
42
|
+
# - step_uuid: Include for step-level operations
|
|
43
|
+
# - namespace: Include for namespace-specific operations
|
|
44
|
+
# - operation: Operation identifier (e.g., "validate_inventory")
|
|
45
|
+
#
|
|
46
|
+
# **Optional fields**:
|
|
47
|
+
# - duration_ms: For timed operations
|
|
48
|
+
# - error_class, error_message: For error context
|
|
49
|
+
# - entity_id: For domain entity operations
|
|
50
|
+
# - retry_count: For retryable operations
|
|
51
|
+
#
|
|
52
|
+
# ## Log Level Guidelines (TAS-29 Phase 6.1)
|
|
53
|
+
#
|
|
54
|
+
# - ERROR: Unrecoverable failures requiring intervention
|
|
55
|
+
# - WARN: Degraded operation, retryable failures
|
|
56
|
+
# - INFO: Lifecycle events, state transitions
|
|
57
|
+
# - DEBUG: Detailed diagnostic information
|
|
58
|
+
# - TRACE: Very verbose, hot-path entry/exit
|
|
59
|
+
#
|
|
60
|
+
class Tracing
|
|
61
|
+
include Singleton
|
|
62
|
+
|
|
63
|
+
# Log ERROR level message with structured fields
|
|
64
|
+
#
|
|
65
|
+
# @param message [String] Log message
|
|
66
|
+
# @param fields [Hash] Structured fields (correlation_id, task_uuid, etc.)
|
|
67
|
+
# @return [void]
|
|
68
|
+
def self.error(message, fields = {})
|
|
69
|
+
fields_hash = normalize_fields(fields)
|
|
70
|
+
TaskerCore::FFI.log_error(message.to_s, fields_hash)
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
# Fallback to stderr if FFI logging fails
|
|
73
|
+
warn "FFI logging failed: #{e.message}"
|
|
74
|
+
warn "#{message} | #{fields.inspect}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Log WARN level message with structured fields
|
|
78
|
+
#
|
|
79
|
+
# @param message [String] Log message
|
|
80
|
+
# @param fields [Hash] Structured fields
|
|
81
|
+
# @return [void]
|
|
82
|
+
def self.warn(message, fields = {})
|
|
83
|
+
fields_hash = normalize_fields(fields)
|
|
84
|
+
TaskerCore::FFI.log_warn(message.to_s, fields_hash)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
warn "FFI logging failed: #{e.message}"
|
|
87
|
+
warn "#{message} | #{fields.inspect}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Log INFO level message with structured fields
|
|
91
|
+
#
|
|
92
|
+
# @param message [String] Log message
|
|
93
|
+
# @param fields [Hash] Structured fields
|
|
94
|
+
# @return [void]
|
|
95
|
+
def self.info(message, fields = {})
|
|
96
|
+
fields_hash = normalize_fields(fields)
|
|
97
|
+
TaskerCore::FFI.log_info(message.to_s, fields_hash)
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
warn "FFI logging failed: #{e.message}"
|
|
100
|
+
warn "#{message} | #{fields.inspect}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Log DEBUG level message with structured fields
|
|
104
|
+
#
|
|
105
|
+
# @param message [String] Log message
|
|
106
|
+
# @param fields [Hash] Structured fields
|
|
107
|
+
# @return [void]
|
|
108
|
+
def self.debug(message, fields = {})
|
|
109
|
+
fields_hash = normalize_fields(fields)
|
|
110
|
+
TaskerCore::FFI.log_debug(message.to_s, fields_hash)
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
warn "FFI logging failed: #{e.message}"
|
|
113
|
+
warn "#{message} | #{fields.inspect}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Log TRACE level message with structured fields
|
|
117
|
+
#
|
|
118
|
+
# @param message [String] Log message
|
|
119
|
+
# @param fields [Hash] Structured fields
|
|
120
|
+
# @return [void]
|
|
121
|
+
def self.trace(message, fields = {})
|
|
122
|
+
fields_hash = normalize_fields(fields)
|
|
123
|
+
TaskerCore::FFI.log_trace(message.to_s, fields_hash)
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
warn "FFI logging failed: #{e.message}"
|
|
126
|
+
warn "#{message} | #{fields.inspect}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Normalize fields to string keys and values for FFI
|
|
130
|
+
#
|
|
131
|
+
# @param fields [Hash] Input fields
|
|
132
|
+
# @return [Hash<String, String>] Normalized fields
|
|
133
|
+
# @api private
|
|
134
|
+
def self.normalize_fields(fields)
|
|
135
|
+
return {} if fields.nil? || fields.empty?
|
|
136
|
+
|
|
137
|
+
fields.transform_keys(&:to_s).transform_values { |v| normalize_value(v) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Normalize a value to string for FFI
|
|
141
|
+
#
|
|
142
|
+
# @param value [Object] Value to normalize
|
|
143
|
+
# @return [String] String representation
|
|
144
|
+
# @api private
|
|
145
|
+
def self.normalize_value(value)
|
|
146
|
+
case value
|
|
147
|
+
when nil
|
|
148
|
+
'nil'
|
|
149
|
+
when String
|
|
150
|
+
value
|
|
151
|
+
when Symbol
|
|
152
|
+
value.to_s
|
|
153
|
+
when Numeric, TrueClass, FalseClass
|
|
154
|
+
value.to_s
|
|
155
|
+
when Exception
|
|
156
|
+
"#{value.class}: #{value.message}"
|
|
157
|
+
else
|
|
158
|
+
value.to_s
|
|
159
|
+
end
|
|
160
|
+
rescue StandardError
|
|
161
|
+
'<serialization_error>'
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private_class_method :normalize_fields, :normalize_value
|
|
165
|
+
end
|
|
166
|
+
end
|