mirror of
https://github.com/neondatabase/neon.git
synced 2025-12-22 21:59:59 +00:00
438 lines
13 KiB
Rust
438 lines
13 KiB
Rust
use std::cell::Cell;
|
|
use std::str::FromStr;
|
|
use std::sync::Arc;
|
|
|
|
use desim::executor::{self, ExternalHandle};
|
|
use desim::node_os::NodeOs;
|
|
use desim::options::{Delay, NetworkOptions};
|
|
use desim::proto::{AnyMessage, NodeEvent};
|
|
use desim::world::{Node, World};
|
|
use rand::{Rng, SeedableRng};
|
|
use tracing::{debug, info_span, warn};
|
|
use utils::id::TenantTimelineId;
|
|
use utils::lsn::Lsn;
|
|
use walproposer::walproposer::{Config, Wrapper};
|
|
|
|
use super::log::SimClock;
|
|
use super::safekeeper_disk::SafekeeperDisk;
|
|
use super::walproposer_api;
|
|
use super::walproposer_disk::DiskWalProposer;
|
|
use crate::walproposer_sim::safekeeper::run_server;
|
|
use crate::walproposer_sim::walproposer_api::SimulationApi;
|
|
|
|
/// Simulated safekeeper node.
|
|
pub struct SafekeeperNode {
|
|
pub node: Arc<Node>,
|
|
pub id: u32,
|
|
pub disk: Arc<SafekeeperDisk>,
|
|
pub thread: Cell<ExternalHandle>,
|
|
}
|
|
|
|
impl SafekeeperNode {
|
|
/// Create and start a safekeeper at the specified Node.
|
|
pub fn new(node: Arc<Node>) -> Self {
|
|
let disk = Arc::new(SafekeeperDisk::new());
|
|
let thread = Cell::new(SafekeeperNode::launch(disk.clone(), node.clone()));
|
|
|
|
Self {
|
|
id: node.id,
|
|
node,
|
|
disk,
|
|
thread,
|
|
}
|
|
}
|
|
|
|
fn launch(disk: Arc<SafekeeperDisk>, node: Arc<Node>) -> ExternalHandle {
|
|
// start the server thread
|
|
node.launch(move |os| {
|
|
run_server(os, disk).expect("server should finish without errors");
|
|
})
|
|
}
|
|
|
|
/// Restart the safekeeper.
|
|
pub fn restart(&self) {
|
|
let new_thread = SafekeeperNode::launch(self.disk.clone(), self.node.clone());
|
|
let old_thread = self.thread.replace(new_thread);
|
|
old_thread.crash_stop();
|
|
}
|
|
}
|
|
|
|
/// Simulated walproposer node.
|
|
pub struct WalProposer {
|
|
thread: ExternalHandle,
|
|
node: Arc<Node>,
|
|
disk: Arc<DiskWalProposer>,
|
|
sync_safekeepers: bool,
|
|
}
|
|
|
|
impl WalProposer {
|
|
/// Generic start function for both modes.
|
|
fn start(
|
|
os: NodeOs,
|
|
disk: Arc<DiskWalProposer>,
|
|
ttid: TenantTimelineId,
|
|
addrs: Vec<String>,
|
|
lsn: Option<Lsn>,
|
|
) {
|
|
let sync_safekeepers = lsn.is_none();
|
|
|
|
let _enter = if sync_safekeepers {
|
|
info_span!("sync", started = executor::now()).entered()
|
|
} else {
|
|
info_span!("walproposer", started = executor::now()).entered()
|
|
};
|
|
|
|
os.log_event(format!("started;walproposer;{}", sync_safekeepers as i32));
|
|
|
|
let config = Config {
|
|
ttid,
|
|
safekeepers_list: addrs,
|
|
safekeeper_conninfo_options: String::new(),
|
|
safekeeper_reconnect_timeout: 1000,
|
|
safekeeper_connection_timeout: 5000,
|
|
sync_safekeepers,
|
|
};
|
|
let args = walproposer_api::Args {
|
|
os,
|
|
config: config.clone(),
|
|
disk,
|
|
redo_start_lsn: lsn,
|
|
};
|
|
let api = SimulationApi::new(args);
|
|
let wp = Wrapper::new(Box::new(api), config);
|
|
wp.start();
|
|
}
|
|
|
|
/// Start walproposer in a sync_safekeepers mode.
|
|
pub fn launch_sync(ttid: TenantTimelineId, addrs: Vec<String>, node: Arc<Node>) -> Self {
|
|
debug!("sync_safekeepers started at node {}", node.id);
|
|
let disk = DiskWalProposer::new();
|
|
let disk_wp = disk.clone();
|
|
|
|
// start the client thread
|
|
let handle = node.launch(move |os| {
|
|
WalProposer::start(os, disk_wp, ttid, addrs, None);
|
|
});
|
|
|
|
Self {
|
|
thread: handle,
|
|
node,
|
|
disk,
|
|
sync_safekeepers: true,
|
|
}
|
|
}
|
|
|
|
/// Start walproposer in a normal mode.
|
|
pub fn launch_walproposer(
|
|
ttid: TenantTimelineId,
|
|
addrs: Vec<String>,
|
|
node: Arc<Node>,
|
|
lsn: Lsn,
|
|
) -> Self {
|
|
debug!("walproposer started at node {}", node.id);
|
|
let disk = DiskWalProposer::new();
|
|
disk.lock().reset_to(lsn);
|
|
let disk_wp = disk.clone();
|
|
|
|
// start the client thread
|
|
let handle = node.launch(move |os| {
|
|
WalProposer::start(os, disk_wp, ttid, addrs, Some(lsn));
|
|
});
|
|
|
|
Self {
|
|
thread: handle,
|
|
node,
|
|
disk,
|
|
sync_safekeepers: false,
|
|
}
|
|
}
|
|
|
|
pub fn write_tx(&mut self, cnt: usize) {
|
|
let start_lsn = self.disk.lock().flush_rec_ptr();
|
|
|
|
for _ in 0..cnt {
|
|
self.disk
|
|
.lock()
|
|
.insert_logical_message(c"prefix", b"message");
|
|
}
|
|
|
|
let end_lsn = self.disk.lock().flush_rec_ptr();
|
|
|
|
// log event
|
|
self.node
|
|
.log_event(format!("write_wal;{};{};{}", start_lsn.0, end_lsn.0, cnt));
|
|
|
|
// now we need to set "Latch" in walproposer
|
|
self.node
|
|
.node_events()
|
|
.send(NodeEvent::Internal(AnyMessage::Just32(0)));
|
|
}
|
|
|
|
pub fn stop(&self) {
|
|
self.thread.crash_stop();
|
|
}
|
|
}
|
|
|
|
/// Holds basic simulation settings, such as network options.
|
|
pub struct TestConfig {
|
|
pub network: NetworkOptions,
|
|
pub timeout: u64,
|
|
pub clock: Option<SimClock>,
|
|
}
|
|
|
|
impl TestConfig {
|
|
/// Create a new TestConfig with default settings.
|
|
pub fn new(clock: Option<SimClock>) -> Self {
|
|
Self {
|
|
network: NetworkOptions {
|
|
keepalive_timeout: Some(2000),
|
|
connect_delay: Delay {
|
|
min: 1,
|
|
max: 5,
|
|
fail_prob: 0.0,
|
|
},
|
|
send_delay: Delay {
|
|
min: 1,
|
|
max: 5,
|
|
fail_prob: 0.0,
|
|
},
|
|
},
|
|
timeout: 1_000 * 10,
|
|
clock,
|
|
}
|
|
}
|
|
|
|
/// Start a new simulation with the specified seed.
|
|
pub fn start(&self, seed: u64) -> Test {
|
|
let world = Arc::new(World::new(seed, Arc::new(self.network.clone())));
|
|
|
|
if let Some(clock) = &self.clock {
|
|
clock.set_clock(world.clock());
|
|
}
|
|
|
|
let servers = [
|
|
SafekeeperNode::new(world.new_node()),
|
|
SafekeeperNode::new(world.new_node()),
|
|
SafekeeperNode::new(world.new_node()),
|
|
];
|
|
|
|
let server_ids = [servers[0].id, servers[1].id, servers[2].id];
|
|
let safekeepers_addrs = server_ids.map(|id| format!("node:{id}")).to_vec();
|
|
|
|
let ttid = TenantTimelineId::generate();
|
|
|
|
Test {
|
|
world,
|
|
servers,
|
|
sk_list: safekeepers_addrs,
|
|
ttid,
|
|
timeout: self.timeout,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Holds simulation state.
|
|
pub struct Test {
|
|
pub world: Arc<World>,
|
|
pub servers: [SafekeeperNode; 3],
|
|
pub sk_list: Vec<String>,
|
|
pub ttid: TenantTimelineId,
|
|
pub timeout: u64,
|
|
}
|
|
|
|
impl Test {
|
|
/// Start a sync_safekeepers thread and wait for it to finish.
|
|
pub fn sync_safekeepers(&self) -> anyhow::Result<Lsn> {
|
|
let wp = self.launch_sync_safekeepers();
|
|
|
|
// poll until exit or timeout
|
|
let time_limit = self.timeout;
|
|
while self.world.step() && self.world.now() < time_limit && !wp.thread.is_finished() {}
|
|
|
|
if !wp.thread.is_finished() {
|
|
anyhow::bail!("timeout or idle stuck");
|
|
}
|
|
|
|
let res = wp.thread.result();
|
|
if res.0 != 0 {
|
|
anyhow::bail!("non-zero exitcode: {:?}", res);
|
|
}
|
|
let lsn = Lsn::from_str(&res.1)?;
|
|
Ok(lsn)
|
|
}
|
|
|
|
/// Spawn a new sync_safekeepers thread.
|
|
pub fn launch_sync_safekeepers(&self) -> WalProposer {
|
|
WalProposer::launch_sync(self.ttid, self.sk_list.clone(), self.world.new_node())
|
|
}
|
|
|
|
/// Spawn a new walproposer thread.
|
|
pub fn launch_walproposer(&self, lsn: Lsn) -> WalProposer {
|
|
let lsn = if lsn.0 == 0 {
|
|
// usual LSN after basebackup
|
|
Lsn(21623024)
|
|
} else {
|
|
lsn
|
|
};
|
|
|
|
WalProposer::launch_walproposer(self.ttid, self.sk_list.clone(), self.world.new_node(), lsn)
|
|
}
|
|
|
|
/// Execute the simulation for the specified duration.
|
|
pub fn poll_for_duration(&self, duration: u64) {
|
|
let time_limit = std::cmp::min(self.world.now() + duration, self.timeout);
|
|
while self.world.step() && self.world.now() < time_limit {}
|
|
}
|
|
|
|
/// Execute the simulation together with events defined in some schedule.
|
|
pub fn run_schedule(&self, schedule: &Schedule) -> anyhow::Result<()> {
|
|
// scheduling empty events so that world will stop in those points
|
|
{
|
|
let clock = self.world.clock();
|
|
|
|
let now = self.world.now();
|
|
for (time, _) in schedule {
|
|
if *time < now {
|
|
continue;
|
|
}
|
|
clock.schedule_fake(*time - now);
|
|
}
|
|
}
|
|
|
|
let mut wp = self.launch_sync_safekeepers();
|
|
|
|
let mut skipped_tx = 0;
|
|
let mut started_tx = 0;
|
|
|
|
let mut schedule_ptr = 0;
|
|
|
|
loop {
|
|
if wp.sync_safekeepers && wp.thread.is_finished() {
|
|
let res = wp.thread.result();
|
|
if res.0 != 0 {
|
|
warn!("sync non-zero exitcode: {:?}", res);
|
|
debug!("restarting sync_safekeepers");
|
|
// restart the sync_safekeepers
|
|
wp = self.launch_sync_safekeepers();
|
|
continue;
|
|
}
|
|
let lsn = Lsn::from_str(&res.1)?;
|
|
debug!("sync_safekeepers finished at LSN {}", lsn);
|
|
wp = self.launch_walproposer(lsn);
|
|
debug!("walproposer started at thread {}", wp.thread.id());
|
|
}
|
|
|
|
let now = self.world.now();
|
|
while schedule_ptr < schedule.len() && schedule[schedule_ptr].0 <= now {
|
|
if now != schedule[schedule_ptr].0 {
|
|
warn!("skipped event {:?} at {}", schedule[schedule_ptr], now);
|
|
}
|
|
|
|
let action = &schedule[schedule_ptr].1;
|
|
match action {
|
|
TestAction::WriteTx(size) => {
|
|
if !wp.sync_safekeepers && !wp.thread.is_finished() {
|
|
started_tx += *size;
|
|
wp.write_tx(*size);
|
|
debug!("written {} transactions", size);
|
|
} else {
|
|
skipped_tx += size;
|
|
debug!("skipped {} transactions", size);
|
|
}
|
|
}
|
|
TestAction::RestartSafekeeper(id) => {
|
|
debug!("restarting safekeeper {}", id);
|
|
self.servers[*id].restart();
|
|
}
|
|
TestAction::RestartWalProposer => {
|
|
debug!("restarting sync_safekeepers");
|
|
wp.stop();
|
|
wp = self.launch_sync_safekeepers();
|
|
}
|
|
}
|
|
schedule_ptr += 1;
|
|
}
|
|
|
|
if schedule_ptr == schedule.len() {
|
|
break;
|
|
}
|
|
let next_event_time = schedule[schedule_ptr].0;
|
|
|
|
// poll until the next event
|
|
if wp.thread.is_finished() {
|
|
while self.world.step() && self.world.now() < next_event_time {}
|
|
} else {
|
|
while self.world.step()
|
|
&& self.world.now() < next_event_time
|
|
&& !wp.thread.is_finished()
|
|
{}
|
|
}
|
|
}
|
|
|
|
debug!(
|
|
"finished schedule, total steps: {}",
|
|
self.world.get_thread_step_count()
|
|
);
|
|
debug!("skipped_tx: {}", skipped_tx);
|
|
debug!("started_tx: {}", started_tx);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum TestAction {
|
|
WriteTx(usize),
|
|
RestartSafekeeper(usize),
|
|
RestartWalProposer,
|
|
}
|
|
|
|
pub type Schedule = Vec<(u64, TestAction)>;
|
|
|
|
pub fn generate_schedule(seed: u64) -> Schedule {
|
|
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
|
|
let mut schedule = Vec::new();
|
|
let mut time = 0;
|
|
|
|
let cnt = rng.random_range(1..100);
|
|
|
|
for _ in 0..cnt {
|
|
time += rng.random_range(0..500);
|
|
let action = match rng.random_range(0..3) {
|
|
0 => TestAction::WriteTx(rng.random_range(1..10)),
|
|
1 => TestAction::RestartSafekeeper(rng.random_range(0..3)),
|
|
2 => TestAction::RestartWalProposer,
|
|
_ => unreachable!(),
|
|
};
|
|
schedule.push((time, action));
|
|
}
|
|
|
|
schedule
|
|
}
|
|
|
|
pub fn generate_network_opts(seed: u64) -> NetworkOptions {
|
|
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
|
|
|
|
let timeout = rng.random_range(100..2000);
|
|
let max_delay = rng.random_range(1..2 * timeout);
|
|
let min_delay = rng.random_range(1..=max_delay);
|
|
|
|
let max_fail_prob = rng.random_range(0.0..0.9);
|
|
let connect_fail_prob = rng.random_range(0.0..max_fail_prob);
|
|
let send_fail_prob = rng.random_range(0.0..connect_fail_prob);
|
|
|
|
NetworkOptions {
|
|
keepalive_timeout: Some(timeout),
|
|
connect_delay: Delay {
|
|
min: min_delay,
|
|
max: max_delay,
|
|
fail_prob: connect_fail_prob,
|
|
},
|
|
send_delay: Delay {
|
|
min: min_delay,
|
|
max: max_delay,
|
|
fail_prob: send_fail_prob,
|
|
},
|
|
}
|
|
}
|