use std::time::{Duration, Instant};
use crate::heartbeat::STATE_SILENT;
use crate::twin_owed::{self, Book};
pub const TARGET: &str = "gunnar::twin";
pub const EVERY: Duration = Duration::from_secs(600);
pub const DEFAULT_WINDOW: u64 = 64;
pub const RETAIN_ENV: &str = "GUNNAR_TXLOG_RETAIN";
pub const WINDOW_ENV: &str = "GUNNAR_TXLOG_RETAIN_WINDOW";
pub const CHECK_EVERY: Duration = Duration::from_secs(30);
pub const DEFAULT_BUDGET_PERCENT: u64 = 5;
pub const DEFAULT_BUDGET_MAX_BYTES: u64 = 4 << 30;
pub const DEFAULT_SILENT_GRACE: Duration = Duration::from_secs(600);
pub const BUDGET_PERCENT_ENV: &str = "GUNNAR_TXLOG_RETAIN_PERCENT";
pub const BUDGET_BYTES_ENV: &str = "GUNNAR_TXLOG_RETAIN_BYTES";
pub const SILENT_GRACE_ENV: &str = "GUNNAR_TXLOG_SILENT_GRACE_SECS";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TwinAt {
pub principal: String,
pub applied: Option<u64>,
pub state: String,
pub silent_for_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Policy {
pub window: u64,
pub budget_percent: u64,
pub budget_max_bytes: u64,
pub silent_grace_ms: u64,
}
impl Policy {
pub fn window_only(window: u64) -> Self {
Self { window, budget_percent: 0, budget_max_bytes: 0, silent_grace_ms: u64::MAX }
}
pub fn budget(&self, data_total: Option<u64>) -> Option<u64> {
let share = data_total.filter(|_| self.budget_percent > 0).map(|t| (u128::from(t) * u128::from(self.budget_percent) / 100) as u64);
let cap = (self.budget_max_bytes > 0).then_some(self.budget_max_bytes);
match (share, cap) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
pub fn from_env(retain: Option<&str>, window: Option<&str>, percent: Option<&str>, bytes: Option<&str>, grace_secs: Option<&str>) -> Option<Self> {
let window = window_from_env(retain, window)?;
Some(Self {
window,
budget_percent: percent.and_then(|p| p.trim().parse().ok()).unwrap_or(DEFAULT_BUDGET_PERCENT).min(100),
budget_max_bytes: bytes.and_then(parse_bytes).unwrap_or(DEFAULT_BUDGET_MAX_BYTES),
silent_grace_ms: grace_secs.and_then(|g| g.trim().parse::<u64>().ok()).map(|g| g.saturating_mul(1000)).unwrap_or(DEFAULT_SILENT_GRACE.as_millis() as u64),
})
}
}
pub fn parse_bytes(v: &str) -> Option<u64> {
let v = v.trim();
let split = v.find(|c: char| !c.is_ascii_digit()).unwrap_or(v.len());
let (n, unit) = v.split_at(split);
let n: u64 = n.parse().ok()?;
let shift = match unit.trim().to_ascii_lowercase().as_str() {
"" | "b" => 0,
"k" | "kb" | "kib" => 10,
"m" | "mb" | "mib" => 20,
"g" | "gb" | "gib" => 30,
_ => return None,
};
n.checked_mul(1u64 << shift)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Plan {
pub from: u64,
pub gave_up: Vec<String>,
pub why: String,
}
pub fn decide(layout: &[(u64, u64, u64)], head: u64, twins: &[TwinAt], policy: &Policy, budget: Option<u64>, full: bool) -> Result<Plan, String> {
if twins.is_empty() {
return Err("no twin is enrolled: the log is the only second copy of history, nothing is trimmed".to_owned());
}
let first = layout.first().map(|s| s.0).unwrap_or(head + 1);
let bytes: u64 = layout.iter().map(|s| s.2).sum();
let over = budget.is_some_and(|b| bytes > b);
let hard = budget.is_some_and(|b| bytes > b.saturating_mul(2));
let mut gave_up = Vec::new();
let mut slowest: Option<(&str, u64)> = None;
for t in twins {
let silent = t.applied.is_none() || t.state == STATE_SILENT;
if silent {
if over && (t.silent_for_ms >= policy.silent_grace_ms || hard) {
gave_up.push(t.principal.clone());
continue;
}
return Err(match t.applied {
None => format!("twin '{}' has not reported since this server started: its position is unknown, nothing is trimmed", t.principal),
Some(applied) => format!(
"twin '{}' is silent (last applied {applied}): nothing is trimmed while a twin may need what is cut{}",
t.principal,
match budget {
Some(b) if over => format!(" (the log is {bytes} bytes, over its {b}-byte budget; it is given up on after {} s of silence, or at twice the budget)", policy.silent_grace_ms / 1000),
Some(b) => format!(" (the log is {bytes} bytes, within its {b}-byte budget)"),
None => String::new(),
}
),
});
}
let applied = t.applied.unwrap_or(0);
if applied + 1 < first {
continue;
}
if slowest.is_none_or(|(_, a)| applied < a) {
slowest = Some((&t.principal, applied));
}
}
let acked = slowest.map(|(_, a)| a.min(head)).unwrap_or(head);
let from_window = (acked + 1).saturating_sub(policy.window).max(1);
let mut from = from_window;
let mut why = match slowest {
Some((p, a)) => format!("the slowest twin '{p}' applied {a}, less a window of {} entries", policy.window),
None => "no twin that is heard holds anything back".to_owned(),
};
if let Some(b) = budget {
let mut keep_from = head + 1;
let mut kept = 0u64;
for (sf, _, sb) in layout.iter().rev() {
if kept + sb > b {
break;
}
kept += sb;
keep_from = *sf;
}
let keep_from = keep_from.min(acked + 1);
if keep_from > from {
from = keep_from;
why = format!("{why}; the log is {bytes} bytes and its budget {b}: only what fits it is kept, and only what that twin acknowledged goes");
}
}
if !gave_up.is_empty() {
why = format!("{why}; NOT waited for any more (silent past the grace with the log over its budget — owed a full resync): {}", gave_up.join(", "));
}
if from <= first {
return Err(format!("nothing to trim: the log starts at {first} and {why} keeps it from {from}"));
}
if !full {
let droppable = layout.iter().any(|(f, l, _)| l >= f && *l < from);
let seal_helps = over && layout.last().is_some_and(|(f, l, _)| l >= f && *f < from);
if !droppable && !seal_helps {
return Err(format!("nothing to trim yet: no whole segment lies below {from} ({why})"));
}
}
Ok(Plan { from, gave_up, why })
}
pub fn point(first: u64, head: u64, twins: &[TwinAt], window: u64) -> Result<u64, String> {
if twins.is_empty() {
return Err("no twin is enrolled: the log is the only second copy of history, nothing is trimmed".to_owned());
}
let mut slowest: Option<(&str, u64)> = None;
for t in twins {
let Some(applied) = t.applied else {
return Err(format!("twin '{}' has not reported since this server started: its position is unknown, nothing is trimmed", t.principal));
};
if t.state == STATE_SILENT {
return Err(format!("twin '{}' is silent (last applied {applied}): nothing is trimmed while a twin may need what is cut", t.principal));
}
if slowest.is_none_or(|(_, a)| applied < a) {
slowest = Some((&t.principal, applied));
}
}
let (_, applied) = slowest.expect("at least one twin");
let from = (applied.min(head) + 1).saturating_sub(window).max(1);
if from <= first {
return Err(format!("nothing to trim: the log starts at {first} and the slowest twin at {applied} minus the window of {window} keeps it from {from}"));
}
Ok(from)
}
pub fn window_from_env(retain: Option<&str>, window: Option<&str>) -> Option<u64> {
if retain.map(str::trim).is_some_and(|v| v.eq_ignore_ascii_case("off")) {
return None;
}
Some(window.and_then(|w| w.trim().parse().ok()).unwrap_or(DEFAULT_WINDOW))
}
pub fn pass(ctx: &crate::ServiceContext, window: u64) -> Result<String, String> {
pass_with(ctx, &Policy::window_only(window), true)
}
pub fn note_debts(book: &mut Book, rows: &[gunnar_proto::TwinHeartbeat], reported: &dyn Fn(&str) -> bool, data_total: Option<u64>, first_seq: u64, now_ms: i64) -> Vec<String> {
let mut said = Vec::new();
for r in rows {
let o = book.twins.entry(r.principal.clone()).or_default();
let heard = r.state != STATE_SILENT && reported(&r.principal);
if !heard {
if o.silent_since_ms == 0 {
o.silent_since_ms = if r.last_seen_unix_ms > 0 { r.last_seen_unix_ms } else { now_ms };
o.main_total_at_silence = data_total.unwrap_or(0);
}
if o.main_total_at_silence == 0 {
o.main_total_at_silence = data_total.unwrap_or(0);
}
if r.disk_total_bytes > 0 {
o.twin_total = r.disk_total_bytes;
}
if let Some(total) = data_total {
let grew = o.main_total_at_silence > 0 && twin_owed::clearly_above(total, o.main_total_at_silence);
let outgrown = o.twin_total == 0 || twin_owed::clearly_above(total, o.twin_total);
if grew && outgrown && total > o.grow_to_bytes {
o.grow_to_bytes = total;
o.why = format!(
"main's data set grew from {} to {} while twin '{}' was silent (its volume {}): it may not follow again until it holds main's set",
twin_owed::gib(o.main_total_at_silence),
twin_owed::gib(total),
r.principal,
if o.twin_total == 0 { "unknown".to_owned() } else { twin_owed::gib(o.twin_total) }
);
said.push(o.why.clone());
}
}
} else {
o.silent_since_ms = 0;
o.main_total_at_silence = 0;
if r.disk_total_bytes > 0 {
o.twin_total = r.disk_total_bytes;
}
if o.grow_to_bytes > 0 && r.disk_total_bytes > 0 && !twin_owed::clearly_above(o.grow_to_bytes, r.disk_total_bytes) {
said.push(format!("twin '{}' holds main's grown set again ({} for {}): it follows", r.principal, twin_owed::gib(r.disk_total_bytes), twin_owed::gib(o.grow_to_bytes)));
o.grow_to_bytes = 0;
}
if o.resync_from > 0 && r.applied_seq + 1 >= first_seq {
said.push(format!("twin '{}' took its full resync: it reports {} applied and the log retains from {first_seq}", r.principal, r.applied_seq));
o.resync_from = 0;
}
if o.grow_to_bytes == 0 && o.resync_from == 0 {
o.why.clear();
}
}
}
book.twins.retain(|_, o| !o.is_empty());
said
}
pub fn pass_with(ctx: &crate::ServiceContext, policy: &Policy, full: bool) -> Result<String, String> {
let head = ctx.control.log_head().map_err(|e| format!("the log's head: {e}"))?;
if head.twin {
return Err("this box is a twin: its log is the primary's numbers and its only copy, never trimmed here".to_owned());
}
let members: Vec<String> = ctx
.control
.list_group_members(crate::TWIN_GROUP)
.map(|ps| ps.into_iter().map(|p| p.id.to_string()).collect())
.unwrap_or_default();
let now = estate_clock::now_unix_ms();
let rows = ctx.heartbeats.view(&members, now);
let data = ctx.control.data_volume();
let mut book = ctx.heartbeats.owed();
let before = book.clone();
for line in note_debts(&mut book, &rows, &|p| ctx.heartbeats.has_reported(p), data.map(|d| d.0), head.first_seq, now) {
tracing::warn!(target: TARGET, "{line}");
}
let twins: Vec<TwinAt> = rows
.iter()
.map(|r| {
let applied = ctx.heartbeats.has_reported(&r.principal).then_some(r.applied_seq);
let silent = applied.is_none() || r.state == STATE_SILENT;
let since = book.twins.get(&r.principal).map(|o| o.silent_since_ms).unwrap_or(0);
TwinAt {
principal: r.principal.clone(),
applied,
state: r.state.clone(),
silent_for_ms: if silent && since > 0 { now.saturating_sub(since).max(0) as u64 } else { 0 },
}
})
.collect();
let layout = ctx.control.log_layout().unwrap_or_else(|_| vec![(head.first_seq, head.head_seq, head.bytes)]);
let budget = policy.budget(data.map(|d| d.0));
let decided = decide(&layout, head.head_seq, &twins, policy, budget, full);
let result = match decided {
Err(why) => Err(why),
Ok(plan) => match ctx.control.log_retain(plan.from) {
Err(e) => Err(format!("retaining from {}: {e}", plan.from)),
Ok(after) => {
for p in &plan.gave_up {
let o = book.twins.entry(p.clone()).or_default();
let first_time = o.resync_from == 0;
o.resync_from = after.first_seq;
o.why = format!("the log was trimmed past twin '{p}' while it was silent (the log is bounded in bytes): it retains from {}, and the twin takes a full resync (snapshot) when it polls again", after.first_seq);
if first_time {
tracing::warn!(target: TARGET, twin = %p, first = after.first_seq, "{}", o.why);
} else {
tracing::info!(target: TARGET, twin = %p, first = after.first_seq, "{}", o.why);
}
}
let line = if after.first_seq > head.first_seq {
format!(
"the default retention trimmed the transaction log: it now starts at {} (was {}; retained from {}), head {}, {} bytes (was {}){}; {}",
after.first_seq,
head.first_seq,
plan.from,
after.head_seq,
after.bytes,
head.bytes,
budget.map(|b| format!(", budget {b}")).unwrap_or_default(),
plan.why
)
} else {
format!("the default retention retained from {}: no whole segment lay below it yet (the log still starts at {}), head {}; {}", plan.from, after.first_seq, after.head_seq, plan.why)
};
tracing::info!(target: TARGET, first = after.first_seq, was = head.first_seq, head = after.head_seq, bytes = after.bytes, window = policy.window, "{line}");
Ok(line)
}
},
};
if book != before {
ctx.heartbeats.set_owed(book.clone());
if let Err(e) = ctx.control.twin_owed_write(&book.to_json()) {
tracing::debug!(target: TARGET, "twin-owed.json was not written (kept in memory): {e}");
}
}
result
}
pub async fn run(ctx: crate::ServiceContext) {
let env = |k: &str| std::env::var(k).ok();
let Some(policy) = Policy::from_env(env(RETAIN_ENV).as_deref(), env(WINDOW_ENV).as_deref(), env(BUDGET_PERCENT_ENV).as_deref(), env(BUDGET_BYTES_ENV).as_deref(), env(SILENT_GRACE_ENV).as_deref()) else {
tracing::warn!(target: TARGET, "{RETAIN_ENV}=off: the transaction log is trimmed only by an operator's Mirror.LogRetain");
return;
};
match ctx.control.twin_owed_read() {
Ok(bytes) => match Book::from_json(&bytes) {
Ok(book) => {
if !book.twins.is_empty() {
tracing::warn!(target: TARGET, owed = book.twins.len(), "twin-owed.json: {} twin(s) owed a resync or a growth", book.twins.len());
}
ctx.heartbeats.set_owed(book);
}
Err(e) => tracing::warn!(target: TARGET, "{e}: starting with an empty book of what the twins are owed"),
},
Err(e) => tracing::debug!(target: TARGET, "twin-owed.json not read: {e}"),
}
tracing::info!(
target: TARGET,
window = policy.window,
budget_percent = policy.budget_percent,
budget_max_bytes = policy.budget_max_bytes,
silent_grace_s = policy.silent_grace_ms / 1000,
"the transaction log's retention: {} entries behind the slowest twin, at most {} % of the data volume and {} bytes, a silent twin waited for {} s while it is over",
policy.window,
policy.budget_percent,
policy.budget_max_bytes,
policy.silent_grace_ms / 1000
);
let mut every = tokio::time::interval(CHECK_EVERY);
every.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
every.tick().await;
let mut last_full = Instant::now();
loop {
every.tick().await;
let full = last_full.elapsed() >= EVERY;
if full {
last_full = Instant::now();
}
let c = ctx.clone();
match tokio::task::spawn_blocking(move || pass_with(&c, &policy, full)).await {
Ok(Ok(_)) => {}
Ok(Err(why)) => tracing::debug!(target: TARGET, "default retention: {why}"),
Err(e) => tracing::warn!(target: TARGET, "default retention pass panicked: {e}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn at(p: &str, applied: Option<u64>, state: &str) -> TwinAt {
TwinAt { principal: p.into(), applied, state: state.into(), silent_for_ms: 0 }
}
#[test]
fn the_log_is_retained_behind_the_slowest_reporting_twin_minus_the_window() {
assert_eq!(point(1, 5_000, &[at("gunnar-twin", Some(4_990), "ok")], 1_000), Ok(3_991));
assert_eq!(point(1, 5_000, &[at("a", Some(4_990), "ok"), at("b", Some(2_500), "behind")], 1_000), Ok(1_501), "the SLOWEST twin decides");
assert_eq!(point(2_000, 5_000, &[at("a", Some(4_990), "ok")], 0), Ok(4_991), "window 0: everything the twin has applied");
}
#[test]
fn nothing_is_trimmed_without_a_twin_or_with_a_silent_or_unheard_one() {
assert!(point(1, 5_000, &[], 1_000).unwrap_err().contains("no twin"));
let e = point(1, 5_000, &[at("a", Some(4_990), "ok"), at("b", None, "silent")], 1_000).unwrap_err();
assert!(e.contains("'b'") && e.contains("not reported"), "{e}");
let e = point(1, 5_000, &[at("a", Some(4_990), STATE_SILENT)], 1_000).unwrap_err();
assert!(e.contains("silent"), "{e}");
}
#[test]
fn nothing_is_trimmed_when_the_window_already_covers_the_log() {
let e = point(3_991, 5_000, &[at("a", Some(4_990), "ok")], 1_000).unwrap_err();
assert!(e.contains("nothing to trim"), "{e}");
let e = point(1, 500, &[at("a", Some(500), "ok")], 1_000).unwrap_err();
assert!(e.contains("nothing to trim"), "{e}");
assert_eq!(point(1, 5_000, &[at("a", Some(9_999), "ok")], 0), Ok(5_001));
}
#[test]
fn the_default_window_is_a_few_pushes() {
const {
assert!(DEFAULT_WINDOW <= 64);
assert!(DEFAULT_WINDOW >= 16);
}
}
fn layout() -> Vec<(u64, u64, u64)> {
let mut l: Vec<(u64, u64, u64)> = (0..10).map(|i| (i * 10 + 1, i * 10 + 10, 100)).collect();
l.push((101, 100, 0));
l
}
fn silent(p: &str, applied: u64, for_ms: u64) -> TwinAt {
TwinAt { principal: p.into(), applied: Some(applied), state: STATE_SILENT.into(), silent_for_ms: for_ms }
}
const POLICY: Policy = Policy { window: 64, budget_percent: 0, budget_max_bytes: 250, silent_grace_ms: 600_000 };
#[test]
fn the_budget_keeps_the_newest_whole_segments_that_fit_and_only_what_the_twin_applied_goes() {
let p = decide(&layout(), 100, &[at("t", Some(100), "ok")], &POLICY, Some(250), false).unwrap();
assert_eq!((p.from, p.gave_up.len()), (81, 0), "{p:?}");
let p = decide(&layout(), 100, &[at("t", Some(50), "behind")], &POLICY, Some(250), false).unwrap();
assert_eq!(p.from, 51, "{p:?}");
assert_eq!(decide(&layout(), 100, &[at("t", Some(100), "ok")], &POLICY, None, true).unwrap().from, 37);
}
#[test]
fn a_silent_twin_is_given_up_on_past_its_grace_or_at_twice_the_budget_and_never_otherwise() {
let e = decide(&layout(), 100, &[silent("t", 40, 60_000)], &POLICY, Some(600), false).unwrap_err();
assert!(e.contains("silent") && e.contains("given up on after 600 s"), "{e}");
let p = decide(&layout(), 100, &[silent("t", 40, 600_000)], &POLICY, Some(600), false).unwrap();
assert_eq!((p.from, p.gave_up.clone()), (41, vec!["t".to_string()]), "{p:?}");
let p = decide(&layout(), 100, &[silent("t", 40, 1_000)], &POLICY, Some(250), false).unwrap();
assert_eq!((p.from, p.gave_up.len()), (81, 1), "{p:?}");
assert!(decide(&layout(), 100, &[silent("t", 40, u64::MAX)], &POLICY, Some(5_000), false).is_err());
let p = decide(&layout(), 100, &[silent("gone", 10, 900_000), at("alive", Some(70), "ok")], &POLICY, Some(250), false).unwrap();
assert_eq!((p.from, p.gave_up.clone()), (71, vec!["gone".to_string()]), "{p:?}");
}
#[test]
fn the_light_pass_trims_only_whole_segments_or_seals_to_let_the_next_one() {
let one = vec![(1, 100, 1_000)];
assert!(decide(&one, 100, &[at("t", Some(50), "ok")], &POLICY, Some(5_000), false).is_err());
assert!(decide(&one, 100, &[at("t", Some(100), "ok")], &POLICY, None, true).is_ok());
assert_eq!(decide(&one, 100, &[at("t", Some(50), "ok")], &POLICY, Some(250), false).unwrap().from, 51);
}
#[test]
fn the_budget_is_the_smaller_of_its_share_and_its_ceiling() {
let p = Policy { window: 64, budget_percent: 5, budget_max_bytes: 4 << 30, silent_grace_ms: 0 };
assert_eq!(p.budget(Some(20 << 30)), Some(1 << 30), "5 % of 20 GiB");
assert_eq!(p.budget(Some(300 << 30)), Some(4 << 30), "the ceiling on a big set");
assert_eq!(p.budget(None), Some(4 << 30), "an unknown volume: the ceiling");
assert_eq!(Policy::window_only(64).budget(Some(1 << 40)), None);
let env = Policy::from_env(None, None, Some("10"), Some("2GiB"), Some("30")).unwrap();
assert_eq!((env.budget_percent, env.budget_max_bytes, env.silent_grace_ms), (10, 2 << 30, 30_000));
let dflt = Policy::from_env(None, None, None, None, None).unwrap();
assert_eq!((dflt.window, dflt.budget_percent, dflt.budget_max_bytes, dflt.silent_grace_ms), (DEFAULT_WINDOW, 5, 4 << 30, 600_000));
assert_eq!(Policy::from_env(Some("off"), None, None, None, None), None);
assert_eq!((parse_bytes("512KiB"), parse_bytes("64M"), parse_bytes("123"), parse_bytes("lots")), (Some(512 << 10), Some(64 << 20), Some(123), None));
}
#[test]
fn a_twin_silent_through_mains_growth_is_owed_it_and_settles_when_its_volume_holds_it() {
const G: u64 = 1 << 30;
let row = |state: &str, seen: i64, total: u64, applied: u64| gunnar_proto::TwinHeartbeat { principal: "t".into(), state: state.into(), last_seen_unix_ms: seen, disk_total_bytes: total, applied_seq: applied, ..Default::default() };
let yes = |_: &str| true;
let mut book = Book::default();
note_debts(&mut book, &[row("ok", 1_000, 20 * G, 5)], &yes, Some(20 * G), 1, 1_000);
assert!(book.twins.is_empty(), "{book:?}");
note_debts(&mut book, &[row(STATE_SILENT, 1_000, 20 * G, 5)], &yes, Some(20 * G), 1, 90_000);
assert_eq!(book.twins["t"].main_total_at_silence, 20 * G);
assert_eq!(book.twins["t"].grow_to_bytes, 0, "nothing grew");
let said = note_debts(&mut book, &[row(STATE_SILENT, 1_000, 20 * G, 5)], &yes, Some(30 * G), 1, 400_000);
assert_eq!(book.twins["t"].grow_to_bytes, 30 * G, "{said:?}");
assert!(twin_owed::follow_refusal(&book.twins["t"], 20 * G).is_some());
note_debts(&mut book, &[row("ok", 500_000, 20 * G, 5)], &yes, Some(30 * G), 1, 500_000);
assert_eq!(book.twins["t"].grow_to_bytes, 30 * G);
let said = note_debts(&mut book, &[row("ok", 600_000, 30 * G, 5)], &yes, Some(30 * G), 1, 600_000);
assert!(book.twins.is_empty(), "{book:?} {said:?}");
let mut b2 = Book::default();
note_debts(&mut b2, &[row(STATE_SILENT, 1_000, 60 * G - G / 4, 5)], &yes, Some(60 * G), 1, 90_000);
note_debts(&mut b2, &[row(STATE_SILENT, 1_000, 60 * G - G / 4, 5)], &yes, Some(61 * G), 1, 400_000);
assert_eq!(b2.twins["t"].grow_to_bytes, 0, "main grew under 2 %: no debt");
}
#[test]
fn the_switch_and_the_window_come_from_the_environment() {
assert_eq!(window_from_env(None, None), Some(DEFAULT_WINDOW));
assert_eq!(window_from_env(Some("OFF"), None), None);
assert_eq!(window_from_env(Some("on"), Some("50")), Some(50));
assert_eq!(window_from_env(None, Some("nonsense")), Some(DEFAULT_WINDOW));
}
}