use std::sync::Arc;
use std::time::Instant;
use rskit_errors::AppResult;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::DatasetItem;
use crate::manifest::{CacheStatus, Manifest};
use crate::sink::ItemSink;
use crate::source::Source;
use crate::target::Target;
use crate::transform::Transform;
use crate::validate::Validator;
use super::config::CollectorConfig;
use super::event::{WorkerEvent, handle_drained_worker_event, handle_worker_event};
use super::progress::ProgressCallback;
use super::result::CollectorResult;
use super::worker::{WorkItem, WorkerContext, load_manifest, save_manifest, worker_loop};
pub struct Collector<T: DatasetItem> {
sources: Vec<Box<dyn Source<T>>>,
targets: Vec<Box<dyn Target>>,
transforms: Arc<Vec<Box<dyn Transform<T, T>>>>,
sink: Arc<dyn ItemSink<T>>,
validator: Option<Arc<dyn Validator<T>>>,
config: CollectorConfig,
progress: Box<dyn ProgressCallback>,
}
impl<T: DatasetItem> Collector<T> {
pub fn new(
sources: Vec<Box<dyn Source<T>>>,
transforms: Vec<Box<dyn Transform<T, T>>>,
sink: Arc<dyn ItemSink<T>>,
targets: Vec<Box<dyn Target>>,
config: CollectorConfig,
progress: Box<dyn ProgressCallback>,
) -> Self {
Self {
sources,
targets,
transforms: Arc::new(transforms),
sink,
validator: None,
config,
progress,
}
}
#[must_use]
pub fn with_validator(mut self, validator: Arc<dyn Validator<T>>) -> Self {
self.validator = Some(validator);
self
}
pub async fn run(self, cancel: &CancellationToken) -> AppResult<CollectorResult> {
let start = Instant::now();
let cfg = &self.config;
let out = &cfg.output_dir;
self.sink.prepare().await?;
let mut result = CollectorResult {
output_dir: out.clone(),
..Default::default()
};
let total_sources = self.sources.len();
let mut manifest = if cfg.force {
Manifest::default()
} else {
load_manifest(out.to_path_buf()).await?
};
let mut sources_to_fetch: Vec<WorkItem<T>> = Vec::new();
for (idx, mut source) in self.sources.into_iter().enumerate() {
if !cfg.force {
let cache_key = source.cache_key();
match manifest.cache_status(source.name(), &cache_key, source.max_items()) {
CacheStatus::Done(stats) => {
result.total_items += stats.total;
result.real_count += stats.real;
result.ai_count += stats.ai;
result
.source_stats
.insert(source.name().to_string(), stats.clone());
result.cached_sources.push(source.name().to_string());
self.progress.on_source_cached(idx, source.name(), &stats);
tracing::debug!(
source = source.name(),
total = stats.total,
"cached, skipping"
);
continue;
}
CacheStatus::Partial(stats) => {
tracing::debug!(
source = source.name(),
total = stats.total,
offset = stats.fetched_offset,
"partial cache, resuming"
);
source.set_resume_state(stats.fetched_offset, stats.total);
sources_to_fetch.push((idx, source, Some(stats)));
}
CacheStatus::NotCached => {
sources_to_fetch.push((idx, source, None));
}
}
} else {
sources_to_fetch.push((idx, source, None));
}
}
if !sources_to_fetch.is_empty() {
let (work_tx, work_rx) = mpsc::channel::<WorkItem<T>>(total_sources);
let work_rx = Arc::new(tokio::sync::Mutex::new(work_rx));
let event_capacity = cfg
.limits
.stream_buffer
.max(total_sources.saturating_mul(4))
.max(1);
let (event_tx, mut event_rx) = mpsc::channel::<WorkerEvent>(event_capacity);
let num_workers = cfg.concurrency.max(1).min(sources_to_fetch.len());
let mut worker_handles = Vec::with_capacity(num_workers);
for _ in 0..num_workers {
let ctx = WorkerContext {
transforms: self.transforms.clone(),
sink: self.sink.clone(),
validator: self.validator.clone(),
timeout_secs: cfg.source_timeout_secs,
limits: cfg.limits,
cancel: cancel.clone(),
event_tx: event_tx.clone(),
};
let rx = work_rx.clone();
worker_handles.push(tokio::spawn(worker_loop(rx, ctx)));
}
drop(event_tx);
for (idx, source, resume_stats) in sources_to_fetch {
if work_tx.send((idx, source, resume_stats)).await.is_err() {
break;
}
}
drop(work_tx);
let mut completed_count = 0usize;
let mut cancellation_seen = false;
loop {
tokio::select! {
event = event_rx.recv() => {
match event {
Some(event) => {
let (completed, should_save) = handle_worker_event(
event,
&mut result,
&mut manifest,
self.progress.as_ref(),
);
if should_save
&& let Err(e) =
save_manifest(out.to_path_buf(), manifest.clone()).await
{
tracing::warn!(error = %e, "failed to save manifest");
}
if completed {
completed_count += 1;
}
}
None => break, }
}
_ = cancel.cancelled(), if !cancellation_seen => {
tracing::debug!(completed = completed_count, "cancelled, waiting for workers");
cancellation_seen = true;
}
}
}
for handle in worker_handles {
let _ = handle.await;
}
while let Ok(event) = event_rx.try_recv() {
handle_drained_worker_event(
event,
&mut result,
&mut manifest,
self.progress.as_ref(),
);
}
}
self.sink.finish().await?;
for target in &self.targets {
if cancel.is_cancelled() {
break;
}
self.progress.on_publish_start(target.name());
tracing::debug!(target = target.name(), "publishing");
match target.publish(out, None).await {
Ok(pub_result) => {
self.progress.on_publish_done(target.name(), &pub_result);
result.publish_results.push(pub_result);
}
Err(e) => {
self.progress
.on_publish_error(target.name(), &e.to_string());
tracing::error!(target = target.name(), error = %e, "publish failed");
}
}
}
result.duration_seconds = start.elapsed().as_secs_f64();
save_manifest(out.to_path_buf(), manifest).await?;
tracing::debug!(
total = result.total_items,
real = result.real_count,
ai = result.ai_count,
duration = format!("{:.1}s", result.duration_seconds),
"collection complete"
);
Ok(result)
}
}