Files
turso/simulator/runner/env.rs
Jussi Saurio 9aae1ff00a sim: add Fault::ReopenDatabase
Add fault type that reopens the DB and reconnects the connections.
It purposefully does not call conn.close(), as the default behavior
of that method is to checkpoint the database.

This easily exposes multiple manifestations of DB/WAL corruption caused by
this issue: https://github.com/tursodatabase/limbo/issues/1725

in fact, in my testing, every run fails when this fault is enabled. Hence I've
added the --disable-reopen-database flag if you want to try to find some other
bugs.
2025-06-24 14:41:50 -03:00

240 lines
7.6 KiB
Rust

use std::fmt::Display;
use std::mem;
use std::path::Path;
use std::sync::Arc;
use limbo_core::Database;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::model::table::Table;
use crate::runner::io::SimulatorIO;
use super::cli::SimulatorCLI;
pub(crate) struct SimulatorEnv {
pub(crate) opts: SimulatorOpts,
pub(crate) tables: Vec<Table>,
pub(crate) connections: Vec<SimConnection>,
pub(crate) io: Arc<SimulatorIO>,
pub(crate) db: Arc<Database>,
pub(crate) rng: ChaCha8Rng,
pub(crate) db_path: String,
}
impl SimulatorEnv {
pub(crate) fn clone_without_connections(&self) -> Self {
SimulatorEnv {
opts: self.opts.clone(),
tables: self.tables.clone(),
connections: (0..self.connections.len())
.map(|_| SimConnection::Disconnected)
.collect(),
io: self.io.clone(),
db: self.db.clone(),
rng: self.rng.clone(),
db_path: self.db_path.clone(),
}
}
}
impl SimulatorEnv {
pub(crate) fn new(seed: u64, cli_opts: &SimulatorCLI, db_path: &Path) -> Self {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let total = 100.0;
let mut create_percent = 0.0;
let mut create_index_percent = 0.0;
let mut drop_percent = 0.0;
let mut delete_percent = 0.0;
let mut update_percent = 0.0;
let read_percent = rng.gen_range(0.0..=total);
let write_percent = total - read_percent;
if !cli_opts.disable_create {
// Create percent should be 5-15% of the write percent
create_percent = rng.gen_range(0.05..=0.15) * write_percent;
}
if !cli_opts.disable_create_index {
// Create indexpercent should be 2-5% of the write percent
create_index_percent = rng.gen_range(0.02..=0.05) * write_percent;
}
if !cli_opts.disable_drop {
// Drop percent should be 2-5% of the write percent
drop_percent = rng.gen_range(0.02..=0.05) * write_percent;
}
if !cli_opts.disable_delete {
// Delete percent should be 10-20% of the write percent
delete_percent = rng.gen_range(0.1..=0.2) * write_percent;
}
if !cli_opts.disable_update {
// Update percent should be 10-20% of the write percent
// TODO: freestyling the percentage
update_percent = rng.gen_range(0.1..=0.2) * write_percent;
}
let write_percent = write_percent
- create_percent
- create_index_percent
- delete_percent
- drop_percent
- update_percent;
let summed_total: f64 = read_percent
+ write_percent
+ create_percent
+ create_index_percent
+ drop_percent
+ update_percent
+ delete_percent;
let abs_diff = (summed_total - total).abs();
if abs_diff > 0.0001 {
panic!(
"Summed total {} is not equal to total {}",
summed_total, total
);
}
let opts = SimulatorOpts {
ticks: rng.gen_range(cli_opts.minimum_tests..=cli_opts.maximum_tests),
max_connections: 1, // TODO: for now let's use one connection as we didn't implement
// correct transactions processing
max_tables: rng.gen_range(0..128),
create_percent,
create_index_percent,
read_percent,
write_percent,
delete_percent,
drop_percent,
update_percent,
disable_select_optimizer: cli_opts.disable_select_optimizer,
disable_insert_values_select: cli_opts.disable_insert_values_select,
disable_double_create_failure: cli_opts.disable_double_create_failure,
disable_select_limit: cli_opts.disable_select_limit,
disable_delete_select: cli_opts.disable_delete_select,
disable_drop_select: cli_opts.disable_drop_select,
page_size: 4096, // TODO: randomize this too
max_interactions: rng.gen_range(cli_opts.minimum_tests..=cli_opts.maximum_tests),
max_time_simulation: cli_opts.maximum_time,
disable_reopen_database: cli_opts.disable_reopen_database,
};
let io = Arc::new(SimulatorIO::new(seed, opts.page_size).unwrap());
// Remove existing database file if it exists
if db_path.exists() {
std::fs::remove_file(db_path).unwrap();
}
let wal_path = db_path.with_extension("db-wal");
if wal_path.exists() {
std::fs::remove_file(wal_path).unwrap();
}
let db = match Database::open_file(io.clone(), db_path.to_str().unwrap(), false) {
Ok(db) => db,
Err(e) => {
panic!("error opening simulator test file {:?}: {:?}", db_path, e);
}
};
let connections = (0..opts.max_connections)
.map(|_| SimConnection::Disconnected)
.collect::<Vec<_>>();
SimulatorEnv {
opts,
tables: Vec::new(),
connections,
rng,
io,
db,
db_path: db_path.to_str().unwrap().to_string(),
}
}
}
pub trait ConnectionTrait
where
Self: std::marker::Sized + Clone,
{
fn is_connected(&self) -> bool;
fn disconnect(&mut self);
}
pub(crate) enum SimConnection {
LimboConnection(Arc<limbo_core::Connection>),
SQLiteConnection(rusqlite::Connection),
Disconnected,
}
impl SimConnection {
pub(crate) fn is_connected(&self) -> bool {
match self {
SimConnection::LimboConnection(_) | SimConnection::SQLiteConnection(_) => true,
SimConnection::Disconnected => false,
}
}
pub(crate) fn disconnect(&mut self) {
let conn = mem::replace(self, SimConnection::Disconnected);
match conn {
SimConnection::LimboConnection(conn) => {
conn.close().unwrap();
}
SimConnection::SQLiteConnection(conn) => {
conn.close().unwrap();
}
SimConnection::Disconnected => {}
}
}
}
impl Display for SimConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SimConnection::LimboConnection(_) => {
write!(f, "LimboConnection")
}
SimConnection::SQLiteConnection(_) => {
write!(f, "SQLiteConnection")
}
SimConnection::Disconnected => {
write!(f, "Disconnected")
}
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct SimulatorOpts {
pub(crate) ticks: usize,
pub(crate) max_connections: usize,
pub(crate) max_tables: usize,
// this next options are the distribution of workload where read_percent + write_percent +
// delete_percent == 100%
pub(crate) create_percent: f64,
pub(crate) create_index_percent: f64,
pub(crate) read_percent: f64,
pub(crate) write_percent: f64,
pub(crate) delete_percent: f64,
pub(crate) update_percent: f64,
pub(crate) drop_percent: f64,
pub(crate) disable_select_optimizer: bool,
pub(crate) disable_insert_values_select: bool,
pub(crate) disable_double_create_failure: bool,
pub(crate) disable_select_limit: bool,
pub(crate) disable_delete_select: bool,
pub(crate) disable_drop_select: bool,
pub(crate) disable_reopen_database: bool,
pub(crate) max_interactions: usize,
pub(crate) page_size: usize,
pub(crate) max_time_simulation: usize,
}