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