Skip to content

Commit 8f91261

Browse files
committed
Merge branch 'impl/sds-stack-761' into impl/sds-stack-766
2 parents fcabecb + a2320ff commit 8f91261

4 files changed

Lines changed: 107 additions & 31 deletions

File tree

‎data_plane/src/storage_engines/sketch_db/index/maintenance.rs‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,10 +386,7 @@ impl SketchStore {
386386
.part_cache
387387
.get_or_load(part.part_id)
388388
.map_err(|e| e.to_string())?;
389-
for record in reader.index_records() {
390-
if record.agg_id != sid || record.start_ts < start_ms || record.end_ts > end_ms {
391-
continue;
392-
}
389+
for record in reader.window_records(sid, start_ms, end_ms) {
393390
let entry = reader.load_entry(&record).map_err(|e| e.to_string())?;
394391
if entry.label.as_ref().map_or(0, |label| label.labels.len()) != keys.len() {
395392
return Err("immutable input label arity differs from its descriptor".into());

‎data_plane/src/storage_engines/sketch_db/persistence/cache.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ impl PartCache {
3434
Some(
3535
Cache::builder()
3636
.weigher(|_k: &PartId, v: &LoadedPart| -> u32 {
37-
// Weight = sum of mmap'd bytes. Moka's weigher
37+
// Include mmap bytes and the in-memory window index. The weigher
3838
// returns u32, so clamp large parts.
39-
let len = v.meta.data_len + v.meta.index_len;
39+
let len = v.resident_bytes();
4040
len.min(u32::MAX as u64) as u32
4141
})
4242
.max_capacity(byte_budget)

‎data_plane/src/storage_engines/sketch_db/persistence/part.rs‎

Lines changed: 97 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -430,13 +430,15 @@ pub struct IndexRecord {
430430
pub data_offset: u64,
431431
}
432432

433-
/// mmap-backed reader for a single part. Cheap to construct (three
434-
/// mmaps + one header parse), safe to share across threads via `Arc`.
433+
/// mmap-backed reader with a sorted output/window index built once on open.
434+
/// Safe to share across threads via `Arc`.
435435
pub struct PartReader {
436436
pub meta: PartMeta,
437437
pub part_dir: PathBuf,
438438
data_mmap: Arc<Mmap>,
439439
index_mmap: Arc<Mmap>,
440+
// Older parts preserve flush order, so keep a separate sorted lookup.
441+
window_index: Vec<usize>,
440442
}
441443

442444
impl std::fmt::Debug for PartReader {
@@ -479,12 +481,25 @@ impl PartReader {
479481
)));
480482
}
481483

482-
Ok(Self {
484+
let mut reader = Self {
483485
meta,
484486
part_dir: part_dir.to_path_buf(),
485487
data_mmap: Arc::new(data_mmap),
486488
index_mmap: Arc::new(index_mmap),
487-
})
489+
window_index: Vec::new(),
490+
};
491+
let mut positions: Vec<_> = (0..reader.meta.num_entries as usize).collect();
492+
positions.sort_unstable_by_key(|&position| {
493+
let record = reader.index_record(position);
494+
(
495+
record.agg_id,
496+
record.start_ts,
497+
record.end_ts,
498+
record.data_offset,
499+
)
500+
});
501+
reader.window_index = positions;
502+
Ok(reader)
488503
}
489504

490505
/// Read and verify just the meta.bin header (cheap — 64 bytes).
@@ -537,28 +552,51 @@ impl PartReader {
537552
})
538553
}
539554

540-
/// Return all index records. Small (32 B × num_entries) — cheap to
541-
/// materialize.
542-
pub fn index_records(&self) -> Vec<IndexRecord> {
543-
let n = self.meta.num_entries as usize;
544-
let mut out = Vec::with_capacity(n);
545-
for i in 0..n {
546-
let off = i * INDEX_ENTRY_SIZE;
547-
let agg_id = u64::from_le_bytes(self.index_mmap[off..off + 8].try_into().unwrap());
548-
let start_ts =
549-
u64::from_le_bytes(self.index_mmap[off + 8..off + 16].try_into().unwrap());
550-
let end_ts =
551-
u64::from_le_bytes(self.index_mmap[off + 16..off + 24].try_into().unwrap());
552-
let data_offset =
553-
u64::from_le_bytes(self.index_mmap[off + 24..off + 32].try_into().unwrap());
554-
out.push(IndexRecord {
555-
agg_id,
556-
start_ts,
557-
end_ts,
558-
data_offset,
559-
});
555+
fn index_record(&self, position: usize) -> IndexRecord {
556+
let off = position * INDEX_ENTRY_SIZE;
557+
let read =
558+
|offset| u64::from_le_bytes(self.index_mmap[offset..offset + 8].try_into().unwrap());
559+
IndexRecord {
560+
agg_id: read(off),
561+
start_ts: read(off + 8),
562+
end_ts: read(off + 16),
563+
data_offset: read(off + 24),
560564
}
561-
out
565+
}
566+
567+
/// Preserve disk order for callers that need to inspect the entire part.
568+
pub fn index_records(&self) -> Vec<IndexRecord> {
569+
(0..self.meta.num_entries as usize)
570+
.map(|position| self.index_record(position))
571+
.collect()
572+
}
573+
574+
/// Find records contained in the requested window under one stored-output
575+
/// prefix. Duplicate windows remain visible so callers can reject ambiguity.
576+
pub fn window_records(
577+
&self,
578+
agg_id: u64,
579+
start_ts: u64,
580+
end_ts: u64,
581+
) -> impl Iterator<Item = IndexRecord> + '_ {
582+
let first = self.window_index.partition_point(|&position| {
583+
let record = self.index_record(position);
584+
(record.agg_id, record.start_ts) < (agg_id, start_ts)
585+
});
586+
self.window_index[first..]
587+
.iter()
588+
.map(|&position| self.index_record(position))
589+
.take_while(move |record| record.agg_id == agg_id && record.start_ts <= end_ts)
590+
.filter(move |record| record.end_ts <= end_ts)
591+
}
592+
593+
pub fn resident_bytes(&self) -> u64 {
594+
(self.data_mmap.len() as u64)
595+
.saturating_add(self.index_mmap.len() as u64)
596+
.saturating_add(
597+
(self.window_index.capacity() as u64)
598+
.saturating_mul(std::mem::size_of::<usize>() as u64),
599+
)
562600
}
563601

564602
/// Resolve a single index record into a [`SnapshotEntry`] by reading
@@ -658,6 +696,40 @@ mod tests {
658696
}
659697
}
660698

699+
// Old parts can be unsorted; lookup must isolate the prefix/range while
700+
// preserving duplicate records for the immutable reader's ambiguity check.
701+
#[test]
702+
fn window_lookup_handles_unsorted_parts_duplicates_and_recovery() {
703+
let tmp = TempDir::new().unwrap();
704+
let part_dir = tmp.path().join("lookup");
705+
let mut snapshots = Vec::new();
706+
for id in [99, 42, 1] {
707+
let mut snapshot = make_snapshot();
708+
snapshot.agg_id = id;
709+
snapshot.entries.reverse();
710+
if id == 42 {
711+
snapshot.entries.push(snapshot.entries[1].clone());
712+
}
713+
snapshots.push(snapshot);
714+
}
715+
PartWriter::write_part(&part_dir, 1, &snapshots).unwrap();
716+
for _ in 0..2 {
717+
let reader = PartReader::open(&part_dir).unwrap();
718+
let records = reader.window_records(42, 1_000, 1_500).collect::<Vec<_>>();
719+
assert_eq!(records.len(), 2);
720+
assert!(records.iter().all(|record| record.agg_id == 42
721+
&& record.start_ts == 1_000
722+
&& record.end_ts == 1_500));
723+
assert_ne!(records[0].data_offset, records[1].data_offset);
724+
assert_eq!(reader.window_records(42, 1_500, 2_000).count(), 1);
725+
assert_eq!(reader.window_records(43, 0, u64::MAX).count(), 0);
726+
assert_eq!(reader.window_records(42, 1_001, 1_999).count(), 0);
727+
assert_eq!(reader.window_records(42, 2_000, 1_000).count(), 0);
728+
assert_eq!(reader.index_records()[0].agg_id, 99);
729+
assert!(reader.resident_bytes() > reader.meta.data_len + reader.meta.index_len);
730+
}
731+
}
732+
661733
#[test]
662734
fn part_round_trip_writes_and_reads_back() {
663735
let tmp = TempDir::new().unwrap();

‎docs/developer_docs/query-engine/catalog-physical-plan-runtime.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,10 @@ state construction, publication, bound readout and restart of both the store and
5959
resolver. Automatic PromQL physical-candidate installation, Rate-to-heap storage
6060
execution and spatial latest-value heap execution remain pending. This test does
6161
not establish those paths.
62+
63+
Bound frozen reads resolve the stored-output/group prefix through the persistent
64+
series resolver. Each cached part builds a sorted output/window index once on
65+
open, including older parts written in flush order. Range reads inspect only the
66+
matching prefix and start-time range, then validate end times and exact coverage.
67+
Duplicate windows remain visible and are rejected as ambiguous. The index memory
68+
counts toward the part-cache budget; the on-disk part format is unchanged.

0 commit comments

Comments
 (0)