use std::sync::Arc;
use std::time::Duration;
use arc_swap::ArcSwap;
use tessarikv_types::{SequenceNumber, ValueKind};
use super::block_cache::BlockCache;
use super::flush_signals::FlushSignals;
use super::pressure::{self, Load, Pressure};
use super::read_tests::{file, user_key};
use super::view::View;
use crate::memtable::Memtable;
use crate::{Error, KeyspaceOptions};
const RATE: u64 = 1024 * 1024;
fn keyspace() -> KeyspaceOptions {
KeyspaceOptions::builder()
.level0_compaction_trigger(2)
.level0_slowdown_trigger(3)
.level0_stop_trigger(4)
.build()
.expect("valid")
}
fn view_with(tables: u64) -> View {
let cache = Arc::new(BlockCache::new(0, 256));
let files = (1..=tables)
.rev()
.map(|id| {
let table = Memtable::new();
table
.insert(
SequenceNumber::new(id).unwrap(),
ValueKind::Value,
&user_key(1),
b"v",
)
.unwrap();
file(&table, id, &cache)
})
.collect();
View::recovered(vec![(0, Memtable::new())], std::sync::Arc::new([]), files)
}
fn signals() -> FlushSignals {
FlushSignals::new(keyspace().max_immutable_memtables(), RATE, 0)
}
fn weigh(signals: &FlushSignals, view: &View) {
signals.set_load(pressure::of(view, [(0, &keyspace())]));
}
fn press(signals: &FlushSignals, pressure: Pressure) {
signals.set_load(Load { pressure, owed: 0 });
}
fn refusal(result: crate::Result<()>) -> &'static str {
match result {
Err(Error::WriteStalled { reason }) => reason,
other => panic!("expected a stalled write, got {other:?}"),
}
}
#[tokio::test]
async fn level_zero_past_its_slowdown_trigger_refuses_a_write_that_must_not_wait() {
let signals = signals();
let below = ArcSwap::from_pointee(view_with(2));
weigh(&signals, &below.load());
signals
.admit(&below, true, 1)
.await
.expect("under the slowdown trigger");
let slowed = ArcSwap::from_pointee(view_with(3));
weigh(&signals, &slowed.load());
let reason = refusal(signals.admit(&slowed, true, 1).await);
assert!(reason.contains("slowdown"), "{reason}");
signals
.admit(&slowed, false, 1)
.await
.expect("a write that may wait goes after its delay");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn level_zero_at_its_stop_trigger_holds_writes_until_an_install_takes_it_under() {
let signals = Arc::new(signals());
let view = Arc::new(ArcSwap::from_pointee(view_with(4)));
weigh(&signals, &view.load());
let reason = refusal(signals.admit(&view, true, 1).await);
assert!(reason.contains("stop"), "{reason}");
let waiting = {
let (signals, view) = (Arc::clone(&signals), Arc::clone(&view));
tokio::spawn(async move { signals.admit(&view, false, 1).await })
};
for _ in 0..1000 {
tokio::task::yield_now().await;
}
assert!(
!waiting.is_finished(),
"a write went through at the stop trigger"
);
signals.installed();
for _ in 0..1000 {
tokio::task::yield_now().await;
}
assert!(
!waiting.is_finished(),
"an install that left level 0 full released a write"
);
view.store(Arc::new(view_with(2)));
weigh(&signals, &view.load());
tokio::time::timeout(std::time::Duration::from_secs(10), waiting)
.await
.expect("released")
.expect("joined")
.expect("admitted");
}
#[test]
fn a_failed_compaction_is_reported_as_one() {
let signals = signals();
signals.fail(&Error::CompactionFailed);
let refused = futures_lite_block(signals.admit(&ArcSwap::from_pointee(view_with(0)), true, 1));
assert!(
matches!(refused, Err(Error::CompactionFailed)),
"{refused:?}"
);
}
fn futures_lite_block<T>(future: impl std::future::Future<Output = T>) -> T {
super::read_tests::block_on(future)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn writes_stopped_by_level_zero_go_on_once_compactions_take_it_under() {
use super::Db;
use super::flush_tests::{PATH, key, value};
use crate::log::fault::FaultStorage;
use crate::{DbOptions, KeyspaceId, ReadOptions, WriteBatch, WriteOptions};
let keyspace = KeyspaceOptions::builder()
.memtable_bytes(64 * 1024)
.level0_compaction_trigger(2)
.level0_slowdown_trigger(2)
.level0_stop_trigger(2)
.level_base_bytes(64 * 1024)
.level_size_multiplier(2)
.target_file_bytes(16 * 1024)
.build()
.expect("valid");
let options = DbOptions::builder()
.default_keyspace(keyspace)
.wal_segment_bytes(1024 * 1024)
.build()
.expect("valid");
let storage = FaultStorage::default();
let db = Db::open_with_storage(PATH, options, Arc::new(storage))
.await
.expect("opened");
tokio::time::timeout(std::time::Duration::from_secs(30), async {
for index in 0..300 {
let mut batch = WriteBatch::new();
batch
.put(KeyspaceId::DEFAULT, &key(index), &value(index))
.expect("small entry");
db.write(&batch, &WriteOptions::default())
.await
.expect("written");
}
})
.await
.expect("writes held by level 0 were released by compactions");
for index in 0..300 {
let read = db
.get(KeyspaceId::DEFAULT, &key(index), &ReadOptions::default())
.await
.expect("readable");
assert_eq!(read, Some(value(index)), "key {index}");
}
db.close().await.expect("closed");
}
#[tokio::test(start_paused = true)]
async fn a_write_past_the_slowdown_trigger_waits_for_its_bytes_at_the_delayed_rate() {
let signals = signals();
let slowed = ArcSwap::from_pointee(view_with(3));
weigh(&signals, &slowed.load());
let start = tokio::time::Instant::now();
signals.admit(&slowed, false, 512).await.expect("admitted");
assert_eq!(
start.elapsed(),
Duration::ZERO,
"within a millisecond's worth"
);
let start = tokio::time::Instant::now();
signals.admit(&slowed, false, RATE).await.expect("admitted");
let paid = start.elapsed();
assert!(
paid >= Duration::from_millis(990) && paid <= Duration::from_millis(1010),
"a mebibyte at a mebibyte a second, less the burst: {paid:?}"
);
let start = tokio::time::Instant::now();
let below = ArcSwap::from_pointee(view_with(2));
weigh(&signals, &below.load());
signals.admit(&below, false, RATE).await.expect("admitted");
assert_eq!(
start.elapsed(),
Duration::ZERO,
"under the trigger nothing waits"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_delayed_write_rate_reaches_the_installer() {
let options = crate::DbOptions::builder()
.delayed_write_bytes_per_second(3 * RATE)
.build()
.expect("valid");
let db = super::Db::open_with_storage(
super::flush_tests::PATH,
options,
Arc::new(crate::log::fault::FaultStorage::default()),
)
.await
.expect("opened");
let installer = db.installer.as_ref().expect("on storage");
assert_eq!(installer.signals.delayed_rate(), Some(3 * RATE));
db.close().await.expect("closed");
}
#[tokio::test(start_paused = true)]
async fn writes_slow_past_the_soft_pending_limit_and_stop_past_the_hard_one() {
let signals = Arc::new(signals());
let view = Arc::new(ArcSwap::from_pointee(view_with(0)));
press(&signals, Pressure::None);
signals
.admit(&view, true, 1)
.await
.expect("under the soft limit");
press(&signals, Pressure::SlowOwed);
let reason = refusal(signals.admit(&view, true, 1).await);
assert!(reason.contains("soft"), "{reason}");
let start = tokio::time::Instant::now();
signals
.admit(&view, false, RATE)
.await
.expect("paced, then admitted");
assert!(
start.elapsed() >= Duration::from_millis(990),
"paced like a slowdown"
);
press(&signals, Pressure::StopOwed);
let reason = refusal(signals.admit(&view, true, 1).await);
assert!(reason.contains("hard"), "{reason}");
let held = {
let (signals, view) = (Arc::clone(&signals), Arc::clone(&view));
tokio::spawn(async move { signals.admit(&view, false, 1).await })
};
tokio::time::sleep(Duration::from_millis(1)).await;
assert!(!held.is_finished(), "a write waits past the hard limit");
press(&signals, Pressure::None);
while !held.is_finished() {
signals.installed();
tokio::task::yield_now().await;
}
held.await
.expect("joined")
.expect("admitted once compaction caught up");
}