Files
turso/simulator/runner/env.rs
2025-08-30 11:31:52 -03:00

485 lines
15 KiB
Rust

use std::fmt::Display;
use std::mem;
use std::ops::Deref;
use std::panic::UnwindSafe;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use garde::Validate;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use sql_generation::model::table::Table;
use turso_core::Database;
use crate::profiles::Profile;
use crate::runner::io::SimulatorIO;
use super::cli::SimulatorCLI;
#[derive(Debug, Copy, Clone)]
pub(crate) enum SimulationType {
Default,
Doublecheck,
Differential,
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum SimulationPhase {
Test,
Shrink,
}
#[derive(Debug, Clone)]
pub(crate) struct SimulatorTables {
pub(crate) tables: Vec<Table>,
pub(crate) snapshot: Option<Vec<Table>>,
}
impl SimulatorTables {
pub(crate) fn new() -> Self {
Self {
tables: Vec::new(),
snapshot: None,
}
}
pub(crate) fn clear(&mut self) {
self.tables.clear();
self.snapshot = None;
}
pub(crate) fn push(&mut self, table: Table) {
self.tables.push(table);
}
}
impl Deref for SimulatorTables {
type Target = Vec<Table>;
fn deref(&self) -> &Self::Target {
&self.tables
}
}
pub(crate) struct SimulatorEnv {
pub(crate) opts: SimulatorOpts,
pub profile: Profile,
pub(crate) connections: Vec<SimConnection>,
pub(crate) io: Arc<SimulatorIO>,
pub(crate) db: Option<Arc<Database>>,
pub(crate) rng: ChaCha8Rng,
pub(crate) paths: Paths,
pub(crate) type_: SimulationType,
pub(crate) phase: SimulationPhase,
pub(crate) tables: SimulatorTables,
}
impl UnwindSafe for SimulatorEnv {}
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(),
paths: self.paths.clone(),
type_: self.type_,
phase: self.phase,
profile: self.profile.clone(),
}
}
pub(crate) fn clear(&mut self) {
self.tables.clear();
self.connections.iter_mut().for_each(|c| c.disconnect());
self.rng = ChaCha8Rng::seed_from_u64(self.opts.seed);
let latency_prof = &self.profile.io.latency;
let io = Arc::new(
SimulatorIO::new(
self.opts.seed,
self.opts.page_size,
latency_prof.latency_probability,
latency_prof.min_tick,
latency_prof.max_tick,
)
.unwrap(),
);
// Remove existing database file
let db_path = self.get_db_path();
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();
}
self.db = None;
let db = match Database::open_file(
io.clone(),
db_path.to_str().unwrap(),
self.profile.experimental_mvcc,
self.profile.query.gen_opts.indexes,
) {
Ok(db) => db,
Err(e) => {
tracing::error!(%e);
panic!("error opening simulator test file {db_path:?}: {e:?}");
}
};
self.io = io;
self.db = Some(db);
}
pub(crate) fn get_db_path(&self) -> PathBuf {
self.paths.db(&self.type_, &self.phase)
}
pub(crate) fn get_plan_path(&self) -> PathBuf {
self.paths.plan(&self.type_, &self.phase)
}
pub(crate) fn clone_as(&self, simulation_type: SimulationType) -> Self {
let mut env = self.clone_without_connections();
env.type_ = simulation_type;
env.clear();
env
}
pub(crate) fn clone_at_phase(&self, phase: SimulationPhase) -> Self {
let mut env = self.clone_without_connections();
env.phase = phase;
env.clear();
env
}
}
impl SimulatorEnv {
pub(crate) fn new(
seed: u64,
cli_opts: &SimulatorCLI,
paths: Paths,
simulation_type: SimulationType,
profile: &Profile,
) -> 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.random_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.random_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.random_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.random_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.random_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.random_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 {summed_total} is not equal to total {total}");
}
let opts = SimulatorOpts {
seed,
ticks: rng.random_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.random_range(0..128),
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,
disable_where_true_false_null: cli_opts.disable_where_true_false_null,
disable_union_all_preserves_cardinality: cli_opts
.disable_union_all_preserves_cardinality,
disable_fsync_no_wait: cli_opts.disable_fsync_no_wait,
disable_faulty_query: cli_opts.disable_faulty_query,
page_size: 4096, // TODO: randomize this too
max_interactions: rng.random_range(cli_opts.minimum_tests..=cli_opts.maximum_tests)
as u32,
max_time_simulation: cli_opts.maximum_time,
disable_reopen_database: cli_opts.disable_reopen_database,
};
// Remove existing database file if it exists
let db_path = paths.db(&simulation_type, &SimulationPhase::Test);
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 mut profile = profile.clone();
// Conditionals here so that we can override some profile options from the CLI
if let Some(mvcc) = cli_opts.experimental_mvcc {
profile.experimental_mvcc = mvcc;
}
if let Some(indexes) = cli_opts.disable_experimental_indexes {
profile.query.gen_opts.indexes = indexes;
}
if let Some(latency_prob) = cli_opts.latency_probability {
profile.io.latency.latency_probability = latency_prob;
}
if let Some(max_tick) = cli_opts.max_tick {
profile.io.latency.max_tick = max_tick;
}
if let Some(min_tick) = cli_opts.min_tick {
profile.io.latency.min_tick = min_tick;
}
profile.validate().unwrap();
let latency_prof = &profile.io.latency;
let io = Arc::new(
SimulatorIO::new(
seed,
opts.page_size,
latency_prof.latency_probability,
latency_prof.min_tick,
latency_prof.max_tick,
)
.unwrap(),
);
let db = match Database::open_file(
io.clone(),
db_path.to_str().unwrap(),
profile.experimental_mvcc,
profile.query.gen_opts.indexes,
) {
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: SimulatorTables::new(),
connections,
paths,
rng,
io,
db: Some(db),
type_: simulation_type,
phase: SimulationPhase::Test,
profile: profile.clone(),
}
}
pub(crate) fn connect(&mut self, connection_index: usize) {
if connection_index >= self.connections.len() {
panic!("connection index out of bounds");
}
if self.connections[connection_index].is_connected() {
log::trace!(
"Connection {connection_index} is already connected, skipping reconnection"
);
return;
}
match self.type_ {
SimulationType::Default | SimulationType::Doublecheck => {
self.connections[connection_index] = SimConnection::LimboConnection(
self.db
.as_ref()
.expect("db to be Some")
.connect()
.expect("Failed to connect to Limbo database"),
);
}
SimulationType::Differential => {
self.connections[connection_index] = SimConnection::SQLiteConnection(
rusqlite::Connection::open(self.get_db_path())
.expect("Failed to open SQLite connection"),
);
}
};
}
}
pub trait ConnectionTrait
where
Self: std::marker::Sized + Clone,
{
fn is_connected(&self) -> bool;
fn disconnect(&mut self);
}
pub(crate) enum SimConnection {
LimboConnection(Arc<turso_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) seed: u64,
pub(crate) ticks: usize,
pub(crate) max_connections: usize,
pub(crate) max_tables: usize,
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_where_true_false_null: bool,
pub(crate) disable_union_all_preserves_cardinality: bool,
pub(crate) disable_fsync_no_wait: bool,
pub(crate) disable_faulty_query: bool,
pub(crate) disable_reopen_database: bool,
pub(crate) max_interactions: u32,
pub(crate) page_size: usize,
pub(crate) max_time_simulation: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct Paths {
pub(crate) base: PathBuf,
pub(crate) history: PathBuf,
}
impl Paths {
pub(crate) fn new(output_dir: &Path) -> Self {
Paths {
base: output_dir.to_path_buf(),
history: PathBuf::from(output_dir).join("history.txt"),
}
}
fn path_(&self, type_: &SimulationType, phase: &SimulationPhase) -> PathBuf {
match (type_, phase) {
(SimulationType::Default, SimulationPhase::Test) => self.base.join(Path::new("test")),
(SimulationType::Default, SimulationPhase::Shrink) => {
self.base.join(Path::new("shrink"))
}
(SimulationType::Differential, SimulationPhase::Test) => {
self.base.join(Path::new("diff"))
}
(SimulationType::Differential, SimulationPhase::Shrink) => {
self.base.join(Path::new("diff_shrink"))
}
(SimulationType::Doublecheck, SimulationPhase::Test) => {
self.base.join(Path::new("doublecheck"))
}
(SimulationType::Doublecheck, SimulationPhase::Shrink) => {
self.base.join(Path::new("doublecheck_shrink"))
}
}
}
pub(crate) fn db(&self, type_: &SimulationType, phase: &SimulationPhase) -> PathBuf {
self.path_(type_, phase).with_extension("db")
}
pub(crate) fn plan(&self, type_: &SimulationType, phase: &SimulationPhase) -> PathBuf {
self.path_(type_, phase).with_extension("sql")
}
pub fn delete_all_files(&self) {
if self.base.exists() {
let res = std::fs::remove_dir_all(&self.base);
if res.is_err() {
tracing::error!(error = %res.unwrap_err(),"failed to remove directory");
}
}
}
}