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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/DEVELOPMENT.md +548 -0
  3. data/README.md +87 -0
  4. data/ext/tasker_core/Cargo.lock +4720 -0
  5. data/ext/tasker_core/Cargo.toml +76 -0
  6. data/ext/tasker_core/extconf.rb +38 -0
  7. data/ext/tasker_core/src/CLAUDE.md +7 -0
  8. data/ext/tasker_core/src/bootstrap.rs +320 -0
  9. data/ext/tasker_core/src/bridge.rs +400 -0
  10. data/ext/tasker_core/src/client_ffi.rs +173 -0
  11. data/ext/tasker_core/src/conversions.rs +131 -0
  12. data/ext/tasker_core/src/diagnostics.rs +57 -0
  13. data/ext/tasker_core/src/event_handler.rs +179 -0
  14. data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
  15. data/ext/tasker_core/src/ffi_logging.rs +245 -0
  16. data/ext/tasker_core/src/global_event_system.rs +16 -0
  17. data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
  18. data/ext/tasker_core/src/lib.rs +41 -0
  19. data/ext/tasker_core/src/observability_ffi.rs +339 -0
  20. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  21. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  22. data/lib/tasker_core/bootstrap.rb +394 -0
  23. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  24. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  25. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  26. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  27. data/lib/tasker_core/domain_events.rb +43 -0
  28. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  29. data/lib/tasker_core/errors/common.rb +305 -0
  30. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  31. data/lib/tasker_core/errors.rb +4 -0
  32. data/lib/tasker_core/event_bridge.rb +330 -0
  33. data/lib/tasker_core/handlers.rb +159 -0
  34. data/lib/tasker_core/internal.rb +31 -0
  35. data/lib/tasker_core/logger.rb +234 -0
  36. data/lib/tasker_core/models.rb +337 -0
  37. data/lib/tasker_core/observability/types.rb +158 -0
  38. data/lib/tasker_core/observability.rb +292 -0
  39. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  40. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  41. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  42. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  43. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  44. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  45. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  46. data/lib/tasker_core/registry/resolvers.rb +42 -0
  47. data/lib/tasker_core/registry.rb +12 -0
  48. data/lib/tasker_core/step_handler/api.rb +48 -0
  49. data/lib/tasker_core/step_handler/base.rb +354 -0
  50. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  51. data/lib/tasker_core/step_handler/decision.rb +53 -0
  52. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  53. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  54. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  55. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  56. data/lib/tasker_core/subscriber.rb +212 -0
  57. data/lib/tasker_core/task_handler/base.rb +254 -0
  58. data/lib/tasker_core/tasker_rb.so +0 -0
  59. data/lib/tasker_core/template_discovery.rb +181 -0
  60. data/lib/tasker_core/tracing.rb +166 -0
  61. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  62. data/lib/tasker_core/types/client_types.rb +145 -0
  63. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  64. data/lib/tasker_core/types/error_types.rb +72 -0
  65. data/lib/tasker_core/types/simple_message.rb +151 -0
  66. data/lib/tasker_core/types/step_context.rb +328 -0
  67. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  68. data/lib/tasker_core/types/step_message.rb +112 -0
  69. data/lib/tasker_core/types/step_types.rb +207 -0
  70. data/lib/tasker_core/types/task_template.rb +240 -0
  71. data/lib/tasker_core/types/task_types.rb +148 -0
  72. data/lib/tasker_core/types.rb +132 -0
  73. data/lib/tasker_core/version.rb +13 -0
  74. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  75. data/lib/tasker_core/worker/event_poller.rb +224 -0
  76. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  77. data/lib/tasker_core.rb +160 -0
  78. metadata +322 -0
@@ -0,0 +1,400 @@
1
+ //! # Ruby FFI Bridge with System Handle Management
2
+ //!
3
+ //! Provides lifecycle management for the worker system with status checking
4
+ //! and graceful shutdown capabilities, following patterns from embedded mode.
5
+
6
+ use crate::bootstrap::{
7
+ bootstrap_worker, get_worker_status, stop_worker, transition_to_graceful_shutdown,
8
+ };
9
+ use crate::conversions::{
10
+ convert_ffi_step_event_to_ruby, convert_ruby_checkpoint_to_yield_data,
11
+ convert_ruby_completion_to_step_result,
12
+ };
13
+ use crate::ffi_logging::{log_debug, log_error, log_info, log_trace, log_warn};
14
+ use magnus::{function, prelude::*, Error, ExceptionClass, RModule, Ruby, Value};
15
+ use std::sync::{Arc, Mutex};
16
+
17
+ /// Helper to get RuntimeError exception class (magnus 0.8 API)
18
+ fn runtime_error_class() -> ExceptionClass {
19
+ Ruby::get()
20
+ .expect("Ruby runtime should be available")
21
+ .exception_runtime_error()
22
+ }
23
+
24
+ /// Helper to get ArgumentError exception class (magnus 0.8 API)
25
+ fn arg_error_class() -> ExceptionClass {
26
+ Ruby::get()
27
+ .expect("Ruby runtime should be available")
28
+ .exception_arg_error()
29
+ }
30
+ use tasker_shared::errors::TaskerResult;
31
+ use tasker_shared::events::domain_events::DomainEvent;
32
+ use tasker_worker::worker::{FfiDispatchChannel, FfiDispatchMetrics, FfiStepEvent};
33
+ use tasker_worker::{WorkerSystemHandle, WorkerSystemStatus};
34
+ use tokio::sync::broadcast;
35
+ use tracing::{debug, error, info};
36
+ use uuid::Uuid;
37
+
38
+ /// Global handle to the worker system for Ruby FFI
39
+ pub static WORKER_SYSTEM: Mutex<Option<RubyBridgeHandle>> = Mutex::new(None);
40
+
41
+ /// Bridge handle that maintains worker system state
42
+ pub struct RubyBridgeHandle {
43
+ /// Handle from tasker-worker bootstrap
44
+ pub system_handle: WorkerSystemHandle,
45
+ /// TAS-67: FFI dispatch channel for step execution events
46
+ /// Ruby polls this to receive step events and submits completions
47
+ pub ffi_dispatch_channel: Arc<FfiDispatchChannel>,
48
+ /// TAS-65: Domain event publisher for Ruby handlers
49
+ pub domain_event_publisher: Arc<tasker_shared::events::domain_events::DomainEventPublisher>,
50
+ /// TAS-65 Phase 4.1: In-process event receiver for fast domain events
51
+ /// Ruby can poll this channel to receive domain events with delivery_mode: fast
52
+ pub in_process_event_receiver: Option<Arc<Mutex<broadcast::Receiver<DomainEvent>>>>,
53
+ /// TAS-231: FFI client bridge for orchestration API access
54
+ pub client: Option<Arc<tasker_worker::FfiClientBridge>>,
55
+ /// Tokio runtime for async FFI operations
56
+ pub runtime: tokio::runtime::Runtime,
57
+ }
58
+
59
+ impl RubyBridgeHandle {
60
+ pub fn new(
61
+ system_handle: WorkerSystemHandle,
62
+ ffi_dispatch_channel: Arc<FfiDispatchChannel>,
63
+ domain_event_publisher: Arc<tasker_shared::events::domain_events::DomainEventPublisher>,
64
+ in_process_event_receiver: Option<broadcast::Receiver<DomainEvent>>,
65
+ client: Option<Arc<tasker_worker::FfiClientBridge>>,
66
+ runtime: tokio::runtime::Runtime,
67
+ ) -> Self {
68
+ Self {
69
+ system_handle,
70
+ ffi_dispatch_channel,
71
+ domain_event_publisher,
72
+ in_process_event_receiver: in_process_event_receiver.map(|r| Arc::new(Mutex::new(r))),
73
+ client,
74
+ runtime,
75
+ }
76
+ }
77
+
78
+ pub async fn status(&self) -> TaskerResult<WorkerSystemStatus> {
79
+ self.system_handle.status().await
80
+ }
81
+
82
+ pub fn stop(&mut self) -> Result<(), String> {
83
+ self.runtime
84
+ .block_on(self.system_handle.stop())
85
+ .map_err(|e| e.to_string())
86
+ }
87
+
88
+ pub fn runtime_handle(&self) -> &tokio::runtime::Handle {
89
+ &self.system_handle.runtime_handle
90
+ }
91
+
92
+ /// TAS-67: Poll for the next step execution event via FfiDispatchChannel
93
+ /// Returns None if no events are available (non-blocking)
94
+ pub fn poll_step_event(&self) -> Option<FfiStepEvent> {
95
+ self.ffi_dispatch_channel.poll()
96
+ }
97
+
98
+ /// TAS-67: Submit a completion result for an event
99
+ /// Returns true if the completion was successfully submitted
100
+ pub fn complete_step_event(
101
+ &self,
102
+ event_id: Uuid,
103
+ result: tasker_shared::messaging::StepExecutionResult,
104
+ ) -> bool {
105
+ self.ffi_dispatch_channel.complete(event_id, result)
106
+ }
107
+
108
+ /// TAS-125: Submit a checkpoint yield for a batch processing step
109
+ /// Returns true if the checkpoint was persisted and step re-dispatched
110
+ pub fn checkpoint_yield_step_event(
111
+ &self,
112
+ event_id: Uuid,
113
+ checkpoint_data: tasker_shared::models::batch_worker::CheckpointYieldData,
114
+ ) -> bool {
115
+ self.ffi_dispatch_channel
116
+ .checkpoint_yield(event_id, checkpoint_data)
117
+ }
118
+
119
+ /// TAS-67 Phase 2: Get metrics about FFI dispatch channel health
120
+ pub fn get_ffi_dispatch_metrics(&self) -> FfiDispatchMetrics {
121
+ self.ffi_dispatch_channel.metrics()
122
+ }
123
+
124
+ /// TAS-67 Phase 2: Check for starvation warnings and emit logs
125
+ pub fn check_starvation_warnings(&self) {
126
+ self.ffi_dispatch_channel.check_starvation_warnings()
127
+ }
128
+ }
129
+
130
+ /// TAS-67: FFI function for Ruby to poll for step execution events
131
+ /// Returns a Ruby hash representation of the FfiStepEvent or nil if none available
132
+ pub fn poll_step_events() -> Result<Value, Error> {
133
+ let handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
134
+ error!("Failed to acquire worker system lock: {}", e);
135
+ Error::new(runtime_error_class(), "Lock acquisition failed")
136
+ })?;
137
+
138
+ let handle = handle_guard
139
+ .as_ref()
140
+ .ok_or_else(|| Error::new(runtime_error_class(), "Worker system not running"))?;
141
+
142
+ // Poll for next event via FfiDispatchChannel
143
+ if let Some(event) = handle.poll_step_event() {
144
+ debug!(
145
+ event_id = %event.event_id,
146
+ step_uuid = %event.step_uuid,
147
+ "Polled FFI step event for Ruby processing"
148
+ );
149
+
150
+ // Convert the FfiStepEvent to Ruby format
151
+ let ruby_event = convert_ffi_step_event_to_ruby(&event).map_err(|e| {
152
+ error!("Failed to convert event to Ruby: {}", e);
153
+ Error::new(
154
+ runtime_error_class(),
155
+ format!("Failed to convert event to Ruby: {}", e),
156
+ )
157
+ })?;
158
+
159
+ Ok(ruby_event.as_value())
160
+ } else {
161
+ // Return nil when no events are available
162
+ let ruby = magnus::Ruby::get().map_err(|err| {
163
+ Error::new(
164
+ runtime_error_class(),
165
+ format!("Failed to get ruby system: {}", err),
166
+ )
167
+ })?;
168
+ Ok(ruby.qnil().as_value())
169
+ }
170
+ }
171
+
172
+ /// TAS-67: FFI function for Ruby to submit step completion
173
+ /// Called after a handler has completed execution
174
+ ///
175
+ /// # Arguments
176
+ /// * `event_id` - The event ID string from the FfiStepEvent
177
+ /// * `completion_data` - Ruby hash with completion result data
178
+ pub fn complete_step_event(event_id_str: String, completion_data: Value) -> Result<bool, Error> {
179
+ // Parse event_id
180
+ let event_id = Uuid::parse_str(&event_id_str).map_err(|e| {
181
+ error!("Invalid event_id format: {}", e);
182
+ Error::new(arg_error_class(), format!("Invalid event_id format: {}", e))
183
+ })?;
184
+
185
+ // Convert Ruby completion to StepExecutionResult
186
+ let result = convert_ruby_completion_to_step_result(completion_data).map_err(|e| {
187
+ error!("Failed to convert completion data: {}", e);
188
+ Error::new(
189
+ runtime_error_class(),
190
+ format!("Failed to convert completion data: {}", e),
191
+ )
192
+ })?;
193
+
194
+ // Get bridge handle and submit completion
195
+ let handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
196
+ error!("Failed to acquire worker system lock: {}", e);
197
+ Error::new(runtime_error_class(), "Lock acquisition failed")
198
+ })?;
199
+
200
+ let handle = handle_guard
201
+ .as_ref()
202
+ .ok_or_else(|| Error::new(runtime_error_class(), "Worker system not running"))?;
203
+
204
+ // Submit completion via FfiDispatchChannel
205
+ let success = handle.complete_step_event(event_id, result);
206
+
207
+ if success {
208
+ debug!(event_id = %event_id, "Step completion submitted successfully");
209
+ } else {
210
+ error!(event_id = %event_id, "Failed to submit step completion");
211
+ }
212
+
213
+ Ok(success)
214
+ }
215
+
216
+ /// TAS-125: FFI function for Ruby to submit a checkpoint yield
217
+ ///
218
+ /// Called from batch processing handlers when they want to persist progress
219
+ /// and be re-dispatched for continuation. Unlike complete_step_event, this
220
+ /// does NOT complete the step - instead it persists checkpoint data and
221
+ /// re-dispatches the step for continued processing.
222
+ ///
223
+ /// # Arguments
224
+ /// * `event_id` - The event ID string from the FfiStepEvent
225
+ /// * `checkpoint_data` - Ruby hash with checkpoint data:
226
+ /// - step_uuid: String (UUID of the step)
227
+ /// - cursor: Any JSON value (position to resume from)
228
+ /// - items_processed: Integer (count of items processed so far)
229
+ /// - accumulated_results: Optional JSON value (partial results)
230
+ ///
231
+ /// # Returns
232
+ /// `true` if the checkpoint was persisted and step re-dispatched,
233
+ /// `false` if checkpoint support is not configured or an error occurred.
234
+ pub fn checkpoint_yield_step_event(
235
+ event_id_str: String,
236
+ checkpoint_data: Value,
237
+ ) -> Result<bool, Error> {
238
+ // Parse event_id
239
+ let event_id = Uuid::parse_str(&event_id_str).map_err(|e| {
240
+ error!("Invalid event_id format: {}", e);
241
+ Error::new(arg_error_class(), format!("Invalid event_id format: {}", e))
242
+ })?;
243
+
244
+ // Convert Ruby checkpoint data to CheckpointYieldData
245
+ let checkpoint = convert_ruby_checkpoint_to_yield_data(checkpoint_data).map_err(|e| {
246
+ error!("Failed to convert checkpoint data: {}", e);
247
+ Error::new(
248
+ runtime_error_class(),
249
+ format!("Failed to convert checkpoint data: {}", e),
250
+ )
251
+ })?;
252
+
253
+ // Get bridge handle and submit checkpoint yield
254
+ let handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
255
+ error!("Failed to acquire worker system lock: {}", e);
256
+ Error::new(runtime_error_class(), "Lock acquisition failed")
257
+ })?;
258
+
259
+ let handle = handle_guard
260
+ .as_ref()
261
+ .ok_or_else(|| Error::new(runtime_error_class(), "Worker system not running"))?;
262
+
263
+ // Submit checkpoint yield via FfiDispatchChannel
264
+ let success = handle.checkpoint_yield_step_event(event_id, checkpoint);
265
+
266
+ if success {
267
+ info!(
268
+ event_id = %event_id,
269
+ "Checkpoint yield submitted and step re-dispatched"
270
+ );
271
+ } else {
272
+ error!(
273
+ event_id = %event_id,
274
+ "Failed to submit checkpoint yield (checkpoint support may not be configured)"
275
+ );
276
+ }
277
+
278
+ Ok(success)
279
+ }
280
+
281
+ /// TAS-67 Phase 2: FFI function for Ruby to get FFI dispatch channel metrics
282
+ /// Returns a Ruby hash with metrics for monitoring and observability
283
+ ///
284
+ /// The returned hash contains:
285
+ /// - `pending_count`: Number of events waiting for completion
286
+ /// - `oldest_pending_age_ms`: Age of the oldest pending event in milliseconds
287
+ /// - `newest_pending_age_ms`: Age of the newest pending event in milliseconds
288
+ /// - `oldest_event_id`: UUID of the oldest pending event (for debugging)
289
+ /// - `starvation_detected`: Boolean indicating if any events exceed starvation threshold
290
+ /// - `starving_event_count`: Number of events exceeding starvation threshold
291
+ pub fn get_ffi_dispatch_metrics() -> Result<Value, Error> {
292
+ let handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
293
+ error!("Failed to acquire worker system lock: {}", e);
294
+ Error::new(runtime_error_class(), "Lock acquisition failed")
295
+ })?;
296
+
297
+ let handle = handle_guard
298
+ .as_ref()
299
+ .ok_or_else(|| Error::new(runtime_error_class(), "Worker system not running"))?;
300
+
301
+ let metrics = handle.get_ffi_dispatch_metrics();
302
+
303
+ let ruby = magnus::Ruby::get().map_err(|err| {
304
+ Error::new(
305
+ runtime_error_class(),
306
+ format!("Failed to get ruby system: {}", err),
307
+ )
308
+ })?;
309
+
310
+ let hash = ruby.hash_new();
311
+ hash.aset("pending_count", metrics.pending_count)?;
312
+ hash.aset(
313
+ "oldest_pending_age_ms",
314
+ metrics.oldest_pending_age_ms.map(|v| v as i64),
315
+ )?;
316
+ hash.aset(
317
+ "newest_pending_age_ms",
318
+ metrics.newest_pending_age_ms.map(|v| v as i64),
319
+ )?;
320
+ hash.aset(
321
+ "oldest_event_id",
322
+ metrics.oldest_event_id.map(|id| id.to_string()),
323
+ )?;
324
+ hash.aset("starvation_detected", metrics.starvation_detected)?;
325
+ hash.aset("starving_event_count", metrics.starving_event_count)?;
326
+
327
+ Ok(hash.as_value())
328
+ }
329
+
330
+ /// TAS-67 Phase 2: FFI function for Ruby to check for starvation warnings
331
+ /// This emits warning logs for any pending events that exceed the starvation threshold.
332
+ /// Call this periodically (e.g., every poll cycle) for proactive monitoring.
333
+ pub fn check_starvation_warnings() -> Result<bool, Error> {
334
+ let handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
335
+ error!("Failed to acquire worker system lock: {}", e);
336
+ Error::new(runtime_error_class(), "Lock acquisition failed")
337
+ })?;
338
+
339
+ let handle = handle_guard
340
+ .as_ref()
341
+ .ok_or_else(|| Error::new(runtime_error_class(), "Worker system not running"))?;
342
+
343
+ handle.check_starvation_warnings();
344
+ Ok(true)
345
+ }
346
+
347
+ /// Initialize the bridge module with all FFI functions
348
+ pub fn init_bridge(module: &RModule) -> Result<(), Error> {
349
+ info!("🔌 Initializing Ruby FFI bridge");
350
+
351
+ // Bootstrap and lifecycle
352
+ module.define_singleton_method("bootstrap_worker", function!(bootstrap_worker, 0))?;
353
+ module.define_singleton_method("stop_worker", function!(stop_worker, 0))?;
354
+ module.define_singleton_method("worker_status", function!(get_worker_status, 0))?;
355
+ module.define_singleton_method(
356
+ "transition_to_graceful_shutdown",
357
+ function!(transition_to_graceful_shutdown, 0),
358
+ )?;
359
+
360
+ // TAS-67: Event handling via FfiDispatchChannel
361
+ module.define_singleton_method("poll_step_events", function!(poll_step_events, 0))?;
362
+ module.define_singleton_method("complete_step_event", function!(complete_step_event, 2))?;
363
+ // TAS-125: Checkpoint yield for batch processing handlers
364
+ module.define_singleton_method(
365
+ "checkpoint_yield_step_event",
366
+ function!(checkpoint_yield_step_event, 2),
367
+ )?;
368
+
369
+ // TAS-67 Phase 2: Observability and metrics
370
+ module.define_singleton_method(
371
+ "get_ffi_dispatch_metrics",
372
+ function!(get_ffi_dispatch_metrics, 0),
373
+ )?;
374
+ module.define_singleton_method(
375
+ "check_starvation_warnings",
376
+ function!(check_starvation_warnings, 0),
377
+ )?;
378
+
379
+ // TAS-29 Phase 6: Unified structured logging via FFI
380
+ module.define_singleton_method("log_error", function!(log_error, 2))?;
381
+ module.define_singleton_method("log_warn", function!(log_warn, 2))?;
382
+ module.define_singleton_method("log_info", function!(log_info, 2))?;
383
+ module.define_singleton_method("log_debug", function!(log_debug, 2))?;
384
+ module.define_singleton_method("log_trace", function!(log_trace, 2))?;
385
+
386
+ // TAS-65 Phase 2.4a: Domain event publishing (durable path)
387
+ crate::event_publisher_ffi::init_event_publisher_ffi(module)?;
388
+
389
+ // TAS-65 Phase 4.1: In-process event polling (fast path)
390
+ crate::in_process_event_ffi::init_in_process_event_ffi(module)?;
391
+
392
+ // TAS-77: Observability services FFI (health, metrics, templates, config)
393
+ crate::observability_ffi::init_observability_ffi(module)?;
394
+
395
+ // TAS-231: Client API functions
396
+ crate::client_ffi::init_client_ffi(module)?;
397
+
398
+ info!("✅ Ruby FFI bridge initialized");
399
+ Ok(())
400
+ }
@@ -0,0 +1,173 @@
1
+ //! # Ruby Client FFI Functions
2
+ //!
3
+ //! TAS-231: Exposes orchestration client operations to Ruby via Magnus.
4
+ //! Uses serde_magnus for type conversions.
5
+
6
+ use crate::bridge::WORKER_SYSTEM;
7
+ use magnus::{prelude::*, Error, ExceptionClass, Ruby, Value};
8
+ use tracing::error;
9
+
10
+ /// Helper to get RuntimeError exception class
11
+ fn runtime_error_class() -> ExceptionClass {
12
+ Ruby::get()
13
+ .expect("Ruby runtime should be available")
14
+ .exception_runtime_error()
15
+ }
16
+
17
+ /// Helper to get ArgumentError exception class
18
+ fn arg_error_class() -> ExceptionClass {
19
+ Ruby::get()
20
+ .expect("Ruby runtime should be available")
21
+ .exception_arg_error()
22
+ }
23
+
24
+ /// Helper: call a client method, converting the result to a Ruby value.
25
+ fn call_client<F>(op_name: &str, f: F) -> Result<Value, Error>
26
+ where
27
+ F: FnOnce(
28
+ &tasker_worker::FfiClientBridge,
29
+ ) -> Result<serde_json::Value, tasker_worker::FfiClientError>,
30
+ {
31
+ let ruby = Ruby::get().map_err(|e| {
32
+ Error::new(
33
+ runtime_error_class(),
34
+ format!("Failed to get Ruby runtime: {e}"),
35
+ )
36
+ })?;
37
+
38
+ let handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
39
+ error!("Failed to acquire worker system lock: {}", e);
40
+ Error::new(runtime_error_class(), "Lock acquisition failed")
41
+ })?;
42
+
43
+ let handle = handle_guard
44
+ .as_ref()
45
+ .ok_or_else(|| Error::new(runtime_error_class(), "Worker system not running"))?;
46
+
47
+ let client = handle.client.as_ref().ok_or_else(|| {
48
+ Error::new(
49
+ runtime_error_class(),
50
+ "Client not initialized. Orchestration client may not be configured.",
51
+ )
52
+ })?;
53
+
54
+ match f(client) {
55
+ Ok(value) => serde_magnus::serialize(&ruby, &value).map_err(|e| {
56
+ Error::new(
57
+ runtime_error_class(),
58
+ format!("Failed to convert {op_name} response: {e}"),
59
+ )
60
+ }),
61
+ Err(e) => {
62
+ error!(op = op_name, error = %e, recoverable = e.is_recoverable, "Client operation failed");
63
+ Err(Error::new(
64
+ runtime_error_class(),
65
+ format!("{op_name} failed: {e}"),
66
+ ))
67
+ }
68
+ }
69
+ }
70
+
71
+ /// Create a new task via the orchestration API.
72
+ pub fn client_create_task(request_hash: Value) -> Result<Value, Error> {
73
+ let ruby = Ruby::get().map_err(|e| {
74
+ Error::new(
75
+ runtime_error_class(),
76
+ format!("Failed to get Ruby runtime: {e}"),
77
+ )
78
+ })?;
79
+ let task_request: tasker_shared::models::core::task_request::TaskRequest =
80
+ serde_magnus::deserialize(&ruby, request_hash)
81
+ .map_err(|e| Error::new(arg_error_class(), format!("Invalid task request: {e}")))?;
82
+
83
+ call_client("create_task", move |client| {
84
+ client.create_task(task_request)
85
+ })
86
+ }
87
+
88
+ /// Get a task by UUID.
89
+ pub fn client_get_task(task_uuid: String) -> Result<Value, Error> {
90
+ let uuid = uuid::Uuid::parse_str(&task_uuid)
91
+ .map_err(|e| Error::new(arg_error_class(), format!("Invalid UUID: {e}")))?;
92
+
93
+ call_client("get_task", move |client| client.get_task(uuid))
94
+ }
95
+
96
+ /// List tasks with pagination and optional filters.
97
+ pub fn client_list_tasks(
98
+ limit: i32,
99
+ offset: i32,
100
+ namespace: Option<String>,
101
+ status: Option<String>,
102
+ ) -> Result<Value, Error> {
103
+ call_client("list_tasks", move |client| {
104
+ client.list_tasks(limit, offset, namespace.as_deref(), status.as_deref())
105
+ })
106
+ }
107
+
108
+ /// Cancel a task.
109
+ pub fn client_cancel_task(task_uuid: String) -> Result<Value, Error> {
110
+ let uuid = uuid::Uuid::parse_str(&task_uuid)
111
+ .map_err(|e| Error::new(arg_error_class(), format!("Invalid UUID: {e}")))?;
112
+
113
+ call_client("cancel_task", move |client| client.cancel_task(uuid))
114
+ }
115
+
116
+ /// List workflow steps for a task.
117
+ pub fn client_list_task_steps(task_uuid: String) -> Result<Value, Error> {
118
+ let uuid = uuid::Uuid::parse_str(&task_uuid)
119
+ .map_err(|e| Error::new(arg_error_class(), format!("Invalid UUID: {e}")))?;
120
+
121
+ call_client("list_task_steps", move |client| {
122
+ client.list_task_steps(uuid)
123
+ })
124
+ }
125
+
126
+ /// Get a specific workflow step.
127
+ pub fn client_get_step(task_uuid: String, step_uuid: String) -> Result<Value, Error> {
128
+ let t_uuid = uuid::Uuid::parse_str(&task_uuid)
129
+ .map_err(|e| Error::new(arg_error_class(), format!("Invalid task UUID: {e}")))?;
130
+ let s_uuid = uuid::Uuid::parse_str(&step_uuid)
131
+ .map_err(|e| Error::new(arg_error_class(), format!("Invalid step UUID: {e}")))?;
132
+
133
+ call_client("get_step", move |client| client.get_step(t_uuid, s_uuid))
134
+ }
135
+
136
+ /// Get audit history for a workflow step.
137
+ pub fn client_get_step_audit_history(task_uuid: String, step_uuid: String) -> Result<Value, Error> {
138
+ let t_uuid = uuid::Uuid::parse_str(&task_uuid)
139
+ .map_err(|e| Error::new(arg_error_class(), format!("Invalid task UUID: {e}")))?;
140
+ let s_uuid = uuid::Uuid::parse_str(&step_uuid)
141
+ .map_err(|e| Error::new(arg_error_class(), format!("Invalid step UUID: {e}")))?;
142
+
143
+ call_client("get_step_audit_history", move |client| {
144
+ client.get_step_audit_history(t_uuid, s_uuid)
145
+ })
146
+ }
147
+
148
+ /// Check if the orchestration API is healthy.
149
+ pub fn client_health_check() -> Result<Value, Error> {
150
+ call_client("health_check", |client| client.health_check())
151
+ }
152
+
153
+ /// Initialize the client FFI module with all client methods.
154
+ pub fn init_client_ffi(module: &magnus::RModule) -> Result<(), Error> {
155
+ use magnus::function;
156
+
157
+ module.define_singleton_method("client_create_task", function!(client_create_task, 1))?;
158
+ module.define_singleton_method("client_get_task", function!(client_get_task, 1))?;
159
+ module.define_singleton_method("client_list_tasks", function!(client_list_tasks, 4))?;
160
+ module.define_singleton_method("client_cancel_task", function!(client_cancel_task, 1))?;
161
+ module.define_singleton_method(
162
+ "client_list_task_steps",
163
+ function!(client_list_task_steps, 1),
164
+ )?;
165
+ module.define_singleton_method("client_get_step", function!(client_get_step, 2))?;
166
+ module.define_singleton_method(
167
+ "client_get_step_audit_history",
168
+ function!(client_get_step_audit_history, 2),
169
+ )?;
170
+ module.define_singleton_method("client_health_check", function!(client_health_check, 0))?;
171
+
172
+ Ok(())
173
+ }