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