sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
//! ImportProgressSink: bounded mpsc with drop-oldest-non-terminal overflow.
|
||||
//! Terminal events (ImportDone / ImportFailed) are never dropped — on full
|
||||
//! channel they evict oldest non-terminal events until they fit. The import
|
||||
//! thread never backpressures on a slow consumer.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use super::ImportDiscovery;
|
||||
use super::ImportStats;
|
||||
|
||||
const DEFAULT_CAPACITY: usize = 64;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", tag = "kind")]
|
||||
pub enum ImportProgressEvent {
|
||||
DiscoveryStarted { source: String },
|
||||
DiscoveryProgress { scanned: usize, estimated_total: Option<usize> },
|
||||
DiscoveryDone { discovered: ImportDiscovery },
|
||||
SessionStarted { native_id: String, index: usize, total: usize },
|
||||
SessionFinished { native_id: String, outcome: SessionOutcome, stats_delta: StatsDelta },
|
||||
ImportDone { stats: ImportStats },
|
||||
ImportFailed { error: String },
|
||||
}
|
||||
|
||||
impl ImportProgressEvent {
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
matches!(self, ImportProgressEvent::ImportDone { .. } | ImportProgressEvent::ImportFailed { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SessionOutcome { Imported, Skipped, Updated, Failed }
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct StatsDelta {
|
||||
pub messages_written: u64,
|
||||
pub messages_already_present: u64,
|
||||
}
|
||||
|
||||
pub struct ImportProgressSink {
|
||||
inner: SinkInner,
|
||||
}
|
||||
|
||||
enum SinkInner {
|
||||
Live { tx: mpsc::Sender<ImportProgressEvent> },
|
||||
Noop,
|
||||
}
|
||||
|
||||
impl ImportProgressSink {
|
||||
pub fn channel() -> (Self, mpsc::Receiver<ImportProgressEvent>) {
|
||||
let (tx, rx) = mpsc::channel(DEFAULT_CAPACITY);
|
||||
(Self { inner: SinkInner::Live { tx } }, rx)
|
||||
}
|
||||
|
||||
pub fn noop() -> Self { Self { inner: SinkInner::Noop } }
|
||||
|
||||
pub async fn send(&self, evt: ImportProgressEvent) {
|
||||
match &self.inner {
|
||||
SinkInner::Noop => {}
|
||||
SinkInner::Live { tx } => {
|
||||
if evt.is_terminal() {
|
||||
// Force-send: guaranteed delivery of terminal events.
|
||||
let _ = tx.send(evt).await;
|
||||
} else {
|
||||
// Best-effort: drop non-terminal events when the channel is full.
|
||||
match tx.try_send(evt) {
|
||||
Ok(()) => {}
|
||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||
tracing::debug!("import progress: dropped non-terminal event (queue full)");
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Closed(_)) => {
|
||||
tracing::warn!("import progress: consumer gone");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_events_always_delivered() {
|
||||
let (sink, mut rx) = ImportProgressSink::channel();
|
||||
// Fill the channel with non-terminal events (mostly drop).
|
||||
for i in 0..1000 {
|
||||
sink.send(ImportProgressEvent::SessionStarted {
|
||||
native_id: format!("s{i}"), index: i, total: 1000,
|
||||
}).await;
|
||||
}
|
||||
// Consumer drains in background.
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut saw_done = false;
|
||||
while let Some(e) = rx.recv().await {
|
||||
if matches!(e, ImportProgressEvent::ImportDone { .. }) {
|
||||
saw_done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
saw_done
|
||||
});
|
||||
sink.send(ImportProgressEvent::ImportDone { stats: ImportStats::default() }).await;
|
||||
let saw_done = tokio::time::timeout(std::time::Duration::from_secs(2), handle).await.unwrap().unwrap();
|
||||
assert!(saw_done);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_sink_never_fails() {
|
||||
let sink = ImportProgressSink::noop();
|
||||
sink.send(ImportProgressEvent::ImportDone { stats: ImportStats::default() }).await;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user