use rand::rngs::StdRng;
use rand::{RngCore, SeedableRng};
use temp_dir::TempDir;
use crate::CommunityComposer;
use crate::cnf::ConfigMap;
use crate::kvs::Datastore;
use crate::kvs::LockType::Optimistic;
use crate::kvs::TransactionType::*;
fn prefix_byte_range(prefix: &str) -> std::ops::Range<Vec<u8>> {
let start = prefix.as_bytes().to_vec();
let end = start.iter().copied().chain(std::iter::once(0xff)).collect::<Vec<u8>>();
start..end
}
#[tokio::test]
pub async fn read_and_deletion_only() {
let config = ConfigMap::empty()
.with_key_value("rocksdb_sst_max_allowed_space_usage", "10485760")
.with_key_value("rocksdb_write_buffer_size", "10240")
.with_key_value("rocksdb_wal_size_limit", "1");
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let path = format!("rocksdb:{path}");
let ds = Datastore::builder()
.with_config(config)
.build_with_factory_path(&path, CommunityComposer())
.await
.unwrap();
{
let tx = ds.transaction(Write, Optimistic).await.unwrap();
tx.set(&"initial_key", &"initial_value".as_bytes().to_vec()).await.unwrap();
tx.commit().await.unwrap();
}
let ongoing_tx = ds.transaction(Write, Optimistic).await.unwrap();
ongoing_tx.set(&"ongoing_key", &"ongoing_value".as_bytes().to_vec()).await.unwrap();
let mut rng = StdRng::seed_from_u64(0xA5A5_A5A5_A5A5_A5A5);
let mut count_err = 0;
for j in 0..200 {
let tx = ds.transaction(Write, Optimistic).await.unwrap();
for i in 0..100 {
let key = format!("unlimited_key_{}_{}", i, j);
let mut value = vec![0u8; 1024]; rng.fill_bytes(&mut value);
if let Err(e) = tx.set(&key, &value).await {
assert!(
e.to_string().contains("read-and-deletion-only mode"),
"Unexpected error: {e}"
);
count_err += 1;
}
}
if let Err(e) = tx.commit().await {
assert!(e.to_string().contains("read-and-deletion-only mode"), "Unexpected error: {e}");
count_err += 1;
}
}
assert!(count_err > 50, "Count error: {}", count_err);
{
let tx = ds.transaction(Write, Optimistic).await.unwrap();
let res = tx.put(&"other_key", &"other_value".as_bytes().to_vec()).await;
assert!(
res.unwrap_err().to_string().contains("read-and-deletion-only mode"),
"Expected read-and-deletion-only error"
);
tx.cancel().await.unwrap();
}
{
let res = ongoing_tx.commit().await;
assert!(
res.unwrap_err().to_string().contains("read-and-deletion-only mode"),
"Expected read-and-deletion-only error"
);
}
{
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let val = tx.get(&"initial_key", None).await.unwrap();
assert!(matches!(val.as_deref(), Some(b"initial_value")));
tx.cancel().await.unwrap();
}
for j in 0..200 {
let tx = ds.transaction(Write, Optimistic).await.unwrap();
for i in 0..100 {
let key = format!("unlimited_key_{}_{}", i, j);
tx.del(&key).await.unwrap();
}
tx.commit().await.unwrap();
}
{
let tx = ds.transaction(Write, Optimistic).await.unwrap();
tx.put(&"other_key", &"other_value".as_bytes().to_vec()).await.unwrap();
tx.commit().await.unwrap();
}
}
#[tokio::test(flavor = "multi_thread")]
async fn memtable_merge_count_clamp_non_versioned() {
memtable_merge_count_clamp_inner(false).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn memtable_merge_count_clamp_versioned() {
memtable_merge_count_clamp_inner(true).await;
}
async fn memtable_merge_count_clamp_inner(versioned: bool) {
let mut config = ConfigMap::empty()
.with_key_value("rocksdb_max_write_buffer_number", "1")
.with_key_value("rocksdb_min_write_buffer_number_to_merge", "2");
if versioned {
config = config.with_key_value("datastore_versioned", "true");
}
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let path = format!("rocksdb:{path}");
let ds = Datastore::builder()
.with_config(config)
.build_with_factory_path(&path, CommunityComposer())
.await
.unwrap();
let tx = ds.transaction(Write, Optimistic).await.unwrap();
tx.set(&"clamp_key", &"clamp_value".as_bytes().to_vec()).await.unwrap();
tokio::time::timeout(std::time::Duration::from_secs(10), tx.commit())
.await
.expect("commit stalled: min_write_buffer_number_to_merge clamp regressed")
.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn compact_pushes_data_to_bottommost() {
use rand::rngs::StdRng;
use rand::{RngCore, SeedableRng};
use crate::kvs::api::Transactable;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let config = RocksDbConfig {
target_file_size_base: 64 * 1024,
write_buffer_size: 64 * 1024,
..RocksDbConfig::default()
};
let ds = RocksDbDatastore::new(&path, config).await.unwrap();
let mut rng = StdRng::seed_from_u64(0xC0FF_EEC0_FFEE_u64);
for batch in 0..32u32 {
let tx = ds.transaction(true, true).await.unwrap();
for i in 0..32u32 {
let key: Vec<u8> = format!("bottommost_test_{batch:04}_{i:04}").into_bytes();
let mut value = vec![0u8; 256];
rng.fill_bytes(&mut value);
tx.set(key, value).await.unwrap();
}
tx.commit().await.unwrap();
}
{
let tx = ds.transaction(false, true).await.unwrap();
Transactable::compact(tx.as_ref(), None).await.unwrap();
tx.cancel().await.unwrap();
}
let level_file_count = |level: usize| -> u64 {
let name = format!("rocksdb.num-files-at-level{level}");
ds.db.property_int_value(&name).unwrap_or_default().unwrap_or_default()
};
for level in 0..6 {
let n = level_file_count(level);
assert_eq!(n, 0, "expected level {level} to be empty after compact, got {n} files");
}
let bottom = level_file_count(6);
assert!(bottom > 0, "expected bottommost level (L6) to carry SSTs, got 0");
}
#[tokio::test(flavor = "multi_thread")]
async fn periodic_compaction_seconds_wired() {
for versioned in [false, true] {
let mut config =
ConfigMap::empty().with_key_value("rocksdb_periodic_compaction_seconds", "60");
if versioned {
config = config.with_key_value("datastore_versioned", "true");
}
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let path = format!("rocksdb:{path}");
let _ds = Datastore::builder()
.with_config(config)
.build_with_factory_path(&path, CommunityComposer())
.await
.expect("periodic_compaction_seconds should not break the open path");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn universal_compaction_options_wired() {
let config = ConfigMap::empty()
.with_key_value("rocksdb_compaction_style", "universal")
.with_key_value("rocksdb_universal_size_ratio", "5")
.with_key_value("rocksdb_universal_min_merge_width", "3")
.with_key_value("rocksdb_universal_max_merge_width", "16")
.with_key_value("rocksdb_universal_max_size_amplification_percent", "150")
.with_key_value("rocksdb_universal_compression_size_percent", "75")
.with_key_value("rocksdb_universal_stop_style", "similar_size");
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let path = format!("rocksdb:{path}");
let ds = Datastore::builder()
.with_config(config)
.build_with_factory_path(&path, CommunityComposer())
.await
.expect("universal compaction options should not break the open path");
let tx = ds.transaction(Write, Optimistic).await.unwrap();
tx.set(&"universal_key", &"universal_value".as_bytes().to_vec()).await.unwrap();
tx.commit().await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn shutdown_drains_cleanly_with_defaults() {
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let config = RocksDbConfig {
target_file_size_base: 64 * 1024,
write_buffer_size: 64 * 1024,
..RocksDbConfig::default()
};
let ds = RocksDbDatastore::new(&path, config).await.unwrap();
{
let tx = ds.transaction(true, true).await.unwrap();
for i in 0..256u32 {
let key: Vec<u8> = format!("shutdown_default_{i:04}").into_bytes();
let value = vec![0u8; 256];
tx.set(key, value).await.unwrap();
}
tx.commit().await.unwrap();
}
ds.shutdown().await.expect("default shutdown should succeed");
let running = ds
.db
.property_int_value("rocksdb.num-running-compactions")
.unwrap_or_default()
.unwrap_or_default();
assert_eq!(
running, 0,
"expected no background compactions running after shutdown, got {running}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn shutdown_compacts_to_bottommost_when_opted_in() {
use rand::rngs::StdRng;
use rand::{RngCore, SeedableRng};
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let config = RocksDbConfig {
target_file_size_base: 64 * 1024,
write_buffer_size: 64 * 1024,
compact_on_shutdown: true,
shutdown_wait_for_compact_seconds: 30,
..RocksDbConfig::default()
};
let ds = RocksDbDatastore::new(&path, config).await.unwrap();
let mut rng = StdRng::seed_from_u64(0xD15C_0DED_BEEF_FACE_u64);
for batch in 0..32u32 {
let tx = ds.transaction(true, true).await.unwrap();
for i in 0..32u32 {
let key: Vec<u8> = format!("shutdown_compact_{batch:04}_{i:04}").into_bytes();
let mut value = vec![0u8; 256];
rng.fill_bytes(&mut value);
tx.set(key, value).await.unwrap();
}
tx.commit().await.unwrap();
}
ds.shutdown().await.expect("compact-on-shutdown should succeed");
let level_file_count = |level: usize| -> u64 {
let name = format!("rocksdb.num-files-at-level{level}");
ds.db.property_int_value(&name).unwrap_or_default().unwrap_or_default()
};
for level in 0..6 {
let n = level_file_count(level);
assert_eq!(n, 0, "expected level {level} empty after compact_on_shutdown, got {n} files",);
}
let bottom = level_file_count(6);
assert!(bottom > 0, "expected bottommost (L6) to carry SSTs, got 0");
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_cursors_do_not_evict() {
use crate::kvs::api::Transactable;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
use crate::kvs::{Direction, NORMAL_BATCH_SIZE};
const PREFIX_COUNT: usize = 8;
const KEYS_PER_PREFIX: usize = 1500;
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let ds = RocksDbDatastore::new(&path, RocksDbConfig::default()).await.unwrap();
{
let tx = ds.transaction(true, true).await.unwrap();
for p in 0..PREFIX_COUNT {
for k in 0..KEYS_PER_PREFIX {
let key = format!("prefix_{p:02}/key_{k:04}").into_bytes();
let value = format!("v_{p}_{k}").into_bytes();
tx.set(key, value).await.unwrap();
}
}
tx.commit().await.unwrap();
}
let tx = ds.transaction(false, true).await.unwrap();
let tx_ref = tx.as_ref();
let mut cursors = Vec::with_capacity(PREFIX_COUNT);
let mut collected: Vec<Vec<Vec<u8>>> = vec![Vec::new(); PREFIX_COUNT];
for p in 0..PREFIX_COUNT {
let rng = prefix_byte_range(&format!("prefix_{p:02}/"));
let cursor =
Transactable::open_keys_cursor(tx_ref, rng, Direction::Forward, 0, None).await.unwrap();
cursors.push(cursor);
}
let batch_limit = NORMAL_BATCH_SIZE.min(200);
let mut active = [true; PREFIX_COUNT];
while active.iter().any(|a| *a) {
for (p, cursor) in cursors.iter_mut().enumerate() {
if !active[p] {
continue;
}
let batch = cursor.next_batch(batch_limit).await.unwrap();
if batch.is_empty() {
active[p] = false;
} else {
let batch_len = batch.len();
collected[p].extend(batch.iter().map(|k| k.to_vec()));
if batch_len < batch_limit as usize {
active[p] = false;
}
}
}
}
for (p, keys) in collected.iter().enumerate() {
assert_eq!(
keys.len(),
KEYS_PER_PREFIX,
"prefix {p}: expected {KEYS_PER_PREFIX} keys, got {}",
keys.len()
);
for window in keys.windows(2) {
assert!(window[0] < window[1], "prefix {p}: keys not strictly ascending");
}
let expected_prefix = format!("prefix_{p:02}/").into_bytes();
for k in keys {
assert!(k.starts_with(&expected_prefix), "prefix {p}: key outside its range");
}
}
drop(cursors);
tx.cancel().await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_cursors_on_writable_tx() {
use crate::kvs::api::Transactable;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
use crate::kvs::{Direction, NORMAL_BATCH_SIZE};
const PREFIX_COUNT: usize = 6;
const KEYS_PER_PREFIX: usize = 300;
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let ds = RocksDbDatastore::new(&path, RocksDbConfig::default()).await.unwrap();
{
let tx = ds.transaction(true, true).await.unwrap();
for p in 0..PREFIX_COUNT {
for k in 0..KEYS_PER_PREFIX {
let key = format!("wp_{p:02}/key_{k:04}").into_bytes();
let value = vec![p as u8; 8];
tx.set(key, value).await.unwrap();
}
}
tx.commit().await.unwrap();
}
let tx = ds.transaction(true, true).await.unwrap();
let tx_ref = tx.as_ref();
let mut cursors = Vec::with_capacity(PREFIX_COUNT);
for p in 0..PREFIX_COUNT {
let rng = prefix_byte_range(&format!("wp_{p:02}/"));
let cursor =
Transactable::open_keys_cursor(tx_ref, rng, Direction::Forward, 0, None).await.unwrap();
cursors.push(cursor);
}
let batch_limit = NORMAL_BATCH_SIZE.min(64);
let mut totals = [0usize; PREFIX_COUNT];
let mut active = [true; PREFIX_COUNT];
while active.iter().any(|a| *a) {
for (p, cursor) in cursors.iter_mut().enumerate() {
if !active[p] {
continue;
}
let batch = cursor.next_batch(batch_limit).await.unwrap();
let len = batch.len();
totals[p] += len;
if len == 0 || len < batch_limit as usize {
active[p] = false;
}
}
}
for (p, n) in totals.iter().enumerate() {
assert_eq!(*n, KEYS_PER_PREFIX, "prefix {p}: expected {KEYS_PER_PREFIX}, got {n}");
}
drop(cursors);
tx.cancel().await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn cursor_drop_releases_slot() {
use crate::kvs::Direction;
use crate::kvs::api::Transactable;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let ds = RocksDbDatastore::new(&path, RocksDbConfig::default()).await.unwrap();
{
let tx = ds.transaction(true, true).await.unwrap();
for k in 0..10 {
let key = format!("drop_test/{k:02}").into_bytes();
tx.set(key, vec![0u8]).await.unwrap();
}
tx.commit().await.unwrap();
}
let tx = ds.transaction(false, true).await.unwrap();
let tx_ref = tx.as_ref();
for _ in 0..16 {
let rng = prefix_byte_range("drop_test/");
let cursor =
Transactable::open_keys_cursor(tx_ref, rng, Direction::Forward, 0, None).await.unwrap();
drop(cursor);
}
let rng = prefix_byte_range("drop_test/");
let mut cursor =
Transactable::open_keys_cursor(tx_ref, rng, Direction::Forward, 0, None).await.unwrap();
let batch = cursor.next_batch(100).await.unwrap();
assert_eq!(batch.len(), 10, "fresh cursor should observe all seeded keys");
drop(cursor);
tx.cancel().await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn next_batch_borrowed_slices_match_owned_scan() {
use crate::kvs::Direction;
use crate::kvs::api::Transactable;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
const N: usize = 1500;
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let ds = RocksDbDatastore::new(&path, RocksDbConfig::default()).await.unwrap();
{
let tx = ds.transaction(true, true).await.unwrap();
for k in 0..N {
let key = format!("fe_key/{k:06}").into_bytes();
tx.set(key, vec![0u8; 4]).await.unwrap();
}
tx.commit().await.unwrap();
}
let tx = ds.transaction(false, true).await.unwrap();
let tx_ref = tx.as_ref();
let rng = prefix_byte_range("fe_key/");
let mut cursor =
Transactable::open_keys_cursor(tx_ref, rng, Direction::Forward, 0, None).await.unwrap();
let mut collected: Vec<Vec<u8>> = Vec::with_capacity(N);
loop {
let batch = cursor.next_batch(200).await.unwrap();
let batch_len = batch.len();
collected.extend(batch.iter().map(|k| k.to_vec()));
if batch_len < 200 {
break;
}
}
drop(cursor);
tx.cancel().await.unwrap();
let expected: Vec<Vec<u8>> = (0..N).map(|k| format!("fe_key/{k:06}").into_bytes()).collect();
assert_eq!(collected.len(), N, "cursor should observe all seeded keys");
assert_eq!(collected, expected, "borrowed slices must reproduce the inserted bytes");
}
#[tokio::test(flavor = "multi_thread")]
async fn commit_blocks_until_live_cursor_drops() {
use std::time::Duration;
use futures::FutureExt;
use crate::kvs::Direction;
use crate::kvs::api::Transactable;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let ds = RocksDbDatastore::new(&path, RocksDbConfig::default()).await.unwrap();
{
let tx = ds.transaction(true, true).await.unwrap();
for k in 0..16 {
let key = format!("race_key/{k:02}").into_bytes();
tx.set(key, vec![0u8]).await.unwrap();
}
tx.commit().await.unwrap();
}
let tx = ds.transaction(true, true).await.unwrap();
let tx_ref = tx.as_ref();
let rng = prefix_byte_range("race_key/");
let mut cursor =
Transactable::open_keys_cursor(tx_ref, rng, Direction::Forward, 0, None).await.unwrap();
let _ = cursor.next_batch(4).await.unwrap();
let mut commit_fut = tx_ref.commit();
for round in 0..32 {
let snapshot = commit_fut.as_mut().now_or_never();
assert!(
snapshot.is_none(),
"commit_fut completed at round {round} while a cursor was still alive — \
drain_cursors did not actually drain (SeqCst protocol broken)"
);
tokio::task::yield_now().await;
}
drop(cursor);
tokio::time::timeout(Duration::from_secs(5), commit_fut)
.await
.expect("commit deadlocked after cursor was dropped — drain_cursors did not wake up")
.expect("commit failed after cursor drop");
}
#[tokio::test(flavor = "multi_thread")]
async fn cancel_blocks_until_live_cursor_drops() {
use std::time::Duration;
use futures::FutureExt;
use crate::kvs::Direction;
use crate::kvs::api::Transactable;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let ds = RocksDbDatastore::new(&path, RocksDbConfig::default()).await.unwrap();
{
let tx = ds.transaction(true, true).await.unwrap();
for k in 0..16 {
let key = format!("cancel_race/{k:02}").into_bytes();
tx.set(key, vec![0u8]).await.unwrap();
}
tx.commit().await.unwrap();
}
let tx = ds.transaction(false, true).await.unwrap();
let tx_ref = tx.as_ref();
let rng = prefix_byte_range("cancel_race/");
let mut cursor =
Transactable::open_keys_cursor(tx_ref, rng, Direction::Forward, 0, None).await.unwrap();
let _ = cursor.next_batch(4).await.unwrap();
let mut cancel_fut = tx_ref.cancel();
for round in 0..32 {
let snapshot = cancel_fut.as_mut().now_or_never();
assert!(
snapshot.is_none(),
"cancel_fut completed at round {round} while a cursor was still alive — \
drain_cursors did not actually drain on the cancel path"
);
tokio::task::yield_now().await;
}
drop(cursor);
tokio::time::timeout(Duration::from_secs(5), cancel_fut)
.await
.expect("cancel deadlocked after cursor was dropped")
.expect("cancel failed after cursor drop");
}
#[tokio::test(flavor = "multi_thread")]
async fn open_cursor_after_commit_starts_fails() {
use futures::FutureExt;
use crate::kvs::Direction;
use crate::kvs::api::Transactable;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let ds = RocksDbDatastore::new(&path, RocksDbConfig::default()).await.unwrap();
{
let tx = ds.transaction(true, true).await.unwrap();
tx.set(b"after_commit/key".to_vec(), vec![0u8]).await.unwrap();
tx.commit().await.unwrap();
}
let tx = ds.transaction(true, true).await.unwrap();
let tx_ref = tx.as_ref();
let blocker_rng = prefix_byte_range("after_commit/");
let blocker = Transactable::open_keys_cursor(tx_ref, blocker_rng, Direction::Forward, 0, None)
.await
.unwrap();
let mut commit_fut = tx_ref.commit();
let first_poll = commit_fut.as_mut().now_or_never();
assert!(
first_poll.is_none(),
"commit completed despite the blocker cursor still being alive; drain_cursors must \
park while cursors_alive > 0"
);
let rng = prefix_byte_range("after_commit/");
let result = Transactable::open_keys_cursor(tx_ref, rng, Direction::Forward, 0, None).await;
assert!(
result.is_err(),
"open_keys_cursor must fail after commit set done=true; got Ok — the open \
protocol's done re-check raced past the swap"
);
drop(blocker);
commit_fut.await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn open_cursor_cancellation_releases_cursors_alive_slot() {
use std::time::Duration;
use futures::FutureExt;
use crate::kvs::Direction;
use crate::kvs::api::Transactable;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
crate::kvs::threadpool::initialise();
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let ds = RocksDbDatastore::new(&path, RocksDbConfig::default()).await.unwrap();
{
let tx = ds.transaction(true, true).await.unwrap();
for k in 0..50_000u32 {
let key = format!("cancel_open/{k:08}").into_bytes();
tx.set(key, vec![0u8]).await.unwrap();
}
tx.commit().await.unwrap();
}
let tx = ds.transaction(true, true).await.unwrap();
let tx_ref = tx.as_ref();
let rng = prefix_byte_range("cancel_open/");
let mut count_fut = tx_ref.count(rng.clone(), None);
let count_first_poll = count_fut.as_mut().now_or_never();
assert!(
count_first_poll.is_none(),
"count() must be pending — count_blocking should still be running on the affinitypool \
worker and holding the inner lock guard"
);
let mut open_fut =
Transactable::open_keys_cursor(tx_ref, rng.clone(), Direction::Forward, 0, None);
let open_first_poll = open_fut.as_mut().now_or_never();
assert!(
open_first_poll.is_none(),
"open_keys_cursor must park on inner.lock() while count() holds it; if this fires Ok \
the test set-up didn't produce contention and the regression isn't being exercised"
);
drop(open_fut);
count_fut.await.unwrap();
tokio::time::timeout(Duration::from_secs(5), tx_ref.commit())
.await
.expect(
"commit deadlocked — cancelled open_keys_cursor leaked cursors_alive (the \
fetch_add must be wrapped in AliveGuard so the future-drop decrements it)",
)
.expect("commit failed");
}
#[tokio::test(flavor = "multi_thread")]
async fn commit_after_coordinator_shutdown_is_refused_before_apply() {
use crate::kvs::err::Error;
use crate::kvs::rocksdb::{Datastore as RocksDbDatastore, RocksDbConfig};
let path = TempDir::new().unwrap().path().to_string_lossy().to_string();
let ds = RocksDbDatastore::new(&path, RocksDbConfig::default()).await.unwrap();
let tx = ds.transaction(true, false).await.unwrap();
tx.set(b"refused/key".as_slice().into(), vec![1u8]).await.unwrap();
ds.commit_coordinator
.as_ref()
.expect("the default sync mode must run the commit coordinator")
.shutdown()
.unwrap();
let err = tx.commit().await.expect_err("a commit after coordinator shutdown must be refused");
assert!(matches!(err, Error::Shutdown), "expected Shutdown, got {err:?}");
let tx = ds.transaction(false, false).await.unwrap();
let val = tx.get(b"refused/key".as_slice().into(), None).await.unwrap();
assert_eq!(val, None, "a refused commit must not have applied anything");
tx.cancel().await.unwrap();
}