Skip to content

Commit 27a97de

Browse files
authored
Logdb: efficient tail scan rfc and preliminary work (opendata-oss#487)
RFC for efficient tail scans and preliminary pieces of implementation.
1 parent a0c1190 commit 27a97de

9 files changed

Lines changed: 915 additions & 34 deletions

File tree

log/rfcs/0007-efficient-tail-scans.md

Lines changed: 298 additions & 0 deletions
Large diffs are not rendered by default.

log/src/log.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ impl LogDb {
445445
storage.slate_read(),
446446
initial_next_sequence,
447447
segment_cache,
448+
initial_next_sequence,
448449
)));
449450

450451
let (epoch_watcher, durable_sequence_rx, read_subscriber_task) = spawn_subscriber(
@@ -2818,6 +2819,78 @@ mod tests {
28182819
assert_eq!(log.count(Bytes::from("k"), 7..).await.unwrap(), 3);
28192820
}
28202821

2822+
// ---- next_sequence (resumable scan) tests ----
2823+
2824+
#[tokio::test]
2825+
async fn next_sequence_lifts_idle_key_to_global_frontier() {
2826+
// Writes land only on a hot key. A drained scan of an idle key should
2827+
// still resume at the global frontier — the whole point of the
2828+
// cross-key lift — rather than at 0.
2829+
let log = LogDb::open(test_config()).await.unwrap();
2830+
log.try_append(vec![
2831+
Record {
2832+
key: Bytes::from("hot"),
2833+
value: Bytes::from("v0"),
2834+
},
2835+
Record {
2836+
key: Bytes::from("hot"),
2837+
value: Bytes::from("v1"),
2838+
},
2839+
Record {
2840+
key: Bytes::from("hot"),
2841+
value: Bytes::from("v2"),
2842+
},
2843+
])
2844+
.await
2845+
.unwrap();
2846+
2847+
// `scan` runs `sync_reads`, so the read view (and its frontier) reflects
2848+
// the appends above before the iterator is built.
2849+
let mut iter = log.scan(Bytes::from("cold"), ..).await.unwrap();
2850+
assert!(
2851+
iter.next().await.unwrap().is_none(),
2852+
"the cold key has no records"
2853+
);
2854+
assert_eq!(
2855+
iter.next_sequence(),
2856+
3,
2857+
"idle-key scan resumes at the frontier set by the hot key's writes"
2858+
);
2859+
}
2860+
2861+
#[tokio::test]
2862+
async fn next_sequence_resumes_past_consumed_tail() {
2863+
let log = LogDb::open(test_config()).await.unwrap();
2864+
log.try_append(vec![
2865+
Record {
2866+
key: Bytes::from("k"),
2867+
value: Bytes::from("v0"),
2868+
},
2869+
Record {
2870+
key: Bytes::from("k"),
2871+
value: Bytes::from("v1"),
2872+
},
2873+
])
2874+
.await
2875+
.unwrap();
2876+
2877+
let mut iter = log.scan(Bytes::from("k"), ..).await.unwrap();
2878+
let mut last = None;
2879+
while let Some(e) = iter.next().await.unwrap() {
2880+
last = Some(e.sequence);
2881+
}
2882+
assert_eq!(last, Some(1));
2883+
// Drained: resume one past the last record, at the frontier.
2884+
let resume = iter.next_sequence();
2885+
assert_eq!(resume, 2);
2886+
2887+
// Resuming from there sees nothing new and reports the same frontier —
2888+
// the idle-poll steady state.
2889+
let mut iter2 = log.scan(Bytes::from("k"), resume..).await.unwrap();
2890+
assert!(iter2.next().await.unwrap().is_none());
2891+
assert_eq!(iter2.next_sequence(), 2);
2892+
}
2893+
28212894
// ---- durable_sequence tests ----
28222895

28232896
#[tokio::test]

0 commit comments

Comments
 (0)