use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::{Semaphore, mpsc};
use crate::config::TraceStorageMode;
use crate::metrics;
use crate::storage::models;
use crate::storage::repositories::trace_dlq::TraceDlqRepository;
use crate::storage::repositories::traces::TraceRepository;
use super::QueuedItem;
fn serialize_result_with_profile(
message: &dataflow_rs::Message,
profile: Option<&Arc<crate::engine::profile::ProfileCollector>>,
) -> Result<String, serde_json::Error> {
let mut v = serde_json::to_value(message)?;
if let Some(p) = profile
&& let Some(obj) = v.as_object_mut()
{
obj.insert(
"_orion".to_string(),
serde_json::json!({ "profile": p.to_json() }),
);
}
serde_json::to_string(&v)
}
pub(super) struct QueueCounters {
pub(super) pending: Arc<AtomicUsize>,
pub(super) active: Arc<AtomicUsize>,
pub(super) memory_bytes: Arc<AtomicUsize>,
}
pub(super) struct DispatcherContext {
pub(super) max_workers: usize,
pub(super) shutdown_timeout_secs: u64,
pub(super) counters: QueueCounters,
pub(super) processing: ProcessingContext,
}
#[derive(Clone)]
pub(super) struct ProcessingContext {
pub(super) engine: Arc<crate::engine::EngineHandle>,
pub(super) trace_repo: Arc<dyn TraceRepository>,
pub(super) dlq_repo: Option<Arc<dyn TraceDlqRepository>>,
pub(super) processing_timeout_ms: u64,
pub(super) max_result_size_bytes: usize,
pub(super) dlq_max_retries: i64,
pub(super) rollout_sticky_header: std::sync::Arc<str>,
pub(super) channel_registry: Arc<crate::channel::ChannelRegistry>,
pub(super) persistence_queue: crate::queue::TracePersistenceQueue,
pub(super) global_trace_storage: crate::config::TraceStorageConfig,
}
struct DlqCandidate<'a> {
trace_id: &'a str,
channel: &'a str,
payload: &'a serde_json::Value,
metadata: &'a serde_json::Value,
retry_count: i64,
}
pub(super) async fn dispatcher_loop(mut rx: mpsc::Receiver<QueuedItem>, ctx: DispatcherContext) {
let semaphore = Arc::new(Semaphore::new(ctx.max_workers));
while let Some(item) = rx.recv().await {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break, };
let estimated_size = item.payload_size;
let pending = ctx
.counters
.pending
.fetch_sub(1, Ordering::Relaxed)
.saturating_sub(1);
metrics::set_trace_queue_depth(pending as f64);
let active = ctx.counters.active.fetch_add(1, Ordering::Relaxed) + 1;
metrics::set_trace_workers_active(active as f64);
let processing = ctx.processing.clone();
let active_counter = ctx.counters.active.clone();
let memory_counter = ctx.counters.memory_bytes.clone();
tokio::spawn(async move {
let _permit = permit; process_trace(item, processing).await;
let active = active_counter
.fetch_sub(1, Ordering::Relaxed)
.saturating_sub(1);
metrics::set_trace_workers_active(active as f64);
let mem = memory_counter
.fetch_sub(estimated_size, Ordering::Relaxed)
.saturating_sub(estimated_size);
metrics::set_trace_queue_memory_bytes(mem as f64);
});
}
if tokio::time::timeout(
Duration::from_secs(ctx.shutdown_timeout_secs),
semaphore.acquire_many(ctx.max_workers as u32),
)
.await
.is_err()
{
tracing::warn!("Timed out waiting for in-flight traces to complete");
}
tracing::info!("Trace queue workers shut down");
}
impl ProcessingContext {
async fn set_trace_status(
&self,
mode: crate::config::TraceStorageMode,
trace_id: &str,
status: &str,
message: Option<&str>,
) {
match mode {
TraceStorageMode::Sync => {
if let Err(e) = self
.trace_repo
.update_status(trace_id, status, message)
.await
{
tracing::error!(trace_id = %trace_id, error = %e, "Failed to update trace status to {}", status);
}
}
TraceStorageMode::Async | TraceStorageMode::Batch => {
self.persistence_queue
.submit(crate::queue::TracePersistenceTask::UpdateStatus {
id: trace_id.to_string(),
status: status.to_string(),
error_message: message.map(str::to_string),
})
.await;
}
TraceStorageMode::Off => {}
}
}
}
async fn route_set_result(
mode: crate::config::TraceStorageMode,
persistence_queue: &crate::queue::TracePersistenceQueue,
trace_id: &str,
result_json: String,
duration_ms: f64,
task_trace_json: Option<String>,
) {
match mode {
TraceStorageMode::Async | TraceStorageMode::Batch => {
persistence_queue
.submit(crate::queue::TracePersistenceTask::SetResult(
crate::storage::repositories::traces::TraceResultRow {
id: trace_id.to_string(),
result_json,
duration_ms,
task_trace_json,
},
))
.await;
}
TraceStorageMode::Sync | TraceStorageMode::Off => {}
}
}
#[tracing::instrument(skip_all, fields(trace_id = %item.msg.trace_id, channel = %item.msg.channel))]
async fn process_trace(item: QueuedItem, ctx: ProcessingContext) {
let QueuedItem {
mut msg,
dlq_retry_count,
payload_size: _,
} = item;
let _backpressure_permit = msg.backpressure_permit.take();
let (channel_runtime, quarantine_reason) =
match ctx.channel_registry.require_serviceable(&msg.channel) {
Ok(runtime) => (runtime, None),
Err(e) => (None, Some(e.to_string())),
};
let channel_registered = channel_runtime.is_some();
let timeout_ms = crate::channel::guards::effective_timeout_ms(
&channel_runtime,
Some(ctx.processing_timeout_ms),
Some(ctx.processing_timeout_ms),
)
.unwrap_or(ctx.processing_timeout_ms);
let effective_trace = channel_runtime
.map(|c| c.trace_storage)
.unwrap_or_else(|| {
crate::channel::registry::EffectiveTraceConfig::resolve(&ctx.global_trace_storage, None)
})
.for_async_submission();
let trace_mode = effective_trace.mode;
let _cx = crate::server::trace_context::set_parent_from_map(&msg.trace_headers);
let trace_id = msg.trace_id;
let channel = msg.channel;
let metrics_channel = if channel_registered {
channel.as_str()
} else {
"_unknown"
};
let profile = msg
.profile_requested
.then(crate::engine::profile::ProfileCollector::new);
let start = Instant::now();
let dlq = DlqCandidate {
trace_id: &trace_id,
channel: &channel,
payload: &msg.payload,
metadata: &msg.metadata,
retry_count: dlq_retry_count,
};
if let Some(reason) = quarantine_reason {
metrics::record_message(metrics_channel, "error");
metrics::record_error("channel_quarantined");
handle_failure(&ctx, trace_mode, &dlq, &reason).await;
return;
}
if !mark_running(&ctx, trace_mode, &dlq).await {
return;
}
let sticky_identity =
crate::engine::utils::rollout_identity(&msg.metadata, &ctx.rollout_sticky_header);
let mut message = dataflow_rs::Message::builder()
.payload_json(&msg.payload)
.metadata_json(&msg.metadata)
.routing_bucket(crate::engine::utils::rollout_bucket_for_identity(
sticky_identity,
))
.build();
let engine_ref = ctx.engine.load();
let capture = effective_trace
.task_details
.then_some(crate::engine::TraceCapture {
max_snapshot_bytes: ctx.max_result_size_bytes,
});
let processing_timeout_ms = timeout_ms;
let workflow_start = Instant::now();
let timeout_outcome = crate::engine::run_for_channel(
&engine_ref,
&channel,
&mut message,
Some(processing_timeout_ms),
profile.as_ref(),
capture,
)
.await;
if let Some(ref p) = profile {
p.set_workflow_total(workflow_start.elapsed());
}
let (result, task_trace) = match timeout_outcome {
Ok(inner) => inner,
Err(_) => {
tracing::warn!(
trace_id = %trace_id,
channel = %channel,
timeout_ms = processing_timeout_ms,
"Async trace processing timed out"
);
(
Err(dataflow_rs::DataflowError::Timeout(format!(
"Processing timed out after {processing_timeout_ms}ms"
))),
None,
)
}
};
let task_trace_json = crate::engine::utils::serialize_task_trace_capped(
task_trace.as_ref(),
ctx.max_result_size_bytes,
&trace_id,
);
let duration = start.elapsed();
let duration_secs = duration.as_secs_f64();
let duration_ms = duration.as_secs_f64() * 1000.0;
let result = match result {
Ok(()) if message.has_errors() => {
let summary = message
.errors()
.iter()
.map(|e| format!("{}: {}", e.code, e.message))
.collect::<Vec<_>>()
.join("; ");
Err(dataflow_rs::DataflowError::Workflow(summary))
}
other => other,
};
match result {
Ok(()) => {
metrics::record_message(metrics_channel, "ok");
metrics::record_message_duration(metrics_channel, duration_secs);
persist_success(
&ctx,
&effective_trace,
&trace_id,
&message,
profile.as_ref(),
duration_ms,
task_trace_json,
)
.await;
}
Err(e) => {
metrics::record_message(metrics_channel, "error");
metrics::record_error("engine");
handle_failure(&ctx, trace_mode, &dlq, &e.to_string()).await;
}
}
}
async fn mark_running(
ctx: &ProcessingContext,
trace_mode: TraceStorageMode,
dlq: &DlqCandidate<'_>,
) -> bool {
let trace_id = dlq.trace_id;
if matches!(trace_mode, TraceStorageMode::Sync) {
if let Err(e) = ctx
.trace_repo
.update_status(trace_id, models::TRACE_STATUS_RUNNING, None)
.await
{
tracing::error!(
trace_id = %trace_id,
error = %e,
"Failed to update trace status to running — routing to DLQ"
);
metrics::record_error("trace_status_write");
enqueue_dlq_row(
&ctx.dlq_repo,
dlq,
&format!("Failed to mark trace running: {e}"),
ctx.dlq_max_retries,
)
.await;
let _ = ctx
.trace_repo
.update_status(
trace_id,
models::TRACE_STATUS_FAILED,
Some("Could not start processing; routed to DLQ"),
)
.await;
return false;
}
} else {
ctx.set_trace_status(trace_mode, trace_id, models::TRACE_STATUS_RUNNING, None)
.await;
}
true
}
async fn persist_success(
ctx: &ProcessingContext,
effective_trace: &crate::channel::registry::EffectiveTraceConfig,
trace_id: &str,
message: &dataflow_rs::Message,
profile: Option<&Arc<crate::engine::profile::ProfileCollector>>,
duration_ms: f64,
task_trace_json: Option<String>,
) {
let trace_mode = effective_trace.mode;
let result_json = match serialize_result_with_profile(message, profile) {
Ok(json) => json,
Err(e) => {
tracing::error!(trace_id = %trace_id, error = %e, "Failed to serialize trace result");
ctx.set_trace_status(
trace_mode,
trace_id,
models::TRACE_STATUS_FAILED,
Some(&format!("Result serialization failed: {e}")),
)
.await;
return;
}
};
if ctx.max_result_size_bytes > 0 && result_json.len() > ctx.max_result_size_bytes {
tracing::warn!(
trace_id = %trace_id,
result_bytes = result_json.len(),
limit_bytes = ctx.max_result_size_bytes,
"Trace result exceeds size limit"
);
metrics::record_error("result_size_exceeded");
ctx.set_trace_status(
trace_mode,
trace_id,
models::TRACE_STATUS_FAILED,
Some(&format!(
"Result size {} bytes exceeds limit of {} bytes",
result_json.len(),
ctx.max_result_size_bytes
)),
)
.await;
return;
}
let should_persist_result =
match effective_trace.should_drop(false, effective_trace.draw_sample()) {
Some(reason) => {
metrics::record_trace_dropped(reason);
false
}
None => true,
};
let result_saved = if !should_persist_result {
true
} else if matches!(trace_mode, TraceStorageMode::Sync) {
match crate::queue::trace_persistence::with_write_retries(|| async {
ctx.trace_repo
.set_result(
trace_id,
&result_json,
duration_ms,
task_trace_json.as_deref(),
)
.await
})
.await
{
Ok(_) => true,
Err(e) => {
tracing::warn!(
trace_id = %trace_id,
error = %e,
"Failed to save trace result, giving up after the bounded retries"
);
false
}
}
} else {
route_set_result(
trace_mode,
&ctx.persistence_queue,
trace_id,
result_json,
duration_ms,
task_trace_json,
)
.await;
true
};
if result_saved {
ctx.set_trace_status(trace_mode, trace_id, models::TRACE_STATUS_COMPLETED, None)
.await;
} else {
tracing::error!(trace_id = %trace_id, "Failed to save trace result after 3 attempts, marking as failed");
ctx.set_trace_status(
trace_mode,
trace_id,
models::TRACE_STATUS_FAILED,
Some("Result persistence failed after retries"),
)
.await;
}
}
async fn handle_failure(
ctx: &ProcessingContext,
trace_mode: TraceStorageMode,
dlq: &DlqCandidate<'_>,
error_str: &str,
) {
ctx.set_trace_status(
trace_mode,
dlq.trace_id,
models::TRACE_STATUS_FAILED,
Some(error_str),
)
.await;
enqueue_dlq_row(&ctx.dlq_repo, dlq, error_str, ctx.dlq_max_retries).await;
}
async fn enqueue_dlq_row(
dlq_repo: &Option<Arc<dyn TraceDlqRepository>>,
candidate: &DlqCandidate<'_>,
error_str: &str,
dlq_max_retries: i64,
) {
let Some(dlq) = dlq_repo else { return };
let Ok(payload) = serde_json::to_string(candidate.payload) else {
return;
};
let metadata = serde_json::to_string(candidate.metadata).ok();
let metadata = metadata.as_deref().unwrap_or("{}");
let trace_id = candidate.trace_id;
let dlq_retry_count = candidate.retry_count;
let exhausted = dlq_retry_count >= dlq_max_retries;
if let Err(dlq_err) = dlq
.enqueue(
trace_id,
candidate.channel,
&payload,
metadata,
error_str,
dlq_retry_count,
dlq_max_retries,
)
.await
{
tracing::error!(
trace_id = %trace_id,
error = %dlq_err,
"Failed to enqueue failed trace to DLQ"
);
} else if exhausted {
metrics::record_trace_dlq_retry("exhausted");
tracing::warn!(
trace_id = %trace_id,
retry_count = dlq_retry_count,
max_retries = dlq_max_retries,
"Failed trace exhausted its DLQ retries, no further attempts"
);
} else {
tracing::info!(
trace_id = %trace_id,
retry_count = dlq_retry_count,
"Failed trace enqueued to DLQ for retry"
);
}
}