tasker-rb 0.1.3-arm64-darwin
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/README.md +55 -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/client.rb +165 -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.bundle +0 -0
- data/lib/tasker_core/template_discovery.rb +181 -0
- data/lib/tasker_core/test_environment.rb +313 -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 +161 -0
- metadata +292 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
module Worker
|
|
5
|
+
# Bootstrap orchestrator for Ruby worker
|
|
6
|
+
#
|
|
7
|
+
# Manages the complete initialization and lifecycle of the Ruby worker system,
|
|
8
|
+
# coordinating both Rust foundation layer and Ruby business logic components.
|
|
9
|
+
# This is the primary entry point for starting a TaskerCore worker process.
|
|
10
|
+
#
|
|
11
|
+
# The bootstrap process follows this sequence:
|
|
12
|
+
# 1. **Initialize Ruby Components**: EventBridge, HandlerRegistry, Subscribers
|
|
13
|
+
# 2. **Bootstrap Rust Foundation**: Start Rust worker via FFI
|
|
14
|
+
# 3. **Start Event Processing**: Begin EventPoller for step execution
|
|
15
|
+
# 4. **Register Shutdown Handlers**: Setup graceful termination on signals
|
|
16
|
+
#
|
|
17
|
+
# The Bootstrap class is a singleton, ensuring only one worker instance runs
|
|
18
|
+
# per process. It handles the complex coordination between Ruby and Rust,
|
|
19
|
+
# managing lifecycle transitions and providing health monitoring.
|
|
20
|
+
#
|
|
21
|
+
# @example Basic worker startup
|
|
22
|
+
# # Start with default configuration
|
|
23
|
+
# bootstrap = TaskerCore::Worker::Bootstrap.start!
|
|
24
|
+
# # => Returns bootstrap instance in :running status
|
|
25
|
+
#
|
|
26
|
+
# # Check if worker is running
|
|
27
|
+
# bootstrap.running?
|
|
28
|
+
# # => true
|
|
29
|
+
#
|
|
30
|
+
# @example Custom configuration
|
|
31
|
+
# bootstrap = TaskerCore::Worker::Bootstrap.start!(
|
|
32
|
+
# worker_id: "custom-worker-1",
|
|
33
|
+
# enable_web_api: false,
|
|
34
|
+
# event_driven_enabled: true,
|
|
35
|
+
# deployment_mode: "Hybrid", # PollingOnly, EventDrivenOnly, or Hybrid
|
|
36
|
+
# namespaces: ["payments", "fulfillment", "notifications"]
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
39
|
+
# @example Health checking
|
|
40
|
+
# status = TaskerCore::Worker::Bootstrap.instance.health_check
|
|
41
|
+
# # => {
|
|
42
|
+
# # healthy: true,
|
|
43
|
+
# # status: :running,
|
|
44
|
+
# # rust: { running: true, worker_core_status: "processing" },
|
|
45
|
+
# # ruby: {
|
|
46
|
+
# # status: :running,
|
|
47
|
+
# # event_bridge_active: true,
|
|
48
|
+
# # handlers_registered: 12,
|
|
49
|
+
# # subscriber_active: true,
|
|
50
|
+
# # event_poller_active: true
|
|
51
|
+
# # }
|
|
52
|
+
# # }
|
|
53
|
+
#
|
|
54
|
+
# @example Graceful shutdown
|
|
55
|
+
# # Shutdown cleanly, completing in-flight work
|
|
56
|
+
# TaskerCore::Worker::Bootstrap.instance.shutdown!
|
|
57
|
+
# # => Stops Rust worker, Ruby components, runs shutdown handlers
|
|
58
|
+
#
|
|
59
|
+
# @example Custom shutdown handlers
|
|
60
|
+
# bootstrap = TaskerCore::Worker::Bootstrap.instance
|
|
61
|
+
# bootstrap.on_shutdown do
|
|
62
|
+
# puts "Cleaning up resources..."
|
|
63
|
+
# cleanup_database_connections
|
|
64
|
+
# flush_metrics
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# @example Getting comprehensive status
|
|
68
|
+
# status = bootstrap.status
|
|
69
|
+
# # => {
|
|
70
|
+
# # rust: { running: true, ... },
|
|
71
|
+
# # ruby: {
|
|
72
|
+
# # status: :running,
|
|
73
|
+
# # handle_stored: true,
|
|
74
|
+
# # handle_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
75
|
+
# # worker_id: "ruby-worker-123",
|
|
76
|
+
# # event_bridge_active: true,
|
|
77
|
+
# # handler_registry_size: 12,
|
|
78
|
+
# # subscriber_active: true,
|
|
79
|
+
# # event_poller_active: true
|
|
80
|
+
# # }
|
|
81
|
+
# # }
|
|
82
|
+
#
|
|
83
|
+
# Deployment Modes:
|
|
84
|
+
# - **PollingOnly**: Traditional polling-based coordination (highest latency, most reliable)
|
|
85
|
+
# - **EventDrivenOnly**: Pure event-driven using PostgreSQL LISTEN/NOTIFY (lowest latency)
|
|
86
|
+
# - **Hybrid**: Event-driven with polling fallback (recommended for production)
|
|
87
|
+
#
|
|
88
|
+
# Worker States:
|
|
89
|
+
# - **:initialized**: Created but not started
|
|
90
|
+
# - **:running**: Fully operational and processing events
|
|
91
|
+
# - **:shutting_down**: Graceful shutdown in progress
|
|
92
|
+
# - **:stopped**: Completely shut down
|
|
93
|
+
#
|
|
94
|
+
# Signal Handling:
|
|
95
|
+
# The bootstrap automatically registers signal handlers for:
|
|
96
|
+
# - **SIGINT** (Ctrl+C): Graceful shutdown
|
|
97
|
+
# - **SIGTERM**: Graceful shutdown
|
|
98
|
+
# - **at_exit**: Cleanup on process exit
|
|
99
|
+
#
|
|
100
|
+
# @see TaskerCore::Worker::EventPoller For event polling details
|
|
101
|
+
# @see TaskerCore::EventBridge For event coordination
|
|
102
|
+
# @see TaskerCore::Registry::HandlerRegistry For handler management
|
|
103
|
+
# @see TaskerCore::FFI For Rust FFI operations
|
|
104
|
+
class Bootstrap
|
|
105
|
+
include Singleton
|
|
106
|
+
|
|
107
|
+
attr_reader :logger, :config, :rust_handle
|
|
108
|
+
|
|
109
|
+
def initialize
|
|
110
|
+
@logger = TaskerCore::Logger.instance
|
|
111
|
+
@status = :initialized
|
|
112
|
+
@shutdown_handlers = []
|
|
113
|
+
@rust_handle = nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Start the worker system with optional configuration
|
|
117
|
+
def self.start!
|
|
118
|
+
instance.start!
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Main boots
|
|
122
|
+
def start!
|
|
123
|
+
logger.info 'Starting Ruby worker bootstrap'
|
|
124
|
+
|
|
125
|
+
# Initialize Ruby components first
|
|
126
|
+
initialize_ruby_components!
|
|
127
|
+
|
|
128
|
+
# Bootstrap Rust foundation via FFI
|
|
129
|
+
bootstrap_rust_foundation!
|
|
130
|
+
|
|
131
|
+
# Start event processing
|
|
132
|
+
start_event_processing!
|
|
133
|
+
|
|
134
|
+
# Register shutdown handlers
|
|
135
|
+
register_shutdown_handlers!
|
|
136
|
+
|
|
137
|
+
@status = :running
|
|
138
|
+
logger.info 'Ruby worker system started successfully'
|
|
139
|
+
|
|
140
|
+
self
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
logger.error "Failed to start worker: #{e.message}"
|
|
143
|
+
logger.error e.backtrace.join("\n")
|
|
144
|
+
shutdown!
|
|
145
|
+
raise
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if worker is running
|
|
149
|
+
def running?
|
|
150
|
+
@status == :running && rust_worker_running?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Check if Rust handle is valid and running
|
|
154
|
+
def rust_handle_running?
|
|
155
|
+
return false unless @rust_handle.is_a?(Hash) &&
|
|
156
|
+
(@rust_handle[:handle_id] || @rust_handle['handle_id'])
|
|
157
|
+
|
|
158
|
+
rust_worker_running?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get comprehensive status
|
|
162
|
+
def status
|
|
163
|
+
rust_status = TaskerCore::FFI.worker_status
|
|
164
|
+
{
|
|
165
|
+
rust: rust_status,
|
|
166
|
+
ruby: {
|
|
167
|
+
status: @status,
|
|
168
|
+
handle_stored: !@rust_handle.nil?,
|
|
169
|
+
handle_id: @rust_handle&.dig('handle_id') || @rust_handle&.dig(:handle_id),
|
|
170
|
+
worker_id: @rust_handle&.dig('worker_id') || @rust_handle&.dig(:worker_id),
|
|
171
|
+
event_bridge_active: EventBridge.instance.active?,
|
|
172
|
+
handler_registry_size: Registry::HandlerRegistry.instance.handlers.size,
|
|
173
|
+
subscriber_active: @step_subscriber&.active? || false,
|
|
174
|
+
event_poller_active: EventPoller.instance.active?
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
logger.error "Failed to get status: #{e.message}"
|
|
179
|
+
{ error: e.message, status: @status }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Execute a block with the Rust handle, bootstrapping if necessary
|
|
183
|
+
def with_rust_handle(&block)
|
|
184
|
+
bootstrap_rust_foundation! unless rust_handle_running?
|
|
185
|
+
block.call
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Graceful shutdown
|
|
189
|
+
def shutdown!
|
|
190
|
+
return if @status == :stopped
|
|
191
|
+
|
|
192
|
+
logger.info 'Initiating graceful shutdown'
|
|
193
|
+
@status = :shutting_down
|
|
194
|
+
|
|
195
|
+
# Transition Rust to graceful shutdown first
|
|
196
|
+
if @rust_handle
|
|
197
|
+
begin
|
|
198
|
+
TaskerCore::FFI.transition_to_graceful_shutdown
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
logger.error "Failed to transition to graceful shutdown: #{e.message}"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Stop Ruby components
|
|
205
|
+
@step_subscriber&.stop!
|
|
206
|
+
EventBridge.instance.stop!
|
|
207
|
+
EventPoller.instance.stop!
|
|
208
|
+
|
|
209
|
+
# TAS-65: Stop domain event components
|
|
210
|
+
DomainEvents::SubscriberRegistry.instance.stop_all!
|
|
211
|
+
InProcessDomainEventPoller.instance.stop!
|
|
212
|
+
|
|
213
|
+
# Stop Rust worker and clear handle
|
|
214
|
+
if @rust_handle
|
|
215
|
+
begin
|
|
216
|
+
TaskerCore::FFI.stop_worker
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
logger.error "Failed to stop Rust worker: #{e.message}"
|
|
219
|
+
ensure
|
|
220
|
+
@rust_handle = nil
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Run custom shutdown handlers
|
|
225
|
+
@shutdown_handlers.each do |handler|
|
|
226
|
+
handler.call
|
|
227
|
+
rescue StandardError
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
@status = :stopped
|
|
232
|
+
logger.info 'Worker shutdown complete'
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Register custom shutdown handler
|
|
236
|
+
def on_shutdown(&block)
|
|
237
|
+
@shutdown_handlers << block if block_given?
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Perform health check on both Ruby and Rust components
|
|
241
|
+
def health_check
|
|
242
|
+
return { healthy: false, status: @status, error: 'not_running' } unless running?
|
|
243
|
+
|
|
244
|
+
begin
|
|
245
|
+
# Get Rust worker status
|
|
246
|
+
rust_status = TaskerCore::FFI.worker_status
|
|
247
|
+
rust_running = rust_status['running'] || rust_status[:running]
|
|
248
|
+
|
|
249
|
+
# Check Ruby components
|
|
250
|
+
ruby_healthy = @status == :running &&
|
|
251
|
+
EventBridge.instance.active? &&
|
|
252
|
+
EventPoller.instance.active? &&
|
|
253
|
+
Registry::HandlerRegistry.instance.handlers.any?
|
|
254
|
+
|
|
255
|
+
overall_healthy = rust_running && ruby_healthy
|
|
256
|
+
|
|
257
|
+
{
|
|
258
|
+
healthy: overall_healthy,
|
|
259
|
+
status: @status,
|
|
260
|
+
rust: {
|
|
261
|
+
running: rust_running,
|
|
262
|
+
worker_core_status: rust_status['worker_core_status'] || rust_status[:worker_core_status]
|
|
263
|
+
},
|
|
264
|
+
ruby: {
|
|
265
|
+
status: @status,
|
|
266
|
+
event_bridge_active: EventBridge.instance.active?,
|
|
267
|
+
handlers_registered: Registry::HandlerRegistry.instance.handlers.size,
|
|
268
|
+
subscriber_active: @step_subscriber&.active? || false,
|
|
269
|
+
event_poller_active: EventPoller.instance.active?,
|
|
270
|
+
# TAS-65: Domain event system status
|
|
271
|
+
domain_event_poller_active: InProcessDomainEventPoller.instance.active?,
|
|
272
|
+
domain_event_subscribers: DomainEvents::SubscriberRegistry.instance.stats
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
rescue StandardError => e
|
|
276
|
+
logger.error "Health check failed: #{e.message}"
|
|
277
|
+
{
|
|
278
|
+
healthy: false,
|
|
279
|
+
status: @status,
|
|
280
|
+
error: e.message
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def detect_namespaces
|
|
288
|
+
# Auto-detect from registered handlers
|
|
289
|
+
Registry::HandlerRegistry.instance.registered_handlers.map do |handler_class|
|
|
290
|
+
handler_class.namespace if handler_class.respond_to?(:namespace)
|
|
291
|
+
end.compact.uniq
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def initialize_ruby_components!
|
|
295
|
+
logger.info 'Initializing Ruby components...'
|
|
296
|
+
|
|
297
|
+
# Initialize event bridge
|
|
298
|
+
EventBridge.instance
|
|
299
|
+
|
|
300
|
+
# Initialize handler registry (bootstrap happens automatically)
|
|
301
|
+
Registry::HandlerRegistry.instance
|
|
302
|
+
|
|
303
|
+
# Initialize step execution subscriber
|
|
304
|
+
@step_subscriber = StepExecutionSubscriber.new
|
|
305
|
+
|
|
306
|
+
logger.info 'Ruby components initialized'
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def bootstrap_rust_foundation!
|
|
310
|
+
# Check if we already have a running handle
|
|
311
|
+
if rust_handle_running?
|
|
312
|
+
logger.debug 'Rust worker foundation already running, reusing handle'
|
|
313
|
+
return @rust_handle
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
logger.info 'Bootstrapping Rust worker foundation...'
|
|
317
|
+
|
|
318
|
+
# Bootstrap the worker and store the handle result
|
|
319
|
+
result = TaskerCore::FFI.bootstrap_worker
|
|
320
|
+
logger.info "Rust bootstrap result: #{result.inspect}"
|
|
321
|
+
|
|
322
|
+
# Check if it was already running or newly started
|
|
323
|
+
# Handle both string and symbol keys from Rust FFI
|
|
324
|
+
status = result['status'] || result[:status]
|
|
325
|
+
worker_id = result['worker_id'] || result[:worker_id]
|
|
326
|
+
|
|
327
|
+
if status == 'already_running'
|
|
328
|
+
logger.info 'Worker system was already running, reusing existing handle'
|
|
329
|
+
elsif status == 'started'
|
|
330
|
+
logger.info "New worker system started with ID: #{worker_id}"
|
|
331
|
+
else
|
|
332
|
+
raise "Unexpected bootstrap status: #{status}"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Store the handle information
|
|
336
|
+
@rust_handle = result
|
|
337
|
+
|
|
338
|
+
# Verify it's running
|
|
339
|
+
status = TaskerCore::FFI.worker_status
|
|
340
|
+
# Handle both string and symbol keys from Rust FFI
|
|
341
|
+
running = status['running'] || status[:running]
|
|
342
|
+
unless running
|
|
343
|
+
@rust_handle = nil
|
|
344
|
+
raise "Rust worker failed to start: #{status.inspect}"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
handle_id = @rust_handle['handle_id'] || @rust_handle[:handle_id]
|
|
348
|
+
logger.info "Rust foundation bootstrapped with handle: #{handle_id}"
|
|
349
|
+
@rust_handle
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def start_event_processing!
|
|
353
|
+
logger.info 'Starting event processing...'
|
|
354
|
+
|
|
355
|
+
# Start the EventPoller to poll for step execution events from Rust
|
|
356
|
+
EventPoller.instance.start!
|
|
357
|
+
|
|
358
|
+
# NOTE: StepExecutionSubscriber already subscribes to step execution events
|
|
359
|
+
# in its initializer, so we don't need to subscribe again here.
|
|
360
|
+
# Duplicate subscriptions cause the same event to be processed twice,
|
|
361
|
+
# leading to double state transitions.
|
|
362
|
+
|
|
363
|
+
# TAS-65: Start in-process domain event poller for fast events
|
|
364
|
+
InProcessDomainEventPoller.instance.start!
|
|
365
|
+
|
|
366
|
+
# TAS-65: Start domain event subscribers
|
|
367
|
+
DomainEvents::SubscriberRegistry.instance.start_all!
|
|
368
|
+
|
|
369
|
+
logger.info 'Event processing started (step events + domain events)'
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def register_shutdown_handlers!
|
|
373
|
+
# Graceful shutdown on signals
|
|
374
|
+
%w[INT TERM].each do |signal|
|
|
375
|
+
Signal.trap(signal) do
|
|
376
|
+
Thread.new { shutdown! }
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Shutdown on exit
|
|
381
|
+
at_exit { shutdown! if running? }
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def rust_worker_running?
|
|
385
|
+
status = TaskerCore::FFI.worker_status
|
|
386
|
+
# Handle both string and symbol keys from Rust FFI
|
|
387
|
+
running = status['running'] || status[:running]
|
|
388
|
+
running == true
|
|
389
|
+
rescue StandardError
|
|
390
|
+
false
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
# High-level client wrapper around the TaskerCore FFI client methods.
|
|
5
|
+
#
|
|
6
|
+
# The raw FFI exposes `TaskerCore::FFI.client_create_task(hash)` and similar
|
|
7
|
+
# methods that require callers to construct complete request hashes with all
|
|
8
|
+
# required fields (initiator, source_system, reason, etc.) and return plain
|
|
9
|
+
# hashes. This module provides keyword-argument methods with sensible defaults
|
|
10
|
+
# and wraps responses into typed `ClientTypes::*` Dry::Struct objects.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating a task with defaults
|
|
13
|
+
# response = TaskerCore::Client.create_task(
|
|
14
|
+
# name: 'process_order',
|
|
15
|
+
# namespace: 'ecommerce',
|
|
16
|
+
# context: { order_id: 123 }
|
|
17
|
+
# )
|
|
18
|
+
# response.task_uuid # => "550e8400-..."
|
|
19
|
+
# response.status # => "pending"
|
|
20
|
+
#
|
|
21
|
+
# @example Getting a task
|
|
22
|
+
# task = TaskerCore::Client.get_task("550e8400-...")
|
|
23
|
+
# task.name # => "process_order"
|
|
24
|
+
# task.namespace # => "ecommerce"
|
|
25
|
+
#
|
|
26
|
+
# @example Listing tasks with filters
|
|
27
|
+
# list = TaskerCore::Client.list_tasks(namespace: 'ecommerce', limit: 10)
|
|
28
|
+
# list.tasks.size # => 3
|
|
29
|
+
# list.pagination # => { "total_count" => 42, ... }
|
|
30
|
+
module Client
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# Create a task via the orchestration API.
|
|
34
|
+
#
|
|
35
|
+
# @param name [String] Named task template name
|
|
36
|
+
# @param namespace [String] Task namespace (default: "default")
|
|
37
|
+
# @param context [Hash] Workflow context passed to step handlers
|
|
38
|
+
# @param version [String] Template version (default: "1.0.0")
|
|
39
|
+
# @param initiator [String] Who initiated the request
|
|
40
|
+
# @param source_system [String] Originating system
|
|
41
|
+
# @param reason [String] Reason for creating the task
|
|
42
|
+
# @param options [Hash] Additional TaskRequest fields
|
|
43
|
+
# @return [ClientTypes::TaskResponse, Hash] Typed response or raw hash on schema mismatch
|
|
44
|
+
def create_task(name:, namespace: 'default', context: {}, version: '1.0.0',
|
|
45
|
+
initiator: 'tasker-core-ruby', source_system: 'tasker-core',
|
|
46
|
+
reason: 'Task requested', **options)
|
|
47
|
+
request = {
|
|
48
|
+
'name' => name,
|
|
49
|
+
'namespace' => namespace,
|
|
50
|
+
'version' => version,
|
|
51
|
+
'context' => deep_to_hash(context),
|
|
52
|
+
'initiator' => initiator,
|
|
53
|
+
'source_system' => source_system,
|
|
54
|
+
'reason' => reason
|
|
55
|
+
}
|
|
56
|
+
options.each { |k, v| request[k.to_s] = v }
|
|
57
|
+
|
|
58
|
+
response = TaskerCore::FFI.client_create_task(request)
|
|
59
|
+
wrap_response(response, Types::ClientTaskResponse)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get a task by UUID.
|
|
63
|
+
#
|
|
64
|
+
# @param task_uuid [String] The task UUID
|
|
65
|
+
# @return [ClientTypes::TaskResponse, Hash] Typed response or raw hash
|
|
66
|
+
def get_task(task_uuid)
|
|
67
|
+
response = TaskerCore::FFI.client_get_task(task_uuid.to_s)
|
|
68
|
+
wrap_response(response, Types::ClientTaskResponse)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# List tasks with optional filtering and pagination.
|
|
72
|
+
#
|
|
73
|
+
# @param limit [Integer] Maximum number of results (default: 50)
|
|
74
|
+
# @param offset [Integer] Pagination offset (default: 0)
|
|
75
|
+
# @param namespace [String, nil] Filter by namespace
|
|
76
|
+
# @param status [String, nil] Filter by status
|
|
77
|
+
# @return [ClientTypes::TaskListResponse, Hash] Typed response or raw hash
|
|
78
|
+
def list_tasks(limit: 50, offset: 0, namespace: nil, status: nil)
|
|
79
|
+
response = TaskerCore::FFI.client_list_tasks(limit, offset, namespace, status)
|
|
80
|
+
wrap_response(response, Types::ClientTaskListResponse)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Cancel a task by UUID.
|
|
84
|
+
#
|
|
85
|
+
# @param task_uuid [String] The task UUID
|
|
86
|
+
# @return [Hash] Cancellation result
|
|
87
|
+
def cancel_task(task_uuid)
|
|
88
|
+
TaskerCore::FFI.client_cancel_task(task_uuid.to_s)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# List workflow steps for a task.
|
|
92
|
+
#
|
|
93
|
+
# @param task_uuid [String] The task UUID
|
|
94
|
+
# @return [Array<ClientTypes::StepResponse>, Array<Hash>] Typed steps or raw hashes
|
|
95
|
+
def list_task_steps(task_uuid)
|
|
96
|
+
response = TaskerCore::FFI.client_list_task_steps(task_uuid.to_s)
|
|
97
|
+
return response unless response.is_a?(Array)
|
|
98
|
+
|
|
99
|
+
response.map { |step| wrap_response(step, Types::ClientStepResponse) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get a specific workflow step.
|
|
103
|
+
#
|
|
104
|
+
# @param task_uuid [String] The task UUID
|
|
105
|
+
# @param step_uuid [String] The step UUID
|
|
106
|
+
# @return [ClientTypes::StepResponse, Hash] Typed response or raw hash
|
|
107
|
+
def get_step(task_uuid, step_uuid)
|
|
108
|
+
response = TaskerCore::FFI.client_get_step(task_uuid.to_s, step_uuid.to_s)
|
|
109
|
+
wrap_response(response, Types::ClientStepResponse)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get audit history for a workflow step.
|
|
113
|
+
#
|
|
114
|
+
# @param task_uuid [String] The task UUID
|
|
115
|
+
# @param step_uuid [String] The step UUID
|
|
116
|
+
# @return [Array<ClientTypes::StepAuditResponse>, Array<Hash>] Typed audit entries or raw hashes
|
|
117
|
+
def get_step_audit_history(task_uuid, step_uuid)
|
|
118
|
+
response = TaskerCore::FFI.client_get_step_audit_history(task_uuid.to_s, step_uuid.to_s)
|
|
119
|
+
return response unless response.is_a?(Array)
|
|
120
|
+
|
|
121
|
+
response.map { |entry| wrap_response(entry, Types::ClientStepAuditResponse) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check orchestration API health.
|
|
125
|
+
#
|
|
126
|
+
# @return [ClientTypes::HealthResponse, Hash] Typed response or raw hash
|
|
127
|
+
def health_check
|
|
128
|
+
response = TaskerCore::FFI.client_health_check
|
|
129
|
+
wrap_response(response, Types::ClientHealthResponse)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @api private
|
|
133
|
+
# Wrap a raw FFI hash response into a Dry::Struct type.
|
|
134
|
+
# Falls back to the raw hash if the schema doesn't match,
|
|
135
|
+
# providing forward-compatibility when the API adds new fields.
|
|
136
|
+
def wrap_response(raw, type_class)
|
|
137
|
+
return raw unless raw.is_a?(Hash)
|
|
138
|
+
|
|
139
|
+
# Dry::Struct requires symbolized keys; raw FFI returns string keys
|
|
140
|
+
type_class.new(raw.transform_keys(&:to_sym))
|
|
141
|
+
rescue Dry::Struct::Error, KeyError, Dry::Types::ConstraintError
|
|
142
|
+
raw
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @api private
|
|
146
|
+
# Recursively convert ActionController::Parameters (and similar) to plain hashes/arrays.
|
|
147
|
+
def deep_to_hash(obj)
|
|
148
|
+
case obj
|
|
149
|
+
when Hash
|
|
150
|
+
obj.transform_values { |v| deep_to_hash(v) }
|
|
151
|
+
when Array
|
|
152
|
+
obj.map { |v| deep_to_hash(v) }
|
|
153
|
+
else
|
|
154
|
+
# Handle ActionController::Parameters if available (Rails contexts)
|
|
155
|
+
if obj.respond_to?(:to_unsafe_h)
|
|
156
|
+
obj.to_unsafe_h.transform_values { |v| deep_to_hash(v) }
|
|
157
|
+
else
|
|
158
|
+
obj
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private_class_method :wrap_response, :deep_to_hash
|
|
164
|
+
end
|
|
165
|
+
end
|