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,131 @@
|
|
|
1
|
+
use magnus::{RHash, Value as RValue};
|
|
2
|
+
use serde_magnus::serialize;
|
|
3
|
+
use tasker_shared::errors::{TaskerError, TaskerResult};
|
|
4
|
+
use tasker_shared::messaging::StepExecutionResult;
|
|
5
|
+
use tasker_shared::models::batch_worker::CheckpointYieldData;
|
|
6
|
+
use tasker_shared::types::TaskSequenceStep;
|
|
7
|
+
use tasker_worker::worker::FfiStepEvent;
|
|
8
|
+
|
|
9
|
+
// Convert TaskSequenceStep to Ruby hash with full fidelity
|
|
10
|
+
fn convert_task_sequence_step_to_ruby(tss: &TaskSequenceStep) -> TaskerResult<RHash> {
|
|
11
|
+
let ruby = magnus::Ruby::get().map_err(|err| {
|
|
12
|
+
TaskerError::FFIError(format!("Ruby Magnus FFI Ruby Acquire Error: {err}"))
|
|
13
|
+
})?;
|
|
14
|
+
|
|
15
|
+
let ruby_hash: RHash = serialize(&ruby, tss).map_err(|err| {
|
|
16
|
+
TaskerError::FFIError(format!(
|
|
17
|
+
"Could not serialize TaskSequenceStep to Ruby hash: {err}"
|
|
18
|
+
))
|
|
19
|
+
})?;
|
|
20
|
+
|
|
21
|
+
Ok(ruby_hash)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub fn tasker_error_from_magnus_error(err: &magnus::Error) -> TaskerError {
|
|
25
|
+
TaskerError::FFIError(format!("Ruby Magnus FFI Error: {err}"))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// TAS-67: Convert FfiStepEvent to Ruby hash
|
|
29
|
+
///
|
|
30
|
+
/// This converts the new dispatch channel event format to Ruby.
|
|
31
|
+
/// Includes the event_id for correlation when completing the step.
|
|
32
|
+
pub fn convert_ffi_step_event_to_ruby(event: &FfiStepEvent) -> TaskerResult<RHash> {
|
|
33
|
+
let ruby = magnus::Ruby::get().map_err(|err| {
|
|
34
|
+
TaskerError::FFIError(format!("Ruby Magnus FFI Ruby Acquire Error: {err}"))
|
|
35
|
+
})?;
|
|
36
|
+
|
|
37
|
+
let event_hash = ruby.hash_new();
|
|
38
|
+
|
|
39
|
+
// TAS-67: Event ID is critical for completion correlation
|
|
40
|
+
event_hash
|
|
41
|
+
.aset("event_id", event.event_id.to_string())
|
|
42
|
+
.map_err(|err| tasker_error_from_magnus_error(&err))?;
|
|
43
|
+
|
|
44
|
+
event_hash
|
|
45
|
+
.aset("task_uuid", event.task_uuid.to_string())
|
|
46
|
+
.map_err(|err| tasker_error_from_magnus_error(&err))?;
|
|
47
|
+
|
|
48
|
+
event_hash
|
|
49
|
+
.aset("step_uuid", event.step_uuid.to_string())
|
|
50
|
+
.map_err(|err| tasker_error_from_magnus_error(&err))?;
|
|
51
|
+
|
|
52
|
+
event_hash
|
|
53
|
+
.aset("correlation_id", event.correlation_id.to_string())
|
|
54
|
+
.map_err(|err| tasker_error_from_magnus_error(&err))?;
|
|
55
|
+
|
|
56
|
+
// Optional trace context
|
|
57
|
+
if let Some(ref trace_id) = event.trace_id {
|
|
58
|
+
event_hash
|
|
59
|
+
.aset("trace_id", trace_id.clone())
|
|
60
|
+
.map_err(|err| tasker_error_from_magnus_error(&err))?;
|
|
61
|
+
}
|
|
62
|
+
if let Some(ref span_id) = event.span_id {
|
|
63
|
+
event_hash
|
|
64
|
+
.aset("span_id", span_id.clone())
|
|
65
|
+
.map_err(|err| tasker_error_from_magnus_error(&err))?;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Convert the full execution event payload (contains TaskSequenceStep)
|
|
69
|
+
let payload = &event.execution_event.payload;
|
|
70
|
+
|
|
71
|
+
// TAS-29: Also expose correlation_id from task if different
|
|
72
|
+
let task_correlation_id = payload.task_sequence_step.task.task.correlation_id;
|
|
73
|
+
event_hash
|
|
74
|
+
.aset("task_correlation_id", task_correlation_id.to_string())
|
|
75
|
+
.map_err(|err| tasker_error_from_magnus_error(&err))?;
|
|
76
|
+
|
|
77
|
+
// Parent correlation ID if present
|
|
78
|
+
if let Some(parent_id) = payload.task_sequence_step.task.task.parent_correlation_id {
|
|
79
|
+
event_hash
|
|
80
|
+
.aset("parent_correlation_id", parent_id.to_string())
|
|
81
|
+
.map_err(|err| tasker_error_from_magnus_error(&err))?;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Convert TaskSequenceStep to Ruby hash
|
|
85
|
+
let task_sequence_step_hash = convert_task_sequence_step_to_ruby(&payload.task_sequence_step)?;
|
|
86
|
+
event_hash
|
|
87
|
+
.aset("task_sequence_step", task_sequence_step_hash)
|
|
88
|
+
.map_err(|err| tasker_error_from_magnus_error(&err))?;
|
|
89
|
+
|
|
90
|
+
Ok(event_hash)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// TAS-67: Convert Ruby completion hash to StepExecutionResult
|
|
94
|
+
///
|
|
95
|
+
/// This is the new conversion for the FfiDispatchChannel flow.
|
|
96
|
+
/// The Ruby hash should contain the result data that can be deserialized
|
|
97
|
+
/// into a StepExecutionResult.
|
|
98
|
+
pub fn convert_ruby_completion_to_step_result(value: RValue) -> TaskerResult<StepExecutionResult> {
|
|
99
|
+
let ruby = magnus::Ruby::get().map_err(|err| {
|
|
100
|
+
TaskerError::FFIError(format!("Ruby Magnus FFI Ruby Acquire Error: {err}"))
|
|
101
|
+
})?;
|
|
102
|
+
|
|
103
|
+
let result: StepExecutionResult = serde_magnus::deserialize(&ruby, value).map_err(|err| {
|
|
104
|
+
TaskerError::FFIError(format!(
|
|
105
|
+
"Could not deserialize to StepExecutionResult: {err}"
|
|
106
|
+
))
|
|
107
|
+
})?;
|
|
108
|
+
|
|
109
|
+
Ok(result)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// TAS-125: Convert Ruby checkpoint data hash to CheckpointYieldData
|
|
113
|
+
///
|
|
114
|
+
/// The Ruby hash should contain:
|
|
115
|
+
/// - step_uuid: String (UUID of the step)
|
|
116
|
+
/// - cursor: Any JSON value (position to resume from)
|
|
117
|
+
/// - items_processed: Integer (count of items processed so far)
|
|
118
|
+
/// - accumulated_results: Optional JSON value (partial results to carry forward)
|
|
119
|
+
pub fn convert_ruby_checkpoint_to_yield_data(value: RValue) -> TaskerResult<CheckpointYieldData> {
|
|
120
|
+
let ruby = magnus::Ruby::get().map_err(|err| {
|
|
121
|
+
TaskerError::FFIError(format!("Ruby Magnus FFI Ruby Acquire Error: {err}"))
|
|
122
|
+
})?;
|
|
123
|
+
|
|
124
|
+
let data: CheckpointYieldData = serde_magnus::deserialize(&ruby, value).map_err(|err| {
|
|
125
|
+
TaskerError::FFIError(format!(
|
|
126
|
+
"Could not deserialize to CheckpointYieldData: {err}"
|
|
127
|
+
))
|
|
128
|
+
})?;
|
|
129
|
+
|
|
130
|
+
Ok(data)
|
|
131
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//! System diagnostics for Ruby FFI
|
|
2
|
+
//!
|
|
3
|
+
//! Provides diagnostic information about the Ruby FFI environment to help
|
|
4
|
+
//! troubleshoot architecture-specific issues (M2 vs M4 Pro, etc.)
|
|
5
|
+
|
|
6
|
+
use magnus::value::ReprValue;
|
|
7
|
+
use magnus::{Error, ExceptionClass, Ruby, Value};
|
|
8
|
+
|
|
9
|
+
/// Helper to get RuntimeError exception class
|
|
10
|
+
fn runtime_error_class() -> ExceptionClass {
|
|
11
|
+
Ruby::get()
|
|
12
|
+
.expect("Ruby runtime should be available")
|
|
13
|
+
.exception_runtime_error()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Get system diagnostics for troubleshooting
|
|
17
|
+
///
|
|
18
|
+
/// Returns a hash with:
|
|
19
|
+
/// - rust_version: Cargo package version
|
|
20
|
+
/// - ruby_version: Ruby version string
|
|
21
|
+
/// - ruby_abi: Ruby ABI version
|
|
22
|
+
/// - thread_count: Available parallelism (CPU cores)
|
|
23
|
+
/// - arch: System architecture (aarch64, x86_64, etc.)
|
|
24
|
+
/// - tokio_runtime: Information about Tokio runtime if available
|
|
25
|
+
pub fn system_diagnostics() -> Result<Value, Error> {
|
|
26
|
+
let ruby = Ruby::get().map_err(|err| {
|
|
27
|
+
Error::new(
|
|
28
|
+
runtime_error_class(),
|
|
29
|
+
format!("Failed to get Ruby runtime: {}", err),
|
|
30
|
+
)
|
|
31
|
+
})?;
|
|
32
|
+
|
|
33
|
+
let hash = ruby.hash_new();
|
|
34
|
+
|
|
35
|
+
// Rust environment
|
|
36
|
+
hash.aset("rust_version", env!("CARGO_PKG_VERSION"))?;
|
|
37
|
+
hash.aset("magnus_version", "0.8")?;
|
|
38
|
+
|
|
39
|
+
// Ruby environment
|
|
40
|
+
let ruby_version = ruby
|
|
41
|
+
.eval::<String>("RUBY_VERSION")
|
|
42
|
+
.unwrap_or_else(|_| "unknown".to_string());
|
|
43
|
+
hash.aset("ruby_version", ruby_version)?;
|
|
44
|
+
|
|
45
|
+
// System information
|
|
46
|
+
let thread_count = std::thread::available_parallelism()
|
|
47
|
+
.map(|n| n.get())
|
|
48
|
+
.unwrap_or(0);
|
|
49
|
+
hash.aset("thread_count", thread_count)?;
|
|
50
|
+
hash.aset("arch", std::env::consts::ARCH)?;
|
|
51
|
+
hash.aset("os", std::env::consts::OS)?;
|
|
52
|
+
|
|
53
|
+
// Build information - embed feature was removed for M4 Pro compatibility
|
|
54
|
+
hash.aset("magnus_embed_feature", false)?;
|
|
55
|
+
|
|
56
|
+
Ok(hash.as_value())
|
|
57
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
//! # Ruby Event Handler
|
|
2
|
+
//!
|
|
3
|
+
//! Bridges `WorkerEventSystem` to Ruby dry-events without circular dependencies.
|
|
4
|
+
//! Events flow: Rust → Ruby for execution, Ruby → Rust for completion.
|
|
5
|
+
//!
|
|
6
|
+
//! Uses MPSC channel for thread-safe communication between tokio tasks and Ruby threads.
|
|
7
|
+
|
|
8
|
+
use crate::bridge;
|
|
9
|
+
use crate::conversions::convert_ruby_completion_to_rust;
|
|
10
|
+
use magnus::{Error as MagnusError, Value as RValue};
|
|
11
|
+
use std::sync::Arc;
|
|
12
|
+
use tasker_shared::monitoring::ChannelMonitor;
|
|
13
|
+
use tasker_shared::{
|
|
14
|
+
events::{WorkerEventSubscriber, WorkerEventSystem},
|
|
15
|
+
types::{StepExecutionCompletionEvent, StepExecutionEvent},
|
|
16
|
+
};
|
|
17
|
+
use tasker_shared::{TaskerError, TaskerResult};
|
|
18
|
+
use tokio::sync::{broadcast, mpsc};
|
|
19
|
+
use tracing::{debug, error, info, warn};
|
|
20
|
+
|
|
21
|
+
/// Ruby Event Handler - forwards Rust events to Ruby via MPSC channel (TAS-51: bounded)
|
|
22
|
+
pub struct RubyEventHandler {
|
|
23
|
+
event_subscriber: Arc<WorkerEventSubscriber>,
|
|
24
|
+
worker_id: String,
|
|
25
|
+
event_sender: mpsc::Sender<StepExecutionEvent>,
|
|
26
|
+
channel_monitor: ChannelMonitor,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl RubyEventHandler {
|
|
30
|
+
/// Create new Ruby event handler with bounded channel
|
|
31
|
+
///
|
|
32
|
+
/// # Arguments
|
|
33
|
+
/// * `event_system` - Worker event system to subscribe to
|
|
34
|
+
/// * `worker_id` - Worker identifier
|
|
35
|
+
/// * `buffer_size` - MPSC channel buffer size (TAS-51: bounded channels)
|
|
36
|
+
///
|
|
37
|
+
/// # Note
|
|
38
|
+
/// TAS-51: Migrated from unbounded to bounded channel to provide backpressure.
|
|
39
|
+
/// Buffer size should come from: `config.mpsc_channels.shared.ffi.ruby_event_buffer_size`
|
|
40
|
+
pub fn new(
|
|
41
|
+
event_system: Arc<WorkerEventSystem>,
|
|
42
|
+
worker_id: String,
|
|
43
|
+
buffer_size: usize,
|
|
44
|
+
) -> (Self, mpsc::Receiver<StepExecutionEvent>) {
|
|
45
|
+
let event_subscriber = Arc::new(WorkerEventSubscriber::new((*event_system).clone()));
|
|
46
|
+
let (event_sender, event_receiver) = mpsc::channel(buffer_size);
|
|
47
|
+
|
|
48
|
+
// TAS-51: Initialize channel monitor for observability
|
|
49
|
+
let channel_monitor = ChannelMonitor::new("ruby_ffi_event_handler", buffer_size);
|
|
50
|
+
|
|
51
|
+
let handler = Self {
|
|
52
|
+
event_subscriber,
|
|
53
|
+
worker_id,
|
|
54
|
+
event_sender,
|
|
55
|
+
channel_monitor,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
(handler, event_receiver)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub async fn start(&self) -> TaskerResult<()> {
|
|
62
|
+
info!(
|
|
63
|
+
worker_id = %self.worker_id,
|
|
64
|
+
channel_monitor = %self.channel_monitor.channel_name(),
|
|
65
|
+
buffer_size = self.channel_monitor.buffer_size(),
|
|
66
|
+
"Starting Ruby event handler with channel monitoring"
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
let mut receiver = self.event_subscriber.subscribe_to_step_executions();
|
|
70
|
+
let event_sender = self.event_sender.clone();
|
|
71
|
+
// TAS-51: Clone channel monitor for observability in spawned task
|
|
72
|
+
let monitor = self.channel_monitor.clone();
|
|
73
|
+
|
|
74
|
+
tokio::spawn(async move {
|
|
75
|
+
loop {
|
|
76
|
+
match receiver.recv().await {
|
|
77
|
+
Ok(event) => {
|
|
78
|
+
debug!(
|
|
79
|
+
event_id = %event.event_id,
|
|
80
|
+
step_name = %event.payload.task_sequence_step.workflow_step.name,
|
|
81
|
+
"Received step execution event - sending to channel for Ruby processing"
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Send to channel instead of directly calling Ruby
|
|
85
|
+
match event_sender.send(event).await {
|
|
86
|
+
Ok(()) => {
|
|
87
|
+
// TAS-51: Record send and periodically check saturation (optimized)
|
|
88
|
+
if monitor.record_send_success() {
|
|
89
|
+
monitor.check_and_warn_saturation(event_sender.capacity());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
Err(e) => {
|
|
93
|
+
error!(
|
|
94
|
+
error = %e,
|
|
95
|
+
"Failed to send event to Ruby channel"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
Err(broadcast::error::RecvError::Lagged(count)) => {
|
|
101
|
+
warn!(lagged_count = count, "Ruby event handler lagged behind");
|
|
102
|
+
}
|
|
103
|
+
Err(broadcast::error::RecvError::Closed) => {
|
|
104
|
+
info!("Event channel closed - stopping Ruby event handler");
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
Ok(())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// This method is no longer used - events are sent through the channel instead
|
|
115
|
+
// Ruby will poll for events using the receiver returned from new()
|
|
116
|
+
|
|
117
|
+
/// Handle completion event from Ruby and forward to Rust event system
|
|
118
|
+
pub async fn handle_completion(
|
|
119
|
+
&self,
|
|
120
|
+
completion: StepExecutionCompletionEvent,
|
|
121
|
+
) -> TaskerResult<()> {
|
|
122
|
+
// Get the global event system
|
|
123
|
+
let event_system = crate::global_event_system::get_global_event_system();
|
|
124
|
+
|
|
125
|
+
// Publish completion to the event system
|
|
126
|
+
event_system
|
|
127
|
+
.publish_step_completion(completion)
|
|
128
|
+
.await
|
|
129
|
+
.map_err(|err| TaskerError::WorkerError(err.to_string()))?;
|
|
130
|
+
|
|
131
|
+
Ok(())
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Called by Ruby when step processing completes
|
|
136
|
+
/// This properly sends the completion event to the global event system
|
|
137
|
+
pub fn send_step_completion_event(completion_data: RValue) -> Result<(), MagnusError> {
|
|
138
|
+
// Convert Ruby completion to Rust event
|
|
139
|
+
let rust_completion = convert_ruby_completion_to_rust(completion_data).map_err(|err| {
|
|
140
|
+
error!("Could not convert magnus Value to StepExecutionCompletionEvent: {err}");
|
|
141
|
+
MagnusError::new(
|
|
142
|
+
magnus::exception::runtime_error(),
|
|
143
|
+
format!("Could not convert magnus Value to StepExecutionCompletionEvent: {err}"),
|
|
144
|
+
)
|
|
145
|
+
})?;
|
|
146
|
+
|
|
147
|
+
// Get the bridge handle to access the event handler
|
|
148
|
+
let handle_guard = bridge::WORKER_SYSTEM.lock().map_err(|err| {
|
|
149
|
+
error!("Could not acquire WORKER_SYSTEM handle lock: {err}");
|
|
150
|
+
MagnusError::new(
|
|
151
|
+
magnus::exception::runtime_error(),
|
|
152
|
+
format!("Could not acquire WORKER_SYSTEM handle lock: {err}"),
|
|
153
|
+
)
|
|
154
|
+
})?;
|
|
155
|
+
|
|
156
|
+
let handle = handle_guard.as_ref().ok_or_else(|| {
|
|
157
|
+
error!("Could not acquire WORKER_SYSTEM handle");
|
|
158
|
+
MagnusError::new(
|
|
159
|
+
magnus::exception::runtime_error(),
|
|
160
|
+
"Could not acquire WORKER_SYSTEM handle".to_string(),
|
|
161
|
+
)
|
|
162
|
+
})?;
|
|
163
|
+
|
|
164
|
+
// Use the event handler to publish the completion
|
|
165
|
+
handle.runtime_handle().block_on(async {
|
|
166
|
+
handle
|
|
167
|
+
.event_handler()
|
|
168
|
+
.handle_completion(rust_completion)
|
|
169
|
+
.await
|
|
170
|
+
.map_err(|err| {
|
|
171
|
+
error!("Unable to handle event completion: {err}");
|
|
172
|
+
MagnusError::new(
|
|
173
|
+
magnus::exception::runtime_error(),
|
|
174
|
+
format!("Unable to handle event completion: {err}"),
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
})?;
|
|
178
|
+
Ok(())
|
|
179
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
//! # TAS-65 Phase 2.4a: Ruby FFI Bindings for Domain Event Publishing
|
|
2
|
+
//!
|
|
3
|
+
//! Exposes `DomainEventPublisher` to Ruby for step handler event publishing.
|
|
4
|
+
//! Allows Ruby handlers to publish domain events with full execution context.
|
|
5
|
+
//!
|
|
6
|
+
//! ## Architecture
|
|
7
|
+
//!
|
|
8
|
+
//! This FFI layer accepts Ruby hashes that match our domain types and deserializes
|
|
9
|
+
//! them into proper Rust structs using `serde_magnus`. The key types are:
|
|
10
|
+
//!
|
|
11
|
+
//! - `EventPublishRequest`: Top-level FFI input containing all event data
|
|
12
|
+
//! - `TaskSequenceStep`: Full task/step context (reuses tasker_shared types)
|
|
13
|
+
//! - `StepExecutionResult`: Complete execution result (reuses tasker_shared types)
|
|
14
|
+
//! - `EventMetadataInput`: FFI-friendly metadata that converts to `EventMetadata`
|
|
15
|
+
|
|
16
|
+
use crate::bridge::WORKER_SYSTEM;
|
|
17
|
+
use chrono::{DateTime, Utc};
|
|
18
|
+
use magnus::{
|
|
19
|
+
function, prelude::*, Error as MagnusError, ExceptionClass, RModule, Ruby, Value as RValue,
|
|
20
|
+
};
|
|
21
|
+
use serde::Deserialize;
|
|
22
|
+
use serde_magnus::deserialize;
|
|
23
|
+
use tasker_shared::events::domain_events::{DomainEventPayload, EventMetadata};
|
|
24
|
+
use tasker_shared::messaging::execution_types::StepExecutionResult;
|
|
25
|
+
use tasker_shared::types::TaskSequenceStep;
|
|
26
|
+
use tracing::{debug, error};
|
|
27
|
+
use uuid::Uuid;
|
|
28
|
+
|
|
29
|
+
/// Helper to get RuntimeError exception class (magnus 0.8 API)
|
|
30
|
+
fn runtime_error_class() -> ExceptionClass {
|
|
31
|
+
Ruby::get()
|
|
32
|
+
.expect("Ruby runtime should be available")
|
|
33
|
+
.exception_runtime_error()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Helper to get ArgumentError exception class (magnus 0.8 API)
|
|
37
|
+
fn arg_error_class() -> ExceptionClass {
|
|
38
|
+
Ruby::get()
|
|
39
|
+
.expect("Ruby runtime should be available")
|
|
40
|
+
.exception_arg_error()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// FFI input structure for publishing domain events from Ruby
|
|
44
|
+
///
|
|
45
|
+
/// This struct is deserialized directly from a Ruby hash using serde_magnus.
|
|
46
|
+
/// All nested types use the same structures as tasker_shared for consistency.
|
|
47
|
+
#[derive(Debug, Deserialize)]
|
|
48
|
+
pub struct EventPublishRequest {
|
|
49
|
+
/// Event name in dot notation (e.g., "payment.processed")
|
|
50
|
+
pub event_name: String,
|
|
51
|
+
|
|
52
|
+
/// Full task sequence step context
|
|
53
|
+
pub task_sequence_step: TaskSequenceStep,
|
|
54
|
+
|
|
55
|
+
/// Complete step execution result
|
|
56
|
+
pub execution_result: StepExecutionResult,
|
|
57
|
+
|
|
58
|
+
/// Business-specific event payload
|
|
59
|
+
pub business_payload: serde_json::Value,
|
|
60
|
+
|
|
61
|
+
/// Event metadata for routing and correlation
|
|
62
|
+
pub metadata: EventMetadataInput,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// FFI-friendly event metadata input
|
|
66
|
+
///
|
|
67
|
+
/// Similar to `EventMetadata` but with String UUIDs for easier Ruby interop.
|
|
68
|
+
/// Converts to `EventMetadata` with proper UUID parsing.
|
|
69
|
+
#[derive(Debug, Deserialize)]
|
|
70
|
+
pub struct EventMetadataInput {
|
|
71
|
+
/// Task UUID as string
|
|
72
|
+
pub task_uuid: String,
|
|
73
|
+
|
|
74
|
+
/// Step UUID as string (optional)
|
|
75
|
+
pub step_uuid: Option<String>,
|
|
76
|
+
|
|
77
|
+
/// Step name (optional)
|
|
78
|
+
pub step_name: Option<String>,
|
|
79
|
+
|
|
80
|
+
/// Namespace for queue routing
|
|
81
|
+
pub namespace: String,
|
|
82
|
+
|
|
83
|
+
/// Correlation ID as string
|
|
84
|
+
pub correlation_id: String,
|
|
85
|
+
|
|
86
|
+
/// Handler name that fired the event
|
|
87
|
+
pub fired_by: String,
|
|
88
|
+
|
|
89
|
+
/// When the event was fired (optional, defaults to now)
|
|
90
|
+
#[serde(default = "Utc::now")]
|
|
91
|
+
pub fired_at: DateTime<Utc>,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
impl TryFrom<EventMetadataInput> for EventMetadata {
|
|
95
|
+
type Error = MagnusError;
|
|
96
|
+
|
|
97
|
+
fn try_from(input: EventMetadataInput) -> Result<Self, Self::Error> {
|
|
98
|
+
let task_uuid = parse_uuid(&input.task_uuid, "task_uuid")?;
|
|
99
|
+
let correlation_id = parse_uuid(&input.correlation_id, "correlation_id")?;
|
|
100
|
+
let step_uuid = input
|
|
101
|
+
.step_uuid
|
|
102
|
+
.map(|s| parse_uuid(&s, "step_uuid"))
|
|
103
|
+
.transpose()?;
|
|
104
|
+
|
|
105
|
+
Ok(EventMetadata {
|
|
106
|
+
task_uuid,
|
|
107
|
+
step_uuid,
|
|
108
|
+
step_name: input.step_name,
|
|
109
|
+
namespace: input.namespace,
|
|
110
|
+
correlation_id,
|
|
111
|
+
fired_at: input.fired_at,
|
|
112
|
+
fired_by: input.fired_by,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Parse a UUID string with a descriptive error message
|
|
118
|
+
fn parse_uuid(s: &str, field_name: &str) -> Result<Uuid, MagnusError> {
|
|
119
|
+
Uuid::parse_str(s).map_err(|e| {
|
|
120
|
+
MagnusError::new(
|
|
121
|
+
arg_error_class(),
|
|
122
|
+
format!("Invalid {} format: {}", field_name, e),
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// FFI function to publish a domain event from Ruby
|
|
128
|
+
///
|
|
129
|
+
/// # Arguments
|
|
130
|
+
///
|
|
131
|
+
/// A Ruby hash containing:
|
|
132
|
+
/// - `event_name`: String - Event name in dot notation (e.g., "payment.processed")
|
|
133
|
+
/// - `task_sequence_step`: Hash - Full TaskSequenceStep structure
|
|
134
|
+
/// - `execution_result`: Hash - Full StepExecutionResult structure
|
|
135
|
+
/// - `business_payload`: Hash - Business-specific event data
|
|
136
|
+
/// - `metadata`: Hash - Event metadata for routing and correlation
|
|
137
|
+
///
|
|
138
|
+
/// # Returns
|
|
139
|
+
///
|
|
140
|
+
/// String - The generated event_id (UUID v7)
|
|
141
|
+
///
|
|
142
|
+
/// # Ruby Example
|
|
143
|
+
///
|
|
144
|
+
/// ```ruby
|
|
145
|
+
/// TaskerCore::FFI.publish_domain_event({
|
|
146
|
+
/// event_name: "payment.processed",
|
|
147
|
+
/// task_sequence_step: task_sequence_step.to_h,
|
|
148
|
+
/// execution_result: execution_result.to_h,
|
|
149
|
+
/// business_payload: { transaction_id: "txn_123", amount: 100.00 },
|
|
150
|
+
/// metadata: {
|
|
151
|
+
/// task_uuid: task.task_uuid,
|
|
152
|
+
/// step_uuid: workflow_step.workflow_step_uuid,
|
|
153
|
+
/// step_name: workflow_step.name,
|
|
154
|
+
/// namespace: task.namespace_name,
|
|
155
|
+
/// correlation_id: task.task.correlation_id,
|
|
156
|
+
/// fired_by: handler_name
|
|
157
|
+
/// }
|
|
158
|
+
/// })
|
|
159
|
+
/// # => "0199c8f0-1234-7abc-9def-0123456789ab"
|
|
160
|
+
/// ```
|
|
161
|
+
pub fn publish_domain_event(event_params: RValue) -> Result<String, MagnusError> {
|
|
162
|
+
// Get Ruby handle for serde_magnus
|
|
163
|
+
let ruby = magnus::Ruby::get().map_err(|e| {
|
|
164
|
+
error!("Failed to acquire Ruby handle: {}", e);
|
|
165
|
+
MagnusError::new(
|
|
166
|
+
runtime_error_class(),
|
|
167
|
+
format!("Failed to acquire Ruby handle: {}", e),
|
|
168
|
+
)
|
|
169
|
+
})?;
|
|
170
|
+
|
|
171
|
+
// Deserialize the entire request using serde_magnus
|
|
172
|
+
let request: EventPublishRequest = deserialize(&ruby, event_params).map_err(|e| {
|
|
173
|
+
error!("Failed to deserialize event publish request: {}", e);
|
|
174
|
+
MagnusError::new(
|
|
175
|
+
arg_error_class(),
|
|
176
|
+
format!("Invalid event publish request: {}", e),
|
|
177
|
+
)
|
|
178
|
+
})?;
|
|
179
|
+
|
|
180
|
+
// Convert metadata input to EventMetadata
|
|
181
|
+
let metadata: EventMetadata = request.metadata.try_into()?;
|
|
182
|
+
|
|
183
|
+
debug!(
|
|
184
|
+
event_name = %request.event_name,
|
|
185
|
+
namespace = %metadata.namespace,
|
|
186
|
+
correlation_id = %metadata.correlation_id,
|
|
187
|
+
step_success = request.execution_result.success,
|
|
188
|
+
"Publishing domain event from Ruby FFI"
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Construct the domain event payload with full context
|
|
192
|
+
let domain_payload = DomainEventPayload {
|
|
193
|
+
task_sequence_step: request.task_sequence_step,
|
|
194
|
+
execution_result: request.execution_result,
|
|
195
|
+
payload: request.business_payload,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Get worker system to access domain event publisher
|
|
199
|
+
let handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
|
|
200
|
+
error!("Failed to acquire worker system lock: {}", e);
|
|
201
|
+
MagnusError::new(runtime_error_class(), "Lock acquisition failed")
|
|
202
|
+
})?;
|
|
203
|
+
|
|
204
|
+
let handle = handle_guard.as_ref().ok_or_else(|| {
|
|
205
|
+
MagnusError::new(
|
|
206
|
+
runtime_error_class(),
|
|
207
|
+
"Worker system not running - call bootstrap_worker first",
|
|
208
|
+
)
|
|
209
|
+
})?;
|
|
210
|
+
|
|
211
|
+
// Access the domain event publisher created during bootstrap
|
|
212
|
+
let event_publisher = &handle.domain_event_publisher;
|
|
213
|
+
|
|
214
|
+
// Publish event using the async runtime
|
|
215
|
+
let runtime_handle = handle.runtime_handle();
|
|
216
|
+
let event_id = runtime_handle
|
|
217
|
+
.block_on(event_publisher.publish_event(&request.event_name, domain_payload, metadata))
|
|
218
|
+
.map_err(|e| {
|
|
219
|
+
error!("Failed to publish domain event: {}", e);
|
|
220
|
+
MagnusError::new(
|
|
221
|
+
runtime_error_class(),
|
|
222
|
+
format!("Event publication failed: {}", e),
|
|
223
|
+
)
|
|
224
|
+
})?;
|
|
225
|
+
|
|
226
|
+
debug!(
|
|
227
|
+
event_id = %event_id,
|
|
228
|
+
event_name = %request.event_name,
|
|
229
|
+
"Domain event published successfully from Ruby"
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
Ok(event_id.to_string())
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Initialize the event publisher FFI module
|
|
236
|
+
pub fn init_event_publisher_ffi(module: &RModule) -> Result<(), MagnusError> {
|
|
237
|
+
module.define_singleton_method("publish_domain_event", function!(publish_domain_event, 1))?;
|
|
238
|
+
Ok(())
|
|
239
|
+
}
|