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,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
|
+
}
|