From 2840dfe81cebbbae508f76df8963cdc21315f354 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 13:40:49 +0000 Subject: [PATCH] =?UTF-8?q?osm=20lance:=20fix=20the=20total=20tile=20outag?= =?UTF-8?q?e=20=E2=80=94=20verify=20the=20P-CACHE-4=20anchors=20before=20s?= =?UTF-8?q?erving?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of every /api/osm/geometry/tile-bin/* request returning 400 in production: WriteParams defaults max_rows_per_file to 1M, so Berlin's 2,525,052 rows were written as THREE Lance fragments. locate_row_column found row 0 at offset 0 of one fragment and reported the full 1.29 GB slab length against a file that only held ~40% of it; open_slab's .min() clamp then silently truncated the slice to the fragment's file size — footer bytes included — which is not a multiple of NODE_ROW_STRIDE, so RowSlab::new rejected every request. lance-graph's own soa_verbatim.rs warns about exactly this: "a split changes what an mmap offset means." The serving contract stays as directed: hydrate once from S3, sink into Lance ONCE, serve zero-copy lazily from the .lance bytes on disk (the mmap+offset mechanism soa_verbatim.rs proves). What changes is that every precondition that test asserts is now enforced at boot instead of assumed: - write_lance: max_rows_per_file = rows (+ raised max_bytes_per_file) — the dataset is always a single fragment, whatever the region size. - reopen_if_warm: also requires fragment count == 1, so the broken three-fragment Berlin dataset on volume01 is reconverted once and then stays warm. - locate_row_column: productionizes the verbatim test's assertions — sole data file, stride-aligned run start, head anchor, PLUS a tail anchor (the slab's last row must sit at offset + length - 512, in bounds). A coincidental head match cannot also anchor the tail at the computed distance; a partial fragment fails the bounds check outright. - open_slab: exact bounds instead of the .min() clamp — any doubt falls back to the raw .soa file rather than serving a truncated slice. - ensure_lance_local: digest via mmap instead of fs::read — the warm boot path no longer pays a 1.29 GiB heap allocation just to hash the slab. New falsifiers, each verified against the failure it kills: multi-fragment split (the production outage), truncated sole fragment, tail-anchor mismatch, unaligned run start. Tests: osm_lance 10/10, osm_features 18/18 passing (run for real this time — the earlier linker-SIGBUS environment issue is resolved). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NMeiLmtDKhomJNSo2ecbJw --- crates/cockpit-server/src/main.rs | 28 +- crates/cockpit-server/src/osm_features.rs | 93 +++--- crates/cockpit-server/src/osm_lance.rs | 333 +++++++++++++++++----- 3 files changed, 339 insertions(+), 115 deletions(-) diff --git a/crates/cockpit-server/src/main.rs b/crates/cockpit-server/src/main.rs index bfbebb5cd0..6447aa2c92 100644 --- a/crates/cockpit-server/src/main.rs +++ b/crates/cockpit-server/src/main.rs @@ -199,16 +199,17 @@ async fn main() { // SAFETY: single-threaded startup, before any task or listener exists. unsafe { std::env::set_var("OSM_SLAB_PATH", &path) }; - // Load the map into lance-graph: convert the hydrated `.soa` slab - // into a Lance dataset on the same persistent volume (medcare-rs's - // own hydrate-into-Lance shape; see `osm_lance`'s module doc for the - // full recipe and why it stays local rather than S3-backed), then — - // if the row column can be located verbatim inside it — repoint the - // tile-serving mmap AT the Lance-owned bytes instead of the raw - // `.soa` file. `OSM_SLAB_PATH` itself is never touched (it stays - // the sidecar anchor and the fallback row source), so any failure - // along this chain leaves the map served exactly as it was before - // this existed. + // The serving contract (operator-directed): hydrate once from S3, + // sink into Lance ONCE (single-fragment, warm-skip via row count + + // digest + fragment count — medcare-rs's hydrate-into-Lance shape, + // see `osm_lance`'s module doc), then serve the map zero-copy and + // LAZILY from the `.lance` bytes on disk — mmap + verified offset, + // the P-CACHE-4 mechanism lance-graph's `soa_verbatim.rs` proves. + // `locate_row_column` only resolves after four verifications (sole + // fragment, aligned start, head + tail anchors); on ANY doubt the + // env vars stay unset and the raw `.soa` file keeps serving. + // `OSM_SLAB_PATH` itself is never repointed — it stays the + // `.books`/`.chains` sidecar anchor and the fallback row source. match osm_lance::ensure_lance_local(&path).await { Some(dataset_dir) => match osm_lance::locate_row_column(&dataset_dir, &path) { Some((data_file, offset, len)) => { @@ -220,12 +221,13 @@ async fn main() { } tracing::info!( file = %data_file.display(), offset, len, - "osm lance: tile-serving now reads row bytes from the Lance dataset" + "osm lance: tile-serving reads row bytes from the Lance dataset \ + (verified: sole fragment, aligned, head+tail anchored)" ); } None => tracing::warn!( - "osm lance: dataset converted, but its row column could not be \ - located verbatim on disk; the raw .soa slab still serves the map" + "osm lance: dataset present but not servable via mmap+offset; \ + the raw .soa slab keeps serving the map" ), }, None => tracing::warn!( diff --git a/crates/cockpit-server/src/osm_features.rs b/crates/cockpit-server/src/osm_features.rs index 2361b0dbdc..d82ff6b7c7 100644 --- a/crates/cockpit-server/src/osm_features.rs +++ b/crates/cockpit-server/src/osm_features.rs @@ -257,55 +257,74 @@ fn overview_sample( out } -type SlabMmap = (memmap2::Mmap, usize, Option); +type SlabMmap = (memmap2::Mmap, usize, usize); static SLAB_MMAP: OnceLock> = OnceLock::new(); /// The baked slab, mmap'd once and cached for the process lifetime. `None` -/// when no row-byte source resolves or the file can't be opened/mapped — -/// the handler reports that as 503, not a panic. +/// when no row-byte source resolves — the handler reports that as 503, not +/// a panic. /// -/// Two sources, in priority order: `OSM_SLAB_ROW_FILE` (set at boot by -/// [`crate::osm_lance::locate_row_column`] once the hydrated slab has been -/// converted into a Lance dataset AND its row column located verbatim -/// inside the dataset's own on-disk data file — see that module's doc for -/// why this is sound), falling back to `OSM_SLAB_PATH` (the raw `.soa` -/// file, unconditionally set by `osm_slab_hydrate::ensure_slab_local`). -/// `OSM_SLAB_PATH` itself is NEVER repointed at the Lance file: it also -/// anchors the `.books`/`.chains` sidecar paths -/// ([`open_books`], [`open_chains`]), which live beside the raw bake, not -/// inside the Lance dataset's `data/` directory. +/// # The serving contract: hydrate once → sink into Lance once → mmap the +/// # `.lance` bytes lazily from disk (operator-directed) /// -/// `OSM_SLAB_ROW_OFFSET`/`OSM_SLAB_ROW_LEN` bound the mapped slice to -/// EXACTLY the row column when the mapped file is a Lance data file — it -/// carries Lance's own header/footer bytes around the row run, which -/// `RowSlab::new` would otherwise reject outright (it requires the slice -/// length be an exact multiple of `NODE_ROW_STRIDE`, not merely a prefix -/// of one). Both absent (the raw `.soa` case) maps the whole file, -/// unchanged from before this pair of variables existed. +/// Two sources, in priority order: +/// +/// 1. `OSM_SLAB_ROW_FILE` + `OSM_SLAB_ROW_OFFSET` + `OSM_SLAB_ROW_LEN` — +/// the Lance dataset's own sole data file, set at boot by +/// [`crate::osm_lance::locate_row_column`] only after FOUR verifications +/// (sole fragment, aligned start, head anchor, tail anchor — the same +/// assertions lance-graph's `soa_verbatim.rs` test makes; see that +/// module's doc for the outage the unverified version caused). The mmap +/// slice is bounded to EXACTLY `offset..offset + len`; the pages load +/// lazily through the page cache — nothing slab-sized is read into heap. +/// 2. `OSM_SLAB_PATH` — the raw `.soa` file, unconditionally set by +/// `osm_slab_hydrate::ensure_slab_local`. `OSM_SLAB_PATH` is NEVER +/// repointed at the Lance file: it also anchors the `.books`/`.chains` +/// sidecar paths ([`open_books`], [`open_chains`]), which live beside +/// the raw bake, not inside the Lance dataset's `data/` directory. +/// +/// Strictness over clamping: the Lance-file branch uses exact bounds +/// (`get(off..off + len)`), and ANY failure — unset/garbled vars, a file +/// that shrank, a slice that isn't whole rows — falls back to the raw +/// `.soa` path rather than serving a truncated slice. The pre-fix version +/// clamped (`end.min(m.len())`) and served a footer-polluted slice whose +/// length wasn't a multiple of `NODE_ROW_STRIDE`; `RowSlab::new` then +/// rejected every request. fn open_slab() -> Option<&'static [u8]> { - let cell = SLAB_MMAP.get_or_init(|| { - let path = std::env::var("OSM_SLAB_ROW_FILE") - .ok() - .or_else(|| std::env::var("OSM_SLAB_PATH").ok())?; - let file = std::fs::File::open(&path).ok()?; + fn map_range(path: &str, off: usize, len: usize) -> Option<(memmap2::Mmap, usize, usize)> { + if len == 0 || !len.is_multiple_of(lance_graph_contract::canonical_node::NODE_ROW_STRIDE) { + return None; + } + let file = std::fs::File::open(path).ok()?; // SAFETY: read-only mapping of a baked, immutable artifact. Nothing // else in this process (or expected to run alongside it) mutates the // file while it's mapped — same assumption `RowSlab`'s own docs make // about a Lance-returned buffer. let mmap = unsafe { memmap2::Mmap::map(&file) }.ok()?; - let offset: usize = std::env::var("OSM_SLAB_ROW_OFFSET") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(0); - let len: Option = std::env::var("OSM_SLAB_ROW_LEN") - .ok() - .and_then(|v| v.parse().ok()); - Some((mmap, offset, len)) + // Exact bounds — a shrunken or wrong file declines, never clamps. + off.checked_add(len).filter(|&end| end <= mmap.len())?; + Some((mmap, off, len)) + } + + let cell = SLAB_MMAP.get_or_init(|| { + let lance = || -> Option<(memmap2::Mmap, usize, usize)> { + let path = std::env::var("OSM_SLAB_ROW_FILE").ok()?; + let off: usize = std::env::var("OSM_SLAB_ROW_OFFSET").ok()?.parse().ok()?; + let len: usize = std::env::var("OSM_SLAB_ROW_LEN").ok()?.parse().ok()?; + map_range(&path, off, len) + }; + let raw = || -> Option<(memmap2::Mmap, usize, usize)> { + let path = std::env::var("OSM_SLAB_PATH").ok()?; + let file = std::fs::File::open(&path).ok()?; + // SAFETY: as above. + let mmap = unsafe { memmap2::Mmap::map(&file) }.ok()?; + let len = mmap.len(); + Some((mmap, 0, len)) + }; + lance().or_else(raw) }); - cell.as_ref().and_then(|(m, off, len)| { - let end = len.map_or(m.len(), |l| off + l); - m.get(*off..end.min(m.len())) - }) + cell.as_ref() + .and_then(|(m, off, len)| m.get(*off..*off + *len)) } #[derive(Debug, Serialize, PartialEq)] diff --git a/crates/cockpit-server/src/osm_lance.rs b/crates/cockpit-server/src/osm_lance.rs index b93039afc9..bc7129b2e9 100644 --- a/crates/cockpit-server/src/osm_lance.rs +++ b/crates/cockpit-server/src/osm_lance.rs @@ -96,22 +96,34 @@ const OSM_BAKE_CLASSID: &str = "00000000"; pub async fn ensure_lance_local(slab_path: &Path) -> Option { let dest = slab_path.with_extension("lance"); - let bytes = match std::fs::read(slab_path) { - Ok(b) => b, + // Digest over an mmap, not `fs::read`: the warm path must not pay a + // slab-sized heap allocation (1.29 GiB for Berlin) just to compute a + // hash — the pages stream through the page cache and stay reclaimable. + let file = match std::fs::File::open(slab_path) { + Ok(f) => f, Err(e) => { - tracing::error!(path = %slab_path.display(), error = %e, "osm lance: cannot read slab"); + tracing::error!(path = %slab_path.display(), error = %e, "osm lance: cannot open slab"); + return None; + } + }; + // SAFETY: read-only mapping of the baked, immutable artifact. + let map = match unsafe { memmap2::Mmap::map(&file) } { + Ok(m) => m, + Err(e) => { + tracing::error!(path = %slab_path.display(), error = %e, "osm lance: cannot mmap slab"); return None; } }; - if bytes.is_empty() || !bytes.len().is_multiple_of(NODE_ROW_STRIDE) { + if map.is_empty() || !map.len().is_multiple_of(NODE_ROW_STRIDE) { tracing::error!( - path = %slab_path.display(), len = bytes.len(), + path = %slab_path.display(), len = map.len(), "osm lance: slab is not a whole number of {NODE_ROW_STRIDE}-byte rows; refusing" ); return None; } - let rows = bytes.len() / NODE_ROW_STRIDE; - let digest_hex = format!("{:016x}", osm_soa_bake::codebook::hash_slab(&bytes)); + let rows = map.len() / NODE_ROW_STRIDE; + let digest_hex = format!("{:016x}", osm_soa_bake::codebook::hash_slab(&map)); + drop(map); if let Some(warm) = reopen_if_warm(&dest, rows, &digest_hex).await { tracing::info!( @@ -121,6 +133,16 @@ pub async fn ensure_lance_local(slab_path: &Path) -> Option { return Some(warm); } + // Cold path only: the write genuinely needs an owned Vec (adopted by + // `Buffer::from_vec`), so the slab-sized allocation is paid exactly once + // per conversion, never per boot. + let bytes = match std::fs::read(slab_path) { + Ok(b) => b, + Err(e) => { + tracing::error!(path = %slab_path.display(), error = %e, "osm lance: cannot read slab"); + return None; + } + }; tracing::info!( path = %dest.display(), rows, "osm lance: converting the .soa slab into a Lance dataset (zero-copy import)" @@ -156,13 +178,21 @@ async fn reopen_if_warm(dest: &Path, rows: usize, digest_hex: &str) -> Option Option<(PathBuf, usize, usize)> { + use std::io::{Read, Seek, SeekFrom}; + let length = std::fs::metadata(slab_path).ok()?.len() as usize; if length == 0 || !length.is_multiple_of(NODE_ROW_STRIDE) { return None; } - let probe_len = NODE_ROW_STRIDE.min(length); - let mut probe = vec![0u8; probe_len]; - { - use std::io::Read; - std::fs::File::open(slab_path) - .ok()? - .read_exact(&mut probe) - .ok()?; - } + let mut slab = std::fs::File::open(slab_path).ok()?; + let mut first_row = vec![0u8; NODE_ROW_STRIDE]; + slab.read_exact(&mut first_row).ok()?; + let mut last_row = vec![0u8; NODE_ROW_STRIDE]; + slab.seek(SeekFrom::Start((length - NODE_ROW_STRIDE) as u64)) + .ok()?; + slab.read_exact(&mut last_row).ok()?; + // (1) Exactly one .lance data file. let data_dir = dataset_dir.join("data"); - let entries = std::fs::read_dir(&data_dir).ok()?; - for entry in entries.flatten() { - let p = entry.path(); - if p.extension().and_then(|e| e.to_str()) != Some("lance") { - continue; - } - if let Some(off) = search_head_for_probe(&p, &probe) { - tracing::info!( - file = %p.display(), offset = off, length, - "osm lance: row column located verbatim inside the Lance data file" - ); - return Some((p, off, length)); - } + let mut data_files: Vec = std::fs::read_dir(&data_dir) + .ok()? + .flatten() + .map(|e| e.path()) + .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("lance")) + .collect(); + if data_files.len() != 1 { + tracing::warn!( + dir = %data_dir.display(), count = data_files.len(), + "osm lance: dataset has {} data files, need exactly 1 for mmap serving; \ + the raw .soa slab keeps serving the map", + data_files.len() + ); + return None; } + let p = data_files.pop().expect("len checked"); + + // (2) Row 0 near the head. + let Some(off) = search_head_for_probe(&p, &first_row) else { + tracing::warn!( + file = %p.display(), + "osm lance: row 0 not found verbatim in the data file head; \ + the raw .soa slab keeps serving the map" + ); + return None; + }; + + // (3) Stride-aligned start. + if !off.is_multiple_of(NODE_ROW_STRIDE) { + tracing::warn!( + file = %p.display(), offset = off, + "osm lance: row run starts unaligned to the row stride; \ + the raw .soa slab keeps serving the map" + ); + return None; + } + + // (4) Bounds + tail anchor. + let file_len = std::fs::metadata(&p).ok()?.len() as usize; + let end = off.checked_add(length)?; + if end > file_len { + tracing::warn!( + file = %p.display(), offset = off, length, file_len, + "osm lance: data file is shorter than offset + slab length \ + (a fragment holding only part of the column); \ + the raw .soa slab keeps serving the map" + ); + return None; + } + let mut tail = vec![0u8; NODE_ROW_STRIDE]; + let mut data = std::fs::File::open(&p).ok()?; + data.seek(SeekFrom::Start((end - NODE_ROW_STRIDE) as u64)) + .ok()?; + data.read_exact(&mut tail).ok()?; + if tail != last_row { + tracing::warn!( + file = %p.display(), offset = off, length, + "osm lance: the slab's last row does not anchor at offset + length; \ + the head match was coincidental; the raw .soa slab keeps serving the map" + ); + return None; + } + tracing::info!( - dir = %data_dir.display(), - "osm lance: no Lance data file's head contained the row column verbatim; \ - the raw .soa slab will keep serving the map" + file = %p.display(), offset = off, length, + "osm lance: row column verified verbatim inside the Lance data file \ + (sole fragment, aligned start, head + tail anchors)" ); - None + Some((p, off, length)) } /// The bounded byte search itself, split out so it can be unit-tested @@ -376,33 +478,58 @@ mod tests { assert!(search_head_for_probe(&file_path, &probe).is_none()); } - /// End-to-end over [`locate_row_column`] itself, with a fake dataset - /// directory laid out the way a real `Dataset::write` output is - /// (`/data/.lance`) — proves the directory walk, the - /// extension filter, and the length-from-slab-metadata computation all - /// compose correctly, not just the byte search in isolation. + /// A synthetic slab whose rows are DISTINGUISHABLE — row `i`'s bytes are + /// a function of `i` — so a tail anchor genuinely proves the last row is + /// where it is claimed to be, rather than matching any repeated filler. + fn distinct_slab(rows: usize) -> Vec { + let mut bytes = vec![0u8; rows * NODE_ROW_STRIDE]; + for (i, chunk) in bytes.chunks_exact_mut(NODE_ROW_STRIDE).enumerate() { + for (j, b) in chunk.iter_mut().enumerate() { + *b = ((i * 31 + j * 7) % 251) as u8; + } + } + bytes + } + + /// Lay out `/data/` the way `Dataset::write` does, with the slab + /// embedded at a chosen (stride-aligned) offset in a sole fragment. + fn fake_dataset( + dir: &Path, + slab_bytes: &[u8], + header: usize, + footer: usize, + ) -> (PathBuf, PathBuf) { + let dataset_dir = dir.join("region.lance"); + let data_dir = dataset_dir.join("data"); + std::fs::create_dir_all(&data_dir).expect("mkdir"); + // A non-`.lance` file must be skipped, not mistaken for a fragment. + std::fs::write(data_dir.join("README.txt"), b"not a fragment").expect("write"); + let mut fragment = vec![0x99u8; header]; + fragment.extend_from_slice(slab_bytes); + fragment.extend(vec![0x88u8; footer]); + let frag_path = data_dir.join("frag-0.lance"); + std::fs::write(&frag_path, &fragment).expect("write fragment"); + (dataset_dir, frag_path) + } + + /// End-to-end over [`locate_row_column`]: sole fragment, aligned start, + /// head + tail anchors — all four verifications passing together. #[test] fn locate_row_column_finds_the_fragment_and_reports_the_slabs_own_length() { let dir = tempfile::tempdir().expect("tempdir"); let slab_path = dir.path().join("region.soa"); let rows = 3usize; - let slab_bytes = vec![0x7Cu8; rows * NODE_ROW_STRIDE]; + let slab_bytes = distinct_slab(rows); std::fs::write(&slab_path, &slab_bytes).expect("write slab"); - let dataset_dir = dir.path().join("region.lance"); - let data_dir = dataset_dir.join("data"); - std::fs::create_dir_all(&data_dir).expect("mkdir"); - // A non-`.lance` file must be skipped, not mistaken for a fragment. - std::fs::write(data_dir.join("README.txt"), b"not a fragment").expect("write"); - let mut fragment = vec![0x99u8; 64]; // Lance's own header bytes - fragment.extend_from_slice(&slab_bytes); - fragment.extend(vec![0x88u8; 16]); // Lance's own footer bytes - std::fs::write(data_dir.join("frag-0.lance"), &fragment).expect("write fragment"); + // Header must be stride-aligned for check (3) — a real Lance data + // file puts the run at offset 0 (measured); 512 exercises off != 0. + let (dataset_dir, frag_path) = fake_dataset(dir.path(), &slab_bytes, NODE_ROW_STRIDE, 16); let (found_path, offset, length) = locate_row_column(&dataset_dir, &slab_path).expect("must locate the fragment"); - assert_eq!(found_path, data_dir.join("frag-0.lance")); - assert_eq!(offset, 64); + assert_eq!(found_path, frag_path); + assert_eq!(offset, NODE_ROW_STRIDE); assert_eq!(length, rows * NODE_ROW_STRIDE); } @@ -412,7 +539,7 @@ mod tests { fn locate_row_column_declines_when_no_fragment_matches() { let dir = tempfile::tempdir().expect("tempdir"); let slab_path = dir.path().join("region.soa"); - std::fs::write(&slab_path, vec![0x7Cu8; NODE_ROW_STRIDE]).expect("write slab"); + std::fs::write(&slab_path, distinct_slab(1)).expect("write slab"); let dataset_dir = dir.path().join("region.lance"); let data_dir = dataset_dir.join("data"); @@ -421,4 +548,80 @@ mod tests { assert!(locate_row_column(&dataset_dir, &slab_path).is_none()); } + + /// THE production outage's falsifier: a multi-fragment dataset — each + /// data file holding only part of the slab — must be declined outright, + /// even though row 0 IS found verbatim in the first fragment. The + /// pre-fix version accepted this and reported the full slab length + /// against a fragment that couldn't hold it; every tile request then + /// failed with 400. + #[test] + fn locate_row_column_declines_a_multi_fragment_split() { + let dir = tempfile::tempdir().expect("tempdir"); + let slab_path = dir.path().join("region.soa"); + let slab_bytes = distinct_slab(4); + std::fs::write(&slab_path, &slab_bytes).expect("write slab"); + + let dataset_dir = dir.path().join("region.lance"); + let data_dir = dataset_dir.join("data"); + std::fs::create_dir_all(&data_dir).expect("mkdir"); + let half = slab_bytes.len() / 2; + std::fs::write(data_dir.join("frag-0.lance"), &slab_bytes[..half]).expect("write"); + std::fs::write(data_dir.join("frag-1.lance"), &slab_bytes[half..]).expect("write"); + + assert!(locate_row_column(&dataset_dir, &slab_path).is_none()); + } + + /// A sole fragment that holds only a PREFIX of the slab (same failure + /// mode as the split, reached via the bounds check instead of the file + /// count) must be declined — offset + slab length exceeds the file. + #[test] + fn locate_row_column_declines_a_truncated_fragment() { + let dir = tempfile::tempdir().expect("tempdir"); + let slab_path = dir.path().join("region.soa"); + let slab_bytes = distinct_slab(4); + std::fs::write(&slab_path, &slab_bytes).expect("write slab"); + + let half = slab_bytes.len() / 2; + let (dataset_dir, _) = fake_dataset(dir.path(), &slab_bytes[..half], 0, 0); + + assert!(locate_row_column(&dataset_dir, &slab_path).is_none()); + } + + /// A head match whose TAIL does not anchor must be declined: the file + /// is long enough, row 0 matches, but the bytes at offset + length - 512 + /// are not the slab's last row — the coincidental-collision case the + /// two-anchor design exists to kill. + #[test] + fn locate_row_column_declines_when_the_tail_does_not_anchor() { + let dir = tempfile::tempdir().expect("tempdir"); + let slab_path = dir.path().join("region.soa"); + let slab_bytes = distinct_slab(3); + std::fs::write(&slab_path, &slab_bytes).expect("write slab"); + + // Fragment = row 0 verbatim, then garbage of the right total size. + let mut fragment = slab_bytes[..NODE_ROW_STRIDE].to_vec(); + fragment.extend(vec![0xEEu8; slab_bytes.len() - NODE_ROW_STRIDE + 64]); + let dataset_dir = dir.path().join("region.lance"); + let data_dir = dataset_dir.join("data"); + std::fs::create_dir_all(&data_dir).expect("mkdir"); + std::fs::write(data_dir.join("frag-0.lance"), &fragment).expect("write"); + + assert!(locate_row_column(&dataset_dir, &slab_path).is_none()); + } + + /// An unaligned run start must be declined — `RowSlab::rows()` needs the + /// mapped slice 64-byte aligned, and a page-aligned mmap base plus a + /// stride-aligned offset is what guarantees it. + #[test] + fn locate_row_column_declines_an_unaligned_run() { + let dir = tempfile::tempdir().expect("tempdir"); + let slab_path = dir.path().join("region.soa"); + let slab_bytes = distinct_slab(3); + std::fs::write(&slab_path, &slab_bytes).expect("write slab"); + + let (dataset_dir, _) = fake_dataset(dir.path(), &slab_bytes, 100, 16); + + assert!(locate_row_column(&dataset_dir, &slab_path).is_none()); + } }