1use std::sync::Arc;
2
3use linkrs_core::columnar::MaterializedBatch;
4use linkrs_core::error::QueryError;
5use linkrs_core::types::expr::Expression;
6use linkrs_core::value::NullType;
7use linkrs_core::Value;
8
9use crate::executor::base::{MemoryBudget, MemoryTracker};
10use crate::executor::expression::evaluator::ExpressionEvaluator;
11use crate::executor::streaming::chunk::DataChunk;
12use crate::executor::streaming::executor::{StreamingExecutor, ValueRowContext};
13use crate::executor::streaming::spill::{
14 finalize_partitions_with_runtime, HashPartitionConfig, HashPartitionSpiller, RunReader,
15 SpillManager, SpilledRun,
16};
17
18use super::helpers::{emit_batch_slice, BlockingContext};
19use super::materialize::{
20 project_key, DataCollectState, DistinctState, MaterializeState, RollUpApplyState,
21};
22
23pub(super) fn open_distinct(state: &mut Option<DistinctState>) {
24 *state = Some(DistinctState {
25 seen_rows: std::collections::HashSet::new(),
26 key_cols: Vec::new(),
27 batch: MaterializedBatch::new(0, 0),
28 emitted_offset: 0,
29 col_names: Vec::new(),
30 input_layout: None,
31 partition_spiller: None,
32 spilled_runs: vec![],
33 current_partition: 0,
34 partition_seen: std::collections::HashSet::new(),
35 has_spilled: false,
36 });
37}
38
39pub(super) fn open_materialize(state: &mut Option<MaterializeState>) {
40 *state = Some(MaterializeState {
41 batch: MaterializedBatch::new(0, 0),
42 emitted_offset: 0,
43 materialized: false,
44 input_layout: None,
45 spilled_runs: Vec::new(),
46 replay_index: 0,
47 replay_reader: None,
48 replay_batch: None,
49 replay_offset: 0,
50 accounted_bytes: 0,
51 });
52}
53
54pub(super) fn open_data_collect(state: &mut Option<DataCollectState>) {
55 *state = Some(DataCollectState {
56 batch: MaterializedBatch::new(0, 0),
57 emitted: false,
58 emitted_offset: 0,
59 input_layout: None,
60 spilled_runs: Vec::new(),
61 replay_index: 0,
62 replay_reader: None,
63 replay_batch: None,
64 replay_offset: 0,
65 accounted_bytes: 0,
66 });
67}
68
69pub(super) fn open_rollup_apply(state: &mut Option<RollUpApplyState>) {
70 *state = Some(RollUpApplyState {
71 batch: MaterializedBatch::new(0, 0),
72 emitted_offset: 0,
73 accumulated: false,
74 spilled_runs: Vec::new(),
75 replay_index: 0,
76 replay_reader: None,
77 replay_batch: None,
78 replay_offset: 0,
79 accounted_bytes: 0,
80 });
81}
82
83fn drain_batch_to_run(
89 batch: &mut MaterializedBatch,
90 accounted: &mut usize,
91 runs: &mut Vec<SpilledRun>,
92 sm: &SpillManager,
93 memory_tracker: &mut MemoryTracker,
94 ctx: Option<&BlockingContext<'_>>,
95) -> Result<(), QueryError> {
96 if batch.is_empty() {
97 return Ok(());
98 }
99 let mut writer = sm.create_run_writer(batch.schema_fingerprint())?;
100 writer.write_batch(batch)?;
101 let run = sm.finalize_run(writer)?;
102 if let Some(ctx) = ctx {
103 if let Some(rt) = ctx.runtime.as_ref() {
104 rt.columnar_stats()
105 .record_spill(run.row_count, run.byte_size);
106 }
107 }
108 runs.push(run);
109 memory_tracker.release(*accounted);
110 *accounted = 0;
111 batch.clear();
112 Ok(())
113}
114
115fn replay_next_chunk(
120 runs: &[SpilledRun],
121 replay_index: &mut usize,
122 replay_reader: &mut Option<RunReader>,
123 replay_batch: &mut Option<MaterializedBatch>,
124 replay_offset: &mut usize,
125 take: usize,
126 ctx: &BlockingContext<'_>,
127) -> Result<Option<DataChunk>, QueryError> {
128 loop {
129 if *replay_index >= runs.len() {
130 return Ok(None);
131 }
132 let filled = replay_batch
133 .as_ref()
134 .is_some_and(|b| *replay_offset < b.num_rows());
135 if !filled {
136 if replay_reader.is_none() {
137 *replay_reader = Some(RunReader::open(&runs[*replay_index])?);
138 }
139 match replay_reader
140 .as_mut()
141 .expect("replay reader must be present")
142 .read_batch(take)?
143 {
144 Some(b) => {
145 *replay_batch = Some(b);
146 *replay_offset = 0;
147 }
148 None => {
149 let _ = std::fs::remove_file(&runs[*replay_index].path);
150 *replay_reader = None;
151 *replay_batch = None;
152 *replay_offset = 0;
153 *replay_index += 1;
154 continue;
155 }
156 }
157 }
158 let (len, chunk) = {
159 let batch = replay_batch.as_ref().expect("replay batch must be filled");
160 let total = batch.num_rows();
161 let len = take.min(total - *replay_offset);
162 (
163 len,
164 DataChunk::slice_from_batch(
165 batch,
166 *replay_offset,
167 len,
168 Arc::clone(ctx.output_layout),
169 ),
170 )
171 };
172 *replay_offset += len;
173 if let Some(rt) = ctx.runtime.as_ref() {
174 rt.columnar_stats().record_batch_outlet();
175 }
176 return Ok(Some(chunk));
177 }
178}
179
180fn reserve_or_spill(
186 memory_tracker: &mut MemoryTracker,
187 est: usize,
188 batch: &mut MaterializedBatch,
189 accounted: &mut usize,
190 runs: &mut Vec<SpilledRun>,
191 ctx: &BlockingContext<'_>,
192) -> Result<(), QueryError> {
193 if let Err(e) = memory_tracker.try_reserve(est) {
194 match ctx.runtime.as_ref().and_then(|rt| rt.get_spill_manager()) {
195 Some(sm) => {
196 if batch.is_empty() {
197 return Err(e);
198 }
199 drain_batch_to_run(batch, accounted, runs, &sm, memory_tracker, Some(ctx))?;
200 memory_tracker.try_reserve(est).map_err(|_| e)?;
201 }
202 None => return Err(e),
203 }
204 }
205 Ok(())
206}
207
208pub(super) fn next_distinct(
209 memory_tracker: &mut MemoryTracker,
210 state: &mut DistinctState,
211 ctx: &BlockingContext<'_>,
212 input: &mut StreamingExecutor,
213) -> Result<Option<DataChunk>, QueryError> {
214 if state.emitted_offset < state.batch.num_rows() {
216 let total = state.batch.num_rows();
217 let len = 2048.min(total - state.emitted_offset);
218 let chunk = DataChunk::slice_from_batch(
219 &state.batch,
220 state.emitted_offset,
221 len,
222 Arc::clone(ctx.output_layout),
223 );
224 state.emitted_offset += len;
225 if let Some(rt) = ctx.runtime.as_ref() {
226 rt.columnar_stats().record_batch_outlet();
227 }
228 return Ok(Some(chunk));
229 }
230
231 if state.has_spilled && state.partition_spiller.is_none() {
233 while state.current_partition < state.spilled_runs.len() {
234 if let Some(rt) = ctx.runtime.as_ref() {
235 rt.ensure_not_cancelled()?;
236 }
237
238 let run = match &state.spilled_runs[state.current_partition] {
239 Some(r) => r,
240 None => {
241 state.current_partition += 1;
242 continue;
243 }
244 };
245
246 let mut reader = crate::executor::streaming::spill::RunReader::open(run)?;
247 state.batch.clear();
248 state.emitted_offset = 0;
249 if !state.col_names.is_empty() {
250 state
251 .batch
252 .set_schema_names(Arc::from(state.col_names.clone().into_boxed_slice()));
253 }
254
255 while let Some(row) = reader.read_row()? {
256 let key = project_key(&row, &state.key_cols);
257 if !state.partition_seen.contains(&key) {
258 memory_tracker.try_reserve_row(&row)?;
259 state.partition_seen.insert(key);
260 state.batch.append_row(row);
261 }
262 }
263
264 let _ = std::fs::remove_file(&run.path);
265 state.partition_seen.clear();
266 memory_tracker.reset();
267 state.current_partition += 1;
268
269 if !state.batch.is_empty() {
270 let len = 2048.min(state.batch.num_rows());
271 let chunk = DataChunk::slice_from_batch(
272 &state.batch,
273 0,
274 len,
275 Arc::clone(ctx.output_layout),
276 );
277 state.emitted_offset = len;
278 if let Some(rt) = ctx.runtime.as_ref() {
279 rt.columnar_stats().record_batch_outlet();
280 }
281 return Ok(Some(chunk));
282 }
283 }
284 return Ok(None);
285 }
286
287 let mut accumulating = true;
289 while accumulating {
290 match input.advance()? {
291 Some(mut chunk) => {
292 chunk.normalize_for_opaque("Distinct");
293 if let Some(rt) = ctx.runtime.as_ref() {
294 rt.ensure_not_cancelled()?;
295 }
296 if state.col_names.is_empty() {
297 state.col_names = chunk.col_names();
298 state.input_layout = Some(chunk.get_layout());
299 }
300 for row in chunk.rows {
301 let key = project_key(&row, &state.key_cols);
302 if !state.seen_rows.contains(&key) {
303 if let Err(e) = memory_tracker.try_reserve_row(&row) {
304 if let Some(sm) =
305 ctx.runtime.as_ref().and_then(|rt| rt.get_spill_manager())
306 {
307 debug_assert!(
311 state.key_cols.is_empty(),
312 "keyed distinct spill needs a row store"
313 );
314 let config = HashPartitionConfig::default();
315 let mut spiller = HashPartitionSpiller::new(config, &sm, 0)?;
316
317 for seen_row in state.seen_rows.drain() {
318 spiller.insert_row(&seen_row, &sm)?;
319 memory_tracker
320 .release(MemoryBudget::estimate_row_memory(&seen_row));
321 }
322
323 spiller.insert_row(&row, &sm)?;
324 memory_tracker.release(MemoryBudget::estimate_row_memory(&row));
325
326 state.partition_spiller = Some(spiller);
327 state.has_spilled = true;
328 accumulating = false;
329 break;
330 } else {
331 return Err(e);
332 }
333 }
334 state.seen_rows.insert(key);
335 }
336 }
337 }
338 None => {
339 accumulating = false;
340 }
341 }
342 }
343
344 if let Some(ref mut spiller) = state.partition_spiller {
346 while let Some(mut chunk) = input.advance()? {
347 chunk.normalize_for_opaque("Distinct");
348 if let Some(rt) = ctx.runtime.as_ref() {
349 rt.ensure_not_cancelled()?;
350 }
351 let sm = ctx
352 .runtime
353 .as_ref()
354 .and_then(|rt| rt.get_spill_manager())
355 .ok_or_else(|| QueryError::execution("Spill manager not available".to_string()))?;
356 for row in chunk.rows {
357 spiller.insert_row(&row, &sm)?;
358 }
359 }
360
361 let runs = finalize_partitions_with_runtime(
362 state.partition_spiller.take().unwrap(),
363 ctx.runtime.as_ref(),
364 )?;
365 state.spilled_runs = runs;
366 state.current_partition = 0;
367 state.partition_seen.clear();
368
369 return Ok(None);
370 }
371
372 debug_assert!(
375 state.key_cols.is_empty(),
376 "keyed distinct output needs a row store"
377 );
378 state.batch.clear();
379 state.emitted_offset = 0;
380 if !state.col_names.is_empty() {
381 state
382 .batch
383 .set_schema_names(Arc::from(state.col_names.clone().into_boxed_slice()));
384 }
385 for key_row in state.seen_rows.drain() {
386 state.batch.append_row(key_row);
387 }
388
389 if state.batch.is_empty() {
390 return Ok(None);
391 }
392 let len = 2048.min(state.batch.num_rows());
393 let chunk = DataChunk::slice_from_batch(&state.batch, 0, len, Arc::clone(ctx.output_layout));
394 state.emitted_offset = len;
395 if let Some(rt) = ctx.runtime.as_ref() {
396 rt.columnar_stats().record_batch_outlet();
397 }
398 Ok(Some(chunk))
399}
400
401pub(super) fn next_materialize(
402 memory_tracker: &mut MemoryTracker,
403 state: &mut MaterializeState,
404 ctx: &BlockingContext<'_>,
405 input: &mut StreamingExecutor,
406) -> Result<Option<DataChunk>, QueryError> {
407 if !state.materialized {
408 while let Some(mut chunk) = input.advance()? {
409 chunk.normalize_for_opaque("Materialize");
410 if let Some(rt) = ctx.runtime.as_ref() {
411 rt.ensure_not_cancelled()?;
412 }
413 if state.input_layout.is_none() {
414 state.input_layout = Some(chunk.get_layout());
415 state
416 .batch
417 .set_schema_names(Arc::from(chunk.col_names().into_boxed_slice()));
418 }
419 for row in chunk.rows {
420 let est = MemoryBudget::estimate_row_memory(&row);
421 reserve_or_spill(
422 memory_tracker,
423 est,
424 &mut state.batch,
425 &mut state.accounted_bytes,
426 &mut state.spilled_runs,
427 ctx,
428 )?;
429 state.accounted_bytes += est;
430 state.batch.append_row(row);
431 }
432 }
433
434 state.materialized = true;
435 state.emitted_offset = 0;
436 state.replay_index = 0;
437 }
438
439 if let Some(chunk) = replay_next_chunk(
441 &state.spilled_runs,
442 &mut state.replay_index,
443 &mut state.replay_reader,
444 &mut state.replay_batch,
445 &mut state.replay_offset,
446 ctx.config.chunk_size,
447 ctx,
448 )? {
449 return Ok(Some(chunk));
450 }
451
452 if state.emitted_offset < state.batch.num_rows() {
454 return Ok(Some(emit_batch_slice(
455 &state.batch,
456 &mut state.emitted_offset,
457 ctx,
458 )));
459 }
460 Ok(None)
461}
462
463pub(super) fn next_data_collect(
464 memory_tracker: &mut MemoryTracker,
465 state: &mut DataCollectState,
466 ctx: &BlockingContext<'_>,
467 input: &mut StreamingExecutor,
468) -> Result<Option<DataChunk>, QueryError> {
469 if state.emitted {
470 return Ok(None);
471 }
472
473 while let Some(mut chunk) = input.advance()? {
474 chunk.normalize_for_opaque("DataCollect");
475 if let Some(rt) = ctx.runtime.as_ref() {
476 rt.ensure_not_cancelled()?;
477 }
478 if state.input_layout.is_none() {
479 state.input_layout = Some(chunk.get_layout());
480 state
481 .batch
482 .set_schema_names(Arc::from(chunk.col_names().into_boxed_slice()));
483 }
484 for row in chunk.rows {
485 let est = MemoryBudget::estimate_row_memory(&row);
486 reserve_or_spill(
487 memory_tracker,
488 est,
489 &mut state.batch,
490 &mut state.accounted_bytes,
491 &mut state.spilled_runs,
492 ctx,
493 )?;
494 state.accounted_bytes += est;
495 state.batch.append_row(row);
496 }
497 }
498
499 if state.spilled_runs.is_empty() {
501 if !state.batch.is_empty() {
502 state.emitted = true;
503 if let Some(rt) = ctx.runtime.as_ref() {
504 rt.columnar_stats().record_batch_outlet();
505 }
506 return Ok(Some(DataChunk::from_batch(
507 &state.batch,
508 Arc::clone(ctx.output_layout),
509 )));
510 }
511 return Ok(None);
512 }
513
514 if let Some(chunk) = replay_next_chunk(
516 &state.spilled_runs,
517 &mut state.replay_index,
518 &mut state.replay_reader,
519 &mut state.replay_batch,
520 &mut state.replay_offset,
521 ctx.config.chunk_size,
522 ctx,
523 )? {
524 return Ok(Some(chunk));
525 }
526 if state.emitted_offset < state.batch.num_rows() {
527 return Ok(Some(emit_batch_slice(
528 &state.batch,
529 &mut state.emitted_offset,
530 ctx,
531 )));
532 }
533 state.emitted = true;
534 Ok(None)
535}
536
537pub(super) fn next_rollup_apply(
538 rollup_expressions: &[Expression],
539 memory_tracker: &mut MemoryTracker,
540 state: &mut RollUpApplyState,
541 ctx: &BlockingContext<'_>,
542 input: &mut StreamingExecutor,
543) -> Result<Option<DataChunk>, QueryError> {
544 loop {
545 if let Some(chunk) = replay_next_chunk(
547 &state.spilled_runs,
548 &mut state.replay_index,
549 &mut state.replay_reader,
550 &mut state.replay_batch,
551 &mut state.replay_offset,
552 ctx.config.chunk_size,
553 ctx,
554 )? {
555 return Ok(Some(chunk));
556 }
557 if state.emitted_offset < state.batch.num_rows() {
559 return Ok(Some(emit_batch_slice(
560 &state.batch,
561 &mut state.emitted_offset,
562 ctx,
563 )));
564 }
565 if state.accumulated {
566 return Ok(None);
567 }
568
569 let mut col_names: Vec<String> = Vec::new();
570 let mut schema_names_set = false;
571 while let Some(mut chunk) = input.advance()? {
572 chunk.normalize_for_opaque("RollUpApply");
573 if let Some(rt) = ctx.runtime.as_ref() {
574 rt.ensure_not_cancelled()?;
575 }
576 if col_names.is_empty() {
577 col_names = chunk.col_names();
578 }
579 if !schema_names_set {
580 state
581 .batch
582 .set_schema_names(Arc::from(chunk.col_names().into_boxed_slice()));
583 schema_names_set = true;
584 }
585 for row in chunk.rows {
586 let mut ctx_eval = ValueRowContext::from_names(row.clone(), col_names.clone());
587 let mut aggregated = row.clone();
588 for expr in rollup_expressions.iter() {
589 match ExpressionEvaluator::evaluate(expr, &mut ctx_eval) {
590 Ok(val) => aggregated.push(val),
591 Err(_) => aggregated.push(Value::Null(NullType::Null)),
592 }
593 }
594 let est = MemoryBudget::estimate_row_memory(&aggregated);
595 reserve_or_spill(
596 memory_tracker,
597 est,
598 &mut state.batch,
599 &mut state.accounted_bytes,
600 &mut state.spilled_runs,
601 ctx,
602 )?;
603 state.accounted_bytes += est;
604 state.batch.append_row(aggregated);
605 }
606 }
607 state.accumulated = true;
608 }
609}
610
611pub(super) fn close_distinct(state: &mut Option<DistinctState>) {
612 if let Some(ref s) = state {
613 for r in s.spilled_runs.iter().flatten() {
614 let _ = std::fs::remove_file(&r.path);
615 }
616 }
617 *state = None;
618}
619
620pub(super) fn close_materialize(state: &mut Option<MaterializeState>) {
621 if let Some(ref s) = state {
622 for r in &s.spilled_runs {
623 let _ = std::fs::remove_file(&r.path);
624 }
625 }
626 *state = None;
627}
628
629pub(super) fn close_data_collect(state: &mut Option<DataCollectState>) {
630 if let Some(ref s) = state {
631 for r in &s.spilled_runs {
632 let _ = std::fs::remove_file(&r.path);
633 }
634 }
635 *state = None;
636}
637
638pub(super) fn close_rollup_apply(state: &mut Option<RollUpApplyState>) {
639 if let Some(ref s) = state {
640 for r in &s.spilled_runs {
641 let _ = std::fs::remove_file(&r.path);
642 }
643 }
644 *state = None;
645}
646
647pub(super) fn spill_distinct(
648 state: &mut DistinctState,
649 memory_tracker: &mut MemoryTracker,
650 sm: &SpillManager,
651) -> Result<(), QueryError> {
652 if !state.seen_rows.is_empty() {
653 let config = HashPartitionConfig::default();
654 let mut spiller = HashPartitionSpiller::new(config, sm, 0)?;
655 for row in state.seen_rows.drain() {
656 spiller.insert_row(&row, sm)?;
657 memory_tracker.release(MemoryBudget::estimate_row_memory(&row));
658 }
659 state.partition_spiller = Some(spiller);
660 state.has_spilled = true;
661 }
662 Ok(())
663}
664
665pub(super) fn spill_materialize(
666 state: &mut MaterializeState,
667 sm: &SpillManager,
668 memory_tracker: &mut MemoryTracker,
669) -> Result<(), QueryError> {
670 drain_batch_to_run(
671 &mut state.batch,
672 &mut state.accounted_bytes,
673 &mut state.spilled_runs,
674 sm,
675 memory_tracker,
676 None,
677 )
678}
679
680pub(super) fn spill_data_collect(
681 state: &mut DataCollectState,
682 sm: &SpillManager,
683 memory_tracker: &mut MemoryTracker,
684) -> Result<(), QueryError> {
685 drain_batch_to_run(
686 &mut state.batch,
687 &mut state.accounted_bytes,
688 &mut state.spilled_runs,
689 sm,
690 memory_tracker,
691 None,
692 )
693}
694
695pub(super) fn spill_rollup_apply(
696 state: &mut RollUpApplyState,
697 sm: &SpillManager,
698 memory_tracker: &mut MemoryTracker,
699) -> Result<(), QueryError> {
700 drain_batch_to_run(
701 &mut state.batch,
702 &mut state.accounted_bytes,
703 &mut state.spilled_runs,
704 sm,
705 memory_tracker,
706 None,
707 )
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use crate::executor::base::MemoryBudget;
714 use crate::executor::streaming::slot::SlotLayout;
715 use crate::executor::streaming::spill::SpillConfig;
716
717 fn test_ctx<'a>(
718 output_layout: &'a Arc<SlotLayout>,
719 config: &'a crate::executor::streaming::operators::source_operator::OperatorConfig,
720 ) -> BlockingContext<'a> {
721 BlockingContext {
722 runtime: &None,
723 output_layout,
724 config,
725 }
726 }
727
728 #[test]
729 fn spill_replay_preserves_input_order() {
730 let sm = SpillManager::new(SpillConfig::default(), 9101).expect("spill manager");
731 let mut tracker = MemoryTracker::new(MemoryBudget::default_budget());
732 let layout = Arc::new(SlotLayout::from_names(&["v".to_string()]));
733 let config =
734 crate::executor::streaming::operators::source_operator::OperatorConfig::default();
735 let ctx = test_ctx(&layout, &config);
736
737 let rows: Vec<Vec<Value>> = (0..10).map(|i| vec![Value::BigInt(i)]).collect();
738 let mut first = MaterializedBatch::from_rows(rows[0..6].to_vec());
740 let mut accounted = 0usize;
741 let mut runs = Vec::new();
742 drain_batch_to_run(
743 &mut first,
744 &mut accounted,
745 &mut runs,
746 &sm,
747 &mut tracker,
748 Some(&ctx),
749 )
750 .expect("drain first");
751 assert!(first.is_empty());
752 let mut second = MaterializedBatch::from_rows(rows[6..8].to_vec());
753 drain_batch_to_run(
754 &mut second,
755 &mut accounted,
756 &mut runs,
757 &sm,
758 &mut tracker,
759 Some(&ctx),
760 )
761 .expect("drain second");
762 assert_eq!(runs.len(), 2);
763
764 let tail = MaterializedBatch::from_rows(rows[8..10].to_vec());
765 let mut replay_index = 0usize;
766 let mut replay_reader = None;
767 let mut replay_batch = None;
768 let mut replay_offset = 0usize;
769 let mut out = Vec::new();
770 while let Some(chunk) = replay_next_chunk(
771 &runs,
772 &mut replay_index,
773 &mut replay_reader,
774 &mut replay_batch,
775 &mut replay_offset,
776 3,
777 &ctx,
778 )
779 .expect("replay")
780 {
781 out.extend(chunk.rows);
782 }
783 out.extend(tail.to_rows());
784 assert_eq!(out, rows);
785 assert!(!runs[0].path.exists());
787 assert!(!runs[1].path.exists());
788 }
789
790 #[test]
791 fn reserve_or_spill_errors_without_manager() {
792 let mut tracker = MemoryTracker::new(MemoryBudget::new(1));
793 let layout = Arc::new(SlotLayout::from_names(&["v".to_string()]));
794 let config =
795 crate::executor::streaming::operators::source_operator::OperatorConfig::default();
796 let ctx = test_ctx(&layout, &config);
797 let mut batch = MaterializedBatch::new(0, 0);
798 let mut accounted = 0usize;
799 let mut runs = Vec::new();
800 let row = vec![Value::string("way too large for a 1-byte budget")];
801 let est = MemoryBudget::estimate_row_memory(&row);
802 assert!(reserve_or_spill(
803 &mut tracker,
804 est,
805 &mut batch,
806 &mut accounted,
807 &mut runs,
808 &ctx
809 )
810 .is_err());
811 assert!(runs.is_empty());
812 }
813}