1use crate::{config::Config, readers};
2use anyhow::{Result, ensure};
3use arrow::record_batch::RecordBatch;
4use crossbeam_channel::{Receiver, bounded};
5use serde::{Deserialize, Serialize};
6use std::{
7 collections::{HashMap, HashSet},
8 sync::Arc,
9};
10
11#[derive(Clone, Debug, Default, Serialize, Deserialize)]
12pub struct Position {
13 pub file: usize,
14 pub row: usize,
15 pub epoch: u64,
16 pub done: bool,
17}
18#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct Coordinate {
20 pub stage: usize,
21 pub source: usize,
22 pub file: usize,
23 pub row: usize,
24 pub epoch: u64,
25}
26#[derive(Clone, Debug, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct State {
29 pub version: u32,
30 pub stage: usize,
31 pub batch_index: u64,
32 pub batches_yielded: u64,
33 pub rows_yielded: u64,
34 pub positions: Vec<Position>,
35 pub mix_counter: u64,
36 pub shuffle_counter: u64,
37 pub buffer: Vec<Coordinate>,
38 pub partial: Vec<Coordinate>,
39}
40impl State {
41 pub fn new(config: &Config, stage: usize) -> Self {
42 Self {
43 version: 3,
44 stage,
45 batch_index: 0,
46 batches_yielded: 0,
47 rows_yielded: 0,
48 positions: vec![Position::default(); config.stages[stage].sources.len()],
49 mix_counter: 0,
50 shuffle_counter: 0,
51 buffer: Vec::new(),
52 partial: Vec::new(),
53 }
54 }
55 pub fn validate(&self, config: &Config) -> Result<()> {
56 ensure!(
57 self.version == 3 && self.stage < config.stages.len(),
58 "unsupported cursor version or stage"
59 );
60 let stage = &config.stages[self.stage];
61 ensure!(
62 self.positions.len() == stage.sources.len(),
63 "cursor source count mismatch"
64 );
65 for (p, g) in self.positions.iter().zip(&stage.sources) {
66 ensure!(p.file <= g.files.len(), "cursor file out of bounds");
67 }
68 ensure!(
69 self.partial.len() <= stage.batch_size,
70 "cursor partial batch too large"
71 );
72 for c in self.buffer.iter().chain(&self.partial) {
73 ensure!(
74 c.stage < config.stages.len(),
75 "cursor coordinate stage out of bounds"
76 );
77 let stage = &config.stages[c.stage];
78 ensure!(
79 c.source < stage.sources.len() && c.file < stage.sources[c.source].files.len(),
80 "cursor coordinate out of bounds"
81 );
82 }
83 Ok(())
84 }
85}
86fn mix64(mut x: u64) -> u64 {
88 x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
89 x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
90 x ^ (x >> 31)
91}
92pub fn random(seed: u64, stage: usize, name: &str, source: u64, counter: u64) -> u64 {
93 let mut key = 14695981039346656037u64;
94 for b in name.bytes() {
95 key = (key ^ b as u64).wrapping_mul(1099511628211);
96 }
97 let golden = 0x9e3779b97f4a7c15u64;
98 key = key.wrapping_add(seed)
99 ^ mix64((stage as u64).wrapping_add(1).wrapping_mul(golden))
100 ^ mix64(source.wrapping_mul(golden));
101 mix64(key.wrapping_add(counter.wrapping_add(1).wrapping_mul(golden)))
102}
103type Key = (usize, usize, usize);
104pub struct Engine {
105 pub config: Arc<Config>,
106 pub state: State,
107 pub dropped_rows: u64,
108 cache: HashMap<Key, readers::Shard>,
109 pending: HashMap<Key, Receiver<Result<readers::Shard>>>,
110 rows: HashMap<Coordinate, Option<RecordBatch>>,
111 orders: Vec<Vec<Vec<usize>>>,
112 window: Option<Key>,
113 pool: rayon::ThreadPool,
114 client: Arc<crate::storage::Client>,
115}
116pub struct Batch {
117 pub columns: RecordBatch,
118 pub stage: usize,
119 pub index: u64,
120 pub state: State,
121}
122impl Engine {
123 pub fn new(config: Arc<Config>, state: State) -> Result<Self> {
124 let pool = rayon::ThreadPoolBuilder::new()
125 .num_threads(config.threads)
126 .build()?;
127 let runtime = Arc::new(
128 tokio::runtime::Builder::new_multi_thread()
129 .worker_threads(1)
130 .enable_all()
131 .build()?,
132 );
133 let client = Arc::new(crate::storage::Client::new(config.storage.clone(), runtime));
134 for (stage_index, stage) in config.stages.iter().enumerate() {
135 for (source_index, source) in stage.sources.iter().enumerate() {
136 let fields = source.fields.as_deref().unwrap_or(&config.fields);
137 let encoding = source
138 .encoding
139 .as_ref()
140 .or(stage.encoding.as_ref())
141 .unwrap();
142 let mut found = false;
143 for file in &source.files {
144 use anyhow::Context;
145 let sample = readers::read(file, encoding, &client, fields, 0, true, &config)
146 .with_context(|| {
147 format!("stage={stage_index} source={source_index}")
148 })?;
149 if sample.columns.num_rows() > 0 {
150 found = true;
151 break;
152 }
153 }
154 ensure!(
155 found,
156 "stage {stage_index} source {source_index} {}: no record for field {}",
157 source.files[0].uri,
158 fields[0].name
159 );
160 }
161 }
162 let orders = config
163 .stages
164 .iter()
165 .enumerate()
166 .map(|(stage_index, stage)| {
167 stage
168 .sources
169 .iter()
170 .enumerate()
171 .map(|(source_index, source)| {
172 let mut order: Vec<_> = (0..source.files.len()).collect();
173 if stage.shuffle.is_some() {
174 for (draw, i) in (1..order.len()).rev().enumerate() {
175 let j = random(
176 config.seed,
177 stage_index,
178 "shards",
179 source_index as u64,
180 draw as u64,
181 ) % (i as u64 + 1);
182 order.swap(i, j as usize);
183 }
184 }
185 order
186 })
187 .collect()
188 })
189 .collect();
190 let mut engine = Self {
191 config,
192 state,
193 dropped_rows: 0,
194 cache: HashMap::new(),
195 pending: HashMap::new(),
196 rows: HashMap::new(),
197 orders,
198 window: None,
199 pool,
200 client,
201 };
202 engine.restore_rows()?;
203 Ok(engine)
204 }
205 fn heads(&self) -> impl Iterator<Item = Key> + '_ {
206 self.state
207 .positions
208 .iter()
209 .enumerate()
210 .filter(|(_, p)| !p.done)
211 .map(|(source, p)| {
212 (
213 self.state.stage,
214 source,
215 self.orders[self.state.stage][source][p.file],
216 )
217 })
218 }
219 fn schedule(
220 &mut self,
221 stage_index: usize,
222 source: usize,
223 file: usize,
224 row: usize,
225 required: bool,
226 ) {
227 let key = (stage_index, source, file);
228 if self.cache.contains_key(&key) || self.pending.contains_key(&key) {
229 return;
230 }
231 let reserved = if required {
232 0
233 } else {
234 self.heads()
235 .filter(|k| !self.cache.contains_key(k) && !self.pending.contains_key(k))
236 .count()
237 };
238 if self.cache.len() + self.pending.len() + reserved
239 >= self.config.threads * self.config.lookahead
240 {
241 return;
242 }
243 let config = self.config.clone();
244 let client = self.client.clone();
245 let (send, recv) = bounded(1);
246 self.pool.spawn(move || {
247 let stage = &config.stages[stage_index];
248 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
249 readers::read(
250 &stage.sources[source].files[file],
251 stage.sources[source]
252 .encoding
253 .as_ref()
254 .or(stage.encoding.as_ref())
255 .unwrap(),
256 &client,
257 stage.sources[source]
258 .fields
259 .as_deref()
260 .unwrap_or(&config.fields),
261 row,
262 false,
263 &config,
264 )
265 }))
266 .unwrap_or_else(|_| Err(anyhow::anyhow!("shard reader panicked")));
267 let result = result
268 .map_err(|error| anyhow::anyhow!("stage={stage_index} source={source}: {error:#}"));
269 let _ = send.send(result);
270 });
271 self.pending.insert(key, recv);
272 }
273 fn lookahead(&mut self, source: usize, file: usize) {
274 if self.window == Some((self.state.stage, source, file)) {
275 return;
276 }
277 self.window = Some((self.state.stage, source, file));
278 let stage = &self.config.stages[self.state.stage];
279 let (count, infinite) = (stage.sources[source].files.len(), stage.infinite);
280 let window = self.config.threads.max(2);
281 let ahead = if infinite {
282 window.min(count)
283 } else {
284 window.min(count - file)
285 };
286 for k in 0..ahead {
287 self.schedule(
288 self.state.stage,
289 source,
290 self.orders[self.state.stage][source][(file + k) % count],
291 if k == 0 {
292 self.state.positions[source].row
293 } else {
294 0
295 },
296 k == 0,
297 );
298 }
299 }
300 fn load(&mut self, stage_index: usize, source: usize, file: usize, row: usize) -> Result<()> {
301 let key = (stage_index, source, file);
302 if self.cache.get(&key).is_some_and(|s| s.offset <= row) {
303 return Ok(());
304 }
305 self.evict(key)?;
306 if !self.pending.contains_key(&key) {
307 ensure!(
308 self.cache.len() + self.pending.len() < self.config.threads * self.config.lookahead,
309 "reader slots exhausted"
310 );
311 self.schedule(stage_index, source, file, row, true);
312 }
313 let wait = crate::profile::Timer::new(7);
314 let result = self
315 .pending
316 .remove(&key)
317 .unwrap()
318 .recv()
319 .map_err(|_| anyhow::anyhow!("shard worker stopped"))??;
320 drop(wait);
321 if result.offset > row {
322 drop(result);
323 return self.load(stage_index, source, file, row);
324 }
325 self.cache.insert(key, result);
326 Ok(())
327 }
328 fn retain_row(&mut self, c: Coordinate) {
329 self.rows.insert(c, None);
330 }
331 fn evict(&mut self, key: Key) -> Result<()> {
332 if let Some(shard) = self.cache.get(&key) {
333 for (c, row) in &mut self.rows {
334 if row.is_none() && (c.stage, c.source, c.file) == key {
335 *row = Some(arrow::compute::interleave_record_batch(
336 &[&shard.columns],
337 &[(0, c.row - shard.offset)],
338 )?);
339 }
340 }
341 }
342 self.cache.remove(&key);
343 Ok(())
344 }
345 fn restore_rows(&mut self) -> Result<()> {
346 let mut groups = std::collections::BTreeMap::<Key, Vec<Coordinate>>::new();
347 for c in self.state.buffer.iter().chain(&self.state.partial) {
348 groups
349 .entry((c.stage, c.source, c.file))
350 .or_default()
351 .push(c.clone());
352 }
353 let heads: HashSet<_> = self.heads().collect();
354 let mut groups: Vec<_> = groups.into_iter().collect();
355 groups.sort_by_key(|(key, _)| heads.contains(key));
356 for (key, coordinates) in groups {
357 let mut start = coordinates.iter().map(|c| c.row).min().unwrap();
358 if heads.contains(&key) {
359 start = start.min(self.state.positions[key.1].row);
360 }
361 self.load(key.0, key.1, key.2, start)?;
362 for c in coordinates {
363 self.retain_row(c);
364 }
365 if !heads.contains(&key) {
366 self.evict(key)?;
367 }
368 }
369 Ok(())
370 }
371 fn source_next(&mut self, source: usize) -> Result<Option<Coordinate>> {
372 let stage = self.state.stage;
373 let count = self.orders[stage][source].len();
374 let infinite = self.config.stages[stage].infinite;
375 let mut empty = 0;
376 loop {
377 let p = &self.state.positions[source];
378 if p.done {
379 return Ok(None);
380 }
381 let (position, row, epoch) = (p.file, p.row, p.epoch);
382 let file = self.orders[stage][source][position];
383 self.lookahead(source, position);
384 self.load(stage, source, file, row)?;
385 let end = self.cache[&(stage, source, file)].num_rows();
386 let result = if row < end {
387 let c = Coordinate {
388 stage,
389 source,
390 file,
391 row,
392 epoch,
393 };
394 self.retain_row(c.clone());
395 Some(c)
396 } else {
397 None
398 };
399 let p = &mut self.state.positions[source];
400 p.row += usize::from(result.is_some());
401 if p.row >= end {
402 p.row = 0;
403 p.file += 1;
404 if p.file == count {
405 p.epoch += 1;
406 if infinite {
407 p.file = 0;
408 } else {
409 p.done = true;
410 }
411 }
412 let next = (!p.done).then(|| self.orders[stage][source][p.file]);
420 let whole = self.cache[&(stage, source, file)].offset == 0;
421 if next != Some(file) || !whole {
422 self.evict((stage, source, file))?;
423 }
424 }
425 if result.is_some() {
426 return Ok(result);
427 }
428 empty += 1;
429 ensure!(
430 !infinite || empty <= count,
431 "infinite source has no records"
432 );
433 }
434 }
435 fn mixed(&mut self) -> Result<Option<Coordinate>> {
436 loop {
437 let groups = &self.config.stages[self.state.stage].sources;
438 let total: f64 = groups
439 .iter()
440 .zip(&self.state.positions)
441 .filter(|(_, p)| !p.done)
442 .map(|(g, _)| g.weight.unwrap_or(1.0))
443 .sum();
444 if total == 0.0 {
445 return Ok(None);
446 }
447 let mut chosen = 0;
448 if groups.len() > 1 {
449 let draw = (random(
450 self.config.seed,
451 self.state.stage,
452 "mix",
453 0,
454 self.state.mix_counter,
455 ) >> 11) as f64
456 * (1.0 / ((1u64 << 53) as f64));
457 self.state.mix_counter = self.state.mix_counter.wrapping_add(1);
458 let mut target = draw * total;
459 for (i, (g, p)) in groups.iter().zip(&self.state.positions).enumerate() {
460 if p.done || g.weight.unwrap_or(1.0) == 0.0 {
461 continue;
462 }
463 chosen = i;
464 target -= g.weight.unwrap_or(1.0);
465 if target < 0.0 {
466 break;
467 }
468 }
469 }
470 if let Some(c) = self.source_next(chosen)? {
471 return Ok(Some(c));
472 }
473 }
474 }
475 fn coordinate(&mut self) -> Result<Option<Coordinate>> {
476 let capacity = self.config.stages[self.state.stage]
477 .shuffle
478 .as_ref()
479 .map_or(0, |s| s.buffer_rows);
480 if capacity == 0 && self.state.buffer.is_empty() {
481 return self.mixed();
482 }
483 while self.state.buffer.len() < capacity {
484 if let Some(c) = self.mixed()? {
485 self.state.buffer.push(c);
486 } else {
487 break;
488 }
489 }
490 if self.state.buffer.is_empty() {
491 return Ok(None);
492 }
493 let i = (random(
494 self.config.seed,
495 self.state.stage,
496 "shuffle",
497 0,
498 self.state.shuffle_counter,
499 ) % self.state.buffer.len() as u64) as usize;
500 self.state.shuffle_counter = self.state.shuffle_counter.wrapping_add(1);
501 let replacement = if self.state.buffer.len() <= capacity {
502 self.mixed()?
503 } else {
504 None
505 };
506 let result = if let Some(c) = replacement {
507 std::mem::replace(&mut self.state.buffer[i], c)
508 } else {
509 self.state.buffer.swap_remove(i)
510 };
511 Ok(Some(result))
512 }
513 fn retire(&mut self) {
514 let keep: HashSet<_> = self
515 .state
516 .buffer
517 .iter()
518 .chain(&self.state.partial)
519 .collect();
520 self.rows.retain(|c, _| keep.contains(c));
521 }
522 fn materialize(&self, coordinates: &[Coordinate]) -> Result<RecordBatch> {
523 let _assembly = crate::profile::Timer::new(6);
524 let mut rows = Vec::new();
525 let mut shards = HashMap::new();
526 let mut order = Vec::with_capacity(coordinates.len());
527 for c in coordinates {
528 match &self.rows[c] {
529 Some(row) => {
530 order.push((rows.len(), 0));
531 rows.push(row);
532 }
533 None => {
534 let key = (c.stage, c.source, c.file);
535 let shard = &self.cache[&key];
536 let at = *shards.entry(key).or_insert_with(|| {
537 let at = rows.len();
538 rows.push(&shard.columns);
539 at
540 });
541 order.push((at, c.row - shard.offset));
542 }
543 }
544 }
545 Ok(arrow::compute::interleave_record_batch(&rows, &order)?)
546 }
547 fn assemble(&mut self) -> Result<Option<(Vec<Coordinate>, u64)>> {
549 let mut short_epochs = 0;
550 loop {
551 let stage = &self.config.stages[self.state.stage];
552 let size = stage.batch_size;
553 let drop_last = stage.drop_last;
554 let epoch_boundary = stage.sources.len() == 1 && stage.shuffle.is_none();
555 let mut coordinates = std::mem::take(&mut self.state.partial);
556 while coordinates.len() < size {
557 let Some(c) = self.coordinate()? else {
558 break;
559 };
560 if epoch_boundary
561 && coordinates
562 .first()
563 .is_some_and(|first| first.epoch != c.epoch)
564 {
565 self.state.partial.push(c);
566 break;
567 }
568 coordinates.push(c);
569 }
570 if coordinates.len() < size && drop_last {
571 if self.state.batch_index % self.config.world_size == self.config.rank {
574 self.dropped_rows += coordinates.len() as u64;
575 }
576 if self.state.partial.is_empty() {
577 return Ok(None);
578 }
579 short_epochs += 1;
580 ensure!(
581 short_epochs < 2,
582 "dropLast discards every batch: an epoch has fewer than batchSize rows"
583 );
584 continue;
585 }
586 if coordinates.is_empty() {
587 return Ok(None);
588 }
589 let index = self.state.batch_index;
590 self.state.batch_index += 1;
591 return Ok(Some((coordinates, index)));
592 }
593 }
594 pub fn next(&mut self) -> Result<Option<Batch>> {
595 loop {
596 let Some((coordinates, index)) = self.assemble()? else {
597 return Ok(None);
598 };
599 if index % self.config.world_size != self.config.rank {
600 self.retire();
601 continue;
602 }
603 let columns = self.materialize(&coordinates)?;
604 self.retire();
605 while !self
606 .state
607 .batch_index
608 .is_multiple_of(self.config.world_size)
609 {
610 if self.assemble()?.is_none() {
611 self.dropped_rows += coordinates.len() as u64;
615 return Ok(None);
616 }
617 self.retire();
618 }
619 self.state.batches_yielded += 1;
620 self.state.rows_yielded += coordinates.len() as u64;
621 return Ok(Some(Batch {
622 columns,
623 stage: self.state.stage,
624 index,
625 state: self.state.clone(),
626 }));
627 }
628 }
629 pub fn align(&mut self) -> Result<()> {
633 while !self
634 .state
635 .batch_index
636 .is_multiple_of(self.config.world_size)
637 {
638 if self.assemble()?.is_none() {
639 break;
640 }
641 self.retire();
642 }
643 Ok(())
644 }
645}
646
647impl Drop for Engine {
648 fn drop(&mut self) {
649 for (_, pending) in self.pending.drain() {
650 let _ = pending.recv();
651 }
652 }
653}