1pub(crate) mod metrics;
13pub(crate) mod pipeline_metrics;
14pub mod processors;
16
17use crate::dump::{DumpError, DumpReceipt, DumpRequest, Lookback};
18use crate::fs::{EpochWindow, Fs, RemoveReason, TakenFiles, TakenSegment};
19use crate::pipeline::{ProcessErrorKind, SegmentData, SegmentProcessor};
20use crate::rate_limit::rate_limited;
21use crate::sealed::{self, SegmentRef};
22use crate::worker::metrics::{Operation, SegmentProcessMetrics, WorkerCycleMetrics};
23use crate::worker::pipeline_metrics::{MetriqueResult, PipelineMetrics, StageMetrics};
24use futures_util::FutureExt;
25use metrique::timers::Timer;
26use metrique::writer::BoxEntrySink;
27use std::collections::HashMap;
28use std::io;
29use std::path::{Path, PathBuf};
30use std::sync::Arc;
31use std::time::{Duration, SystemTime, UNIX_EPOCH};
32
33pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(1);
34
35#[derive(bon::Builder)]
40#[builder(on(String, into))]
41pub struct BackgroundTaskConfig {
42 #[builder(into)]
44 trace_dir: Option<PathBuf>,
45 trace_stem: Option<String>,
47 #[builder(default = DEFAULT_POLL_INTERVAL)]
49 poll_interval: Duration,
50 #[builder(default)]
52 processors: Vec<Box<dyn SegmentProcessor>>,
53 #[builder(default = metrique::writer::sink::DevNullSink::boxed())]
55 metrics_sink: BoxEntrySink,
56 trigger: Option<crate::dump::DumpRx>,
61}
62
63impl std::fmt::Debug for BackgroundTaskConfig {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.debug_struct("BackgroundTaskConfig")
66 .field("trace_dir", &self.trace_dir)
67 .field("trace_stem", &self.trace_stem)
68 .field("poll_interval", &self.poll_interval)
69 .finish_non_exhaustive()
70 }
71}
72
73impl BackgroundTaskConfig {
74 pub fn poll_interval(&self) -> Duration {
76 self.poll_interval
77 }
78
79 pub fn trace_dir(&self) -> &Path {
81 match self.trace_dir.as_deref() {
82 Some(dir) if !dir.as_os_str().is_empty() => dir,
83 _ => Path::new("."),
84 }
85 }
86
87 pub fn trace_stem(&self) -> &str {
90 self.trace_stem.as_deref().unwrap_or("trace")
91 }
92}
93
94pub(crate) fn run_background_task(
100 mut config: BackgroundTaskConfig,
101 shutdown: tokio::sync::oneshot::Receiver<Duration>,
102 fs: Arc<Fs>,
103) {
104 let rt = tokio::runtime::Builder::new_current_thread()
105 .thread_name("dial9-worker-rt")
106 .enable_all()
107 .build()
108 .expect("failed to create worker runtime");
109
110 let processors = std::mem::take(&mut config.processors);
111 let metrics_sink = config.metrics_sink.clone();
112 let trigger = config.trigger.take();
113
114 tracing::info!(target: "dial9_worker", dir = %config.trace_dir().display(), stem = %config.trace_stem(), processors = processors.len(), triggered = trigger.is_some(), "worker started");
115 rt.block_on(async {
116 let stop = tokio_util::sync::CancellationToken::new();
117 let worker_stop = stop.clone();
118 let worker = async move {
119 let mut worker = WorkerLoop::new(
120 fs,
121 config.poll_interval(),
122 processors,
123 worker_stop,
124 metrics_sink,
125 trigger,
126 )
127 .await?;
128 worker.run().await;
129 io::Result::Ok(())
130 };
131 let mut run_fut =
132 std::pin::pin!(std::panic::AssertUnwindSafe(worker).catch_unwind());
133 let drain_timeout = tokio::select! {
135 result = &mut run_fut => {
136 match result {
137 Ok(Ok(())) => {}
138 Ok(Err(error)) => {
139 tracing::error!(target: "dial9_worker", %error, "worker initialization failed");
140 }
141 Err(_) => {
142 tracing::error!(target: "dial9_worker", "worker panicked");
143 }
144 }
145 return;
146 }
147 msg = shutdown => msg.unwrap_or(Duration::ZERO),
148 };
149 tracing::info!(target: "dial9_worker", ?drain_timeout, "stop signal received, draining");
150 stop.cancel();
152 match tokio::time::timeout(drain_timeout, run_fut).await {
154 Ok(Ok(Ok(()))) => tracing::info!(target: "dial9_worker", "drain complete"),
155 Ok(Ok(Err(error))) => {
156 tracing::error!(target: "dial9_worker", %error, "worker initialization failed");
157 }
158 Ok(Err(_)) => tracing::error!(target: "dial9_worker", "worker panicked"),
159 Err(_) => tracing::warn!(target: "dial9_worker", "drain timed out"),
160 }
161 });
162 tracing::info!(target: "dial9_worker", "worker stopped");
163}
164
165pub(crate) fn spawn<M, Init, Teardown>(
173 writer: &crate::buffer::SegmentWriter<M>,
174 config: BackgroundTaskConfig,
175 shutdown: tokio::sync::oneshot::Receiver<Duration>,
176 thread_init: Init,
177) -> Option<crate::primitives::thread::JoinHandle<()>>
178where
179 M: crate::buffer::BufferMode,
180 Init: FnOnce() -> Teardown + Send + 'static,
181 Teardown: FnOnce(),
182{
183 let fs = writer.fs_handle()?;
184 Some(crate::primitives::thread::spawn_named(
185 "dial9-worker",
186 move || {
187 let teardown = thread_init();
188 run_background_task(config, shutdown, fs);
189 teardown();
190 },
191 ))
192}
193
194pub(crate) struct WorkerLoop {
198 fs: Arc<Fs>,
199 poll_interval: Duration,
200 processors: Vec<Box<dyn SegmentProcessor>>,
201 metrics_sink: BoxEntrySink,
202 stop: tokio_util::sync::CancellationToken,
205 trigger: Option<crate::dump::DumpRx>,
208 epoch_cache: HashMap<u32, (u64, u64)>,
214}
215
216struct ActiveDump {
219 id: crate::dump::DumpId,
220 triggered_at: SystemTime,
221 window: EpochWindow,
222 deadline: Option<tokio::time::Instant>,
225 metadata: Vec<(String, String)>,
226 receipt_tx: Option<tokio::sync::oneshot::Sender<Result<DumpReceipt, DumpError>>>,
227 segments_processed: usize,
228 first_epoch: Option<u64>,
229 last_epoch: Option<u64>,
230 first_error: Option<ProcessErrorKind>,
231}
232
233impl ActiveDump {
234 fn register(req: DumpRequest) -> Self {
235 let trigger_epoch = req
236 .triggered_at
237 .duration_since(UNIX_EPOCH)
238 .unwrap_or_default()
239 .as_secs();
240 let start_secs = match req.lookback {
241 Lookback::Unbounded => None,
242 Lookback::Window(d) => Some(trigger_epoch.saturating_sub(d.as_secs())),
243 };
244 let window = EpochWindow {
245 start_secs,
246 end_secs: trigger_epoch.saturating_add(req.lookforward.as_secs()),
247 };
248 let deadline = (!req.lookforward.is_zero()).then(|| {
249 let elapsed = req.triggered_at.elapsed().unwrap_or_default();
252 tokio::time::Instant::now() + req.lookforward.saturating_sub(elapsed)
253 });
254 Self {
255 id: req.id,
256 triggered_at: req.triggered_at,
257 window,
258 deadline,
259 metadata: req.metadata,
260 receipt_tx: Some(req.receipt_tx),
261 segments_processed: 0,
262 first_epoch: None,
263 last_epoch: None,
264 first_error: None,
265 }
266 }
267
268 fn due(&self, now: tokio::time::Instant) -> bool {
271 self.deadline.is_none_or(|d| now >= d)
272 }
273
274 fn time_range(&self) -> (SystemTime, SystemTime) {
277 match (self.first_epoch, self.last_epoch) {
278 (Some(first), Some(last)) => (epoch_to_system(first), epoch_to_system(last)),
279 _ => (self.triggered_at, self.triggered_at),
280 }
281 }
282
283 fn failed(&self) -> bool {
287 self.first_error.is_some() && self.segments_processed == 0
288 }
289
290 fn completion(&self) -> crate::dump::DumpCompletion {
292 crate::dump::DumpCompletion {
293 dump_id: self.id,
294 triggered_at: self.triggered_at,
295 time_range: self.time_range(),
296 segments_processed: self.segments_processed,
297 metadata: self.metadata.clone(),
298 failed: self.failed(),
299 }
300 }
301
302 fn into_result(
305 mut self,
306 manifest_key: Option<String>,
307 ) -> (
308 tokio::sync::oneshot::Sender<Result<DumpReceipt, DumpError>>,
309 Result<DumpReceipt, DumpError>,
310 ) {
311 let tx = self
312 .receipt_tx
313 .take()
314 .expect("receipt_tx only taken at resolution");
315 let result = match (self.failed(), self.first_error.take()) {
316 (true, Some(kind)) => Err(DumpError::Pipeline(kind)),
317 _ => Ok(DumpReceipt {
318 dump_id: self.id,
319 segments_processed: self.segments_processed,
320 finished_at: SystemTime::now(),
321 time_range: self.time_range(),
322 manifest_key,
323 }),
324 };
325 (tx, result)
326 }
327}
328
329fn epoch_to_system(epoch_secs: u64) -> SystemTime {
330 UNIX_EPOCH + Duration::from_secs(epoch_secs)
331}
332
333#[derive(Debug, Default)]
337struct PassStats {
338 matched_done: usize,
341 retry_dump_ids: Vec<crate::dump::DumpId>,
344 entered_pipeline: usize,
346}
347
348fn record_dump_error(dumps: &mut [ActiveDump], matched: &[usize], kind: ProcessErrorKind) {
352 let msg = kind.to_string();
353 let mut kind = Some(kind);
354 for &i in matched {
355 let d = &mut dumps[i];
356 if d.first_error.is_none() {
357 d.first_error = Some(
358 kind.take()
359 .unwrap_or_else(|| ProcessErrorKind::Io(io::Error::other(msg.clone()))),
360 );
361 }
362 }
363}
364
365impl WorkerLoop {
366 pub(crate) async fn new(
367 fs: Arc<Fs>,
368 poll_interval: Duration,
369 mut processors: Vec<Box<dyn SegmentProcessor>>,
370 stop: tokio_util::sync::CancellationToken,
371 metrics_sink: BoxEntrySink,
372 trigger: Option<crate::dump::DumpRx>,
373 ) -> io::Result<Self> {
374 for processor in &mut processors {
375 let processor_name = processor.name();
376 tracing::debug!(
377 target: "dial9_worker",
378 processor = processor_name,
379 "initializing processor"
380 );
381 processor.initialize().await.map_err(|error| {
382 io::Error::new(
383 error.kind(),
384 format!("processor {processor_name} initialization failed: {error}"),
385 )
386 })?;
387 }
388
389 Ok(Self {
390 fs,
391 poll_interval,
392 processors,
393 metrics_sink,
394 stop,
395 trigger,
396 epoch_cache: HashMap::new(),
397 })
398 }
399
400 pub(crate) async fn run(&mut self) {
401 match self.trigger.take() {
402 None => self.run_continuous().await,
403 Some(rx) => self.run_triggered(rx).await,
404 }
405 }
406
407 async fn run_continuous(&mut self) {
408 loop {
409 let taken = self.fs.take_files();
410 let dispatched = taken.segments.len() as u64;
411 self.emit_cycle_metrics(&taken, dispatched);
412 self.process_segments(taken.segments, &mut []).await;
413
414 if self.stop.is_cancelled() || self.fs.writer_done() {
415 loop {
419 let taken = self.fs.take_files();
420 let dispatched = taken.segments.len() as u64;
421 self.emit_cycle_metrics(&taken, dispatched);
422 if taken.segments.is_empty() {
423 tracing::debug!(target: "dial9_worker", "Exiting run loop: drain complete");
424 return;
425 }
426 self.process_segments(taken.segments, &mut []).await;
427 }
428 }
429
430 Self::wait_for_more(&self.fs, &self.stop, self.poll_interval).await;
431 }
432 }
433
434 async fn run_triggered(&mut self, mut rx: crate::dump::DumpRx) {
439 let mut dumps: Vec<ActiveDump> = Vec::new();
440 let mut rx_open = true;
441
442 loop {
443 if !dumps.is_empty() {
444 let retry_hold = self.drain_matching(&mut dumps).await;
445 let exhaustive = self.fs.take_is_exhaustive();
453 let now = tokio::time::Instant::now();
454 let mut i = 0;
455 while i < dumps.len() {
456 let held = !retry_hold.is_empty()
457 && (!exhaustive || retry_hold.contains(&dumps[i].id));
458 if dumps[i].due(now) && !held {
459 self.resolve_dump(dumps.swap_remove(i)).await;
460 } else {
461 i += 1;
462 }
463 }
464 }
465
466 if self.stop.is_cancelled() || self.fs.writer_done() {
467 self.drain_matching(&mut dumps).await;
471 for dump in dumps.drain(..) {
472 self.resolve_dump(dump).await;
473 }
474 rx.rx.close();
476 while let Ok(req) = rx.rx.try_recv() {
477 let _ = req.receipt_tx.send(Err(DumpError::WorkerStopped));
478 }
479 tracing::debug!(target: "dial9_worker", "Exiting triggered run loop");
480 return;
481 }
482
483 let min_deadline = dumps.iter().filter_map(|d| d.deadline).min();
484 tokio::select! {
485 _ = self.stop.cancelled() => {}
486 req = rx.rx.recv(), if rx_open => {
487 match req {
488 Some(req) => dumps.push(ActiveDump::register(req)),
489 None => rx_open = false,
492 }
493 }
494 _ = tokio::time::sleep_until(
495 min_deadline.unwrap_or_else(tokio::time::Instant::now)
496 ), if min_deadline.is_some() => {}
497 _ = Self::wait_for_more(&self.fs, &self.stop, self.poll_interval),
498 if !dumps.is_empty() => {}
499 }
500 }
501 }
502
503 async fn drain_matching(&mut self, dumps: &mut [ActiveDump]) -> Vec<crate::dump::DumpId> {
508 loop {
509 if dumps.is_empty() {
510 return Vec::new();
511 }
512 let windows: Vec<EpochWindow> = dumps.iter().map(|d| d.window).collect();
513 let mut taken = self.fs.take_files_matching(&windows);
514 if !self.epoch_cache.is_empty() {
518 let live: std::collections::HashSet<u32> =
519 taken.segments.iter().map(|t| t.seg_ref.index()).collect();
520 self.epoch_cache.retain(|idx, _| live.contains(idx));
521 }
522 if taken.segments.is_empty() {
523 self.emit_cycle_metrics(&taken, 0);
524 return Vec::new();
525 }
526 let segments = std::mem::take(&mut taken.segments);
527 let stats = self.process_segments(segments, dumps).await;
528 self.emit_cycle_metrics(&taken, stats.entered_pipeline as u64);
531 if !stats.retry_dump_ids.is_empty() {
532 return stats.retry_dump_ids;
533 }
534 if stats.matched_done == 0 {
535 return Vec::new();
537 }
538 }
539 }
540
541 async fn resolve_dump(&mut self, dump: ActiveDump) {
547 let completion = dump.completion();
548 let mut manifest_key = None;
549 for processor in &mut self.processors {
550 let processor_name = processor.name();
551 let finalize_result = {
554 let mut slot = Some(&mut **processor);
557 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
558 let p = slot.take().expect("closure called once");
559 p.finalize_dump(&completion)
560 })) {
561 Ok(fut) => std::panic::AssertUnwindSafe(fut).catch_unwind().await,
562 Err(panic_payload) => Err(panic_payload),
563 }
564 };
565 match finalize_result {
566 Ok(Some(key)) => manifest_key = Some(key),
567 Ok(None) => {}
568 Err(_) => {
569 rate_limited!(Duration::from_secs(60), {
570 tracing::error!(
571 target: "dial9_worker",
572 processor = processor_name,
573 dump_id = %completion.dump_id,
574 "finalize_dump panicked"
575 );
576 });
577 }
578 }
579 }
580 let (tx, result) = dump.into_result(manifest_key);
581 let _ = tx.send(result);
583 }
584
585 async fn wait_for_more(
592 fs: &Fs,
593 stop: &tokio_util::sync::CancellationToken,
594 poll_interval: Duration,
595 ) {
596 if fs.is_disk() {
597 tokio::select! {
598 _ = stop.cancelled() => {}
599 _ = tokio::time::sleep(poll_interval) => {}
600 }
601 } else {
602 tokio::select! {
603 _ = stop.cancelled() => {}
604 _ = fs.wait_for_wakeup() => {}
605 }
606 }
607 }
608
609 #[cfg(test)]
612 async fn process_open_segments(&mut self) -> bool {
613 let taken = self.fs.take_files();
614 let found = !taken.segments.is_empty();
615 let dispatched = taken.segments.len() as u64;
616 self.emit_cycle_metrics(&taken, dispatched);
617 self.process_segments(taken.segments, &mut []).await;
618 found
619 }
620
621 async fn process_segments(
622 &mut self,
623 segments: Vec<TakenSegment>,
624 dumps: &mut [ActiveDump],
625 ) -> PassStats {
626 let mut stats = PassStats::default();
627 if self.processors.is_empty() {
628 return stats;
629 }
630
631 'next_segment: for (seg_idx, taken) in segments.into_iter().enumerate() {
632 if !dumps.is_empty()
636 && let Some(&(start, seal)) = self.epoch_cache.get(&taken.seg_ref.index())
637 && !dumps.iter().any(|d| d.window.overlaps(start, seal))
638 {
639 self.fs.release_claim(&taken.seg_ref);
640 continue;
641 }
642 let retry_count = taken.retry_count();
645 let original_bytes = taken.original_bytes();
646 let mem_epochs = taken.mem_epochs();
647 let (seg_ref, payload, accounting) = match taken.load() {
648 Ok(t) => t,
649 Err(e) if e.kind() == io::ErrorKind::NotFound => {
650 rate_limited!(Duration::from_secs(60), {
651 tracing::warn!(
652 target: "dial9_worker",
653 "segment vanished between scan and load, skipping"
654 );
655 });
656 continue;
657 }
658 Err(e) => {
659 rate_limited!(Duration::from_secs(60), {
660 tracing::warn!(target: "dial9_worker", error = %e, "failed to load segment");
661 });
662 continue;
663 }
664 };
665
666 let uncompressed_size = payload.len() as u64;
667 let path_for_header = seg_ref.disk_path().unwrap_or_else(|| Path::new(""));
668 let header_bytes = payload.chunks().first().map_or(&[][..], |b| b.as_ref());
672 let (epoch_secs, header_valid) =
673 sealed::creation_epoch_secs(header_bytes, path_for_header);
674 let seal_secs = match mem_epochs {
677 Some((_, seal)) => seal,
678 None => sealed::seal_epoch_secs(path_for_header),
679 };
680
681 let matched: Vec<usize> = dumps
683 .iter()
684 .enumerate()
685 .filter(|(_, d)| d.window.overlaps(epoch_secs, seal_secs))
686 .map(|(i, _)| i)
687 .collect();
688 if !dumps.is_empty() && matched.is_empty() {
689 match &seg_ref {
691 SegmentRef::Disk(_) => {
692 self.epoch_cache
693 .insert(seg_ref.index(), (epoch_secs, seal_secs));
694 self.fs.release_claim(&seg_ref);
695 }
696 SegmentRef::Memory(_) => {
697 if let (Some(count), Some(bytes)) = (retry_count, original_bytes.as_ref()) {
701 self.fs.release_for_retry(
702 &seg_ref,
703 bytes.clone(),
704 count,
705 (epoch_secs, seal_secs),
706 );
707 }
708 }
709 }
710 continue;
711 }
712 stats.entered_pipeline += 1;
713
714 let mut metrics = SegmentProcessMetrics {
715 operation: Operation::ProcessSegment,
716 total_time: Timer::start_now(),
717 status: None,
718 segment_index: seg_ref.index(),
719 uncompressed_size,
720 compressed_size: None,
721 invalid_file_header: !header_valid,
722 panicked: false,
723 panic_message: None,
724 pipeline: PipelineMetrics::default(),
725 }
726 .append_on_drop(self.metrics_sink.clone());
727
728 let seg_ref_retained = seg_ref.clone();
731 let mut data = SegmentData::new(
732 seg_ref,
733 payload,
734 HashMap::from([
735 ("epoch_secs".into(), epoch_secs.to_string()),
736 ("segment_index".into(), seg_ref_retained.index().to_string()),
737 ]),
738 accounting,
739 );
740
741 if !matched.is_empty() {
742 let ids: Vec<String> = matched.iter().map(|&i| dumps[i].id.to_string()).collect();
746 data.metadata_mut().insert("dump_id".into(), ids.join(","));
747 for &i in &matched {
748 for (k, v) in &dumps[i].metadata {
749 data.metadata_mut()
750 .entry(format!("dump.{k}"))
751 .or_insert_with(|| v.clone());
752 }
753 }
754 }
755
756 for processor in &mut self.processors {
757 let mut stage = StageMetrics::start();
758 let proc_start = std::time::Instant::now();
759 tracing::debug!(target: "dial9_worker", processor = processor.name(), segment = seg_idx + 1, "running processor");
760 let process_result = {
765 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
766 processor.process(data)
767 })) {
768 Ok(fut) => std::panic::AssertUnwindSafe(fut).catch_unwind().await,
769 Err(panic_payload) => Err(panic_payload),
770 }
771 };
772 match process_result {
773 Ok(Ok(next)) => {
774 tracing::debug!(target: "dial9_worker", processor = processor.name(), segment = seg_idx + 1, elapsed_ms = proc_start.elapsed().as_secs_f64() * 1000.0, "processor succeeded");
775 data = next;
776 data.adjust_accounting();
777 stage.succeed();
778 metrics.pipeline.push(processor.name(), stage);
779 }
780 Ok(Err(e)) => {
781 tracing::debug!(target: "dial9_worker", processor = processor.name(), segment = seg_idx + 1, elapsed_ms = proc_start.elapsed().as_secs_f64() * 1000.0, error = %e.kind(), "processor failed");
782 let (next_data, err_kind) = e.into_parts();
783 data = next_data;
784 let already_deleted = err_kind.already_deleted();
785 let retryable = err_kind.retryable();
786 let kind_msg = err_kind.to_string();
787 stage.fail();
788 metrics.pipeline.push(processor.name(), stage);
789 metrics.status = Some(MetriqueResult::Failure);
790 metrics.compressed_size = data.compressed_size();
791 metrics.total_time.stop();
792 if already_deleted {
793 tracing::debug!(target: "dial9_worker", id = %data.segment(), "segment evicted during processing, skipping");
794 self.epoch_cache.remove(&seg_ref_retained.index());
797 if !matched.is_empty() {
798 stats.matched_done += 1;
799 }
800 } else if retryable {
801 match data.segment() {
802 SegmentRef::Memory(_) => {
807 match (retry_count, original_bytes.as_ref()) {
808 (Some(prev), Some(bytes)) => {
809 let attempt = prev + 1;
810 if attempt > crate::fs::MEMORY_RETRY_BUDGET {
811 rate_limited!(Duration::from_secs(60), {
812 tracing::warn!(target: "dial9_worker", id = %data.segment(), err = %kind_msg, budget = crate::fs::MEMORY_RETRY_BUDGET, "memory retry budget exhausted, dropping segment");
813 });
814 if !matched.is_empty() {
818 stats.matched_done += 1;
819 record_dump_error(dumps, &matched, err_kind);
820 }
821 } else {
822 tokio::time::sleep(self.poll_interval).await;
823 self.fs.release_for_retry(
824 data.segment(),
825 bytes.clone(),
826 attempt,
827 (epoch_secs, seal_secs),
828 );
829 stats
830 .retry_dump_ids
831 .extend(matched.iter().map(|&i| dumps[i].id));
832 }
833 }
834 _ => {
835 rate_limited!(Duration::from_secs(60), {
836 tracing::warn!(target: "dial9_worker", id = %data.segment(), "memory segment missing retry state, dropping");
837 });
838 if !matched.is_empty() {
839 stats.matched_done += 1;
840 record_dump_error(dumps, &matched, err_kind);
841 }
842 }
843 }
844 }
845 SegmentRef::Disk(_) => {
846 tracing::debug!(target: "dial9_worker", id = %data.segment(), err = %kind_msg, "retryable error");
847 self.fs.release_claim(data.segment());
848 stats
849 .retry_dump_ids
850 .extend(matched.iter().map(|&i| dumps[i].id));
851 }
852 }
853 } else {
854 self.fs
855 .remove_sealed(data.segment(), RemoveReason::Terminal);
856 rate_limited!(Duration::from_secs(60), {
857 tracing::warn!(target: "dial9_worker", error = %kind_msg, id = %data.segment(), "processor failed, removing segment");
858 });
859 self.epoch_cache.remove(&seg_ref_retained.index());
860 if !matched.is_empty() {
861 stats.matched_done += 1;
862 record_dump_error(dumps, &matched, err_kind);
863 }
864 }
865 continue 'next_segment;
866 }
867 Err(panic_payload) => {
868 let panic_msg = panic_payload
869 .downcast_ref::<&str>()
870 .copied()
871 .or_else(|| panic_payload.downcast_ref::<String>().map(|s| s.as_str()))
872 .unwrap_or("unknown panic");
873 rate_limited!(
874 Duration::from_secs(60),
875 tracing::error!(
876 target: "dial9_worker",
877 processor = processor.name(),
878 segment = seg_idx + 1,
879 id = %seg_ref_retained,
880 panic = panic_msg,
881 "processor panicked, skipping segment"
882 )
883 );
884 metrics.status = Some(MetriqueResult::Failure);
888 metrics.panicked = true;
889 metrics.panic_message = Some(panic_msg.to_owned());
890 metrics.total_time.stop();
891 self.fs
892 .remove_sealed(&seg_ref_retained, RemoveReason::Terminal);
893 self.epoch_cache.remove(&seg_ref_retained.index());
894 if !matched.is_empty() {
895 stats.matched_done += 1;
896 record_dump_error(
897 dumps,
898 &matched,
899 ProcessErrorKind::Io(io::Error::other(format!(
900 "processor panicked: {panic_msg}"
901 ))),
902 );
903 }
904 continue 'next_segment;
905 }
906 }
907 }
908
909 metrics.status = Some(MetriqueResult::Success);
910 metrics.compressed_size = data.compressed_size();
911 metrics.total_time.stop();
912 self.epoch_cache.remove(&seg_ref_retained.index());
913 if !matched.is_empty() {
914 stats.matched_done += 1;
915 for &i in &matched {
916 let d = &mut dumps[i];
917 d.segments_processed += 1;
918 d.first_epoch = Some(d.first_epoch.map_or(epoch_secs, |e| e.min(epoch_secs)));
919 d.last_epoch = Some(d.last_epoch.map_or(seal_secs, |e| e.max(seal_secs)));
920 }
921 }
922 }
923
924 stats
925 }
926
927 fn emit_cycle_metrics(&self, taken: &TakenFiles, segments_dispatched: u64) {
928 drop(
929 WorkerCycleMetrics {
930 operation: Operation::WorkerCycle,
931 memory_queued_segments: taken.queued_segments,
932 memory_queued_bytes: taken.queued_bytes,
933 in_flight_segments: taken.in_flight_segments,
934 in_flight_bytes: taken.in_flight_bytes,
935 memory_peak_in_flight_bytes: taken.in_flight_bytes_peak,
936 segments_evicted: taken.segments_dropped,
937 segments_dispatched,
938 }
939 .append_on_drop(self.metrics_sink.clone()),
940 );
941 }
942}
943
944#[cfg(test)]
945mod tests;