use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use kglite::api::io::{open_or_create_graph_in_mode, GraphWriterLease, OpenDisposition};
use kglite::api::storage::StorageMode;
use kglite::api::DirGraph;
const WRITER_LEASE_TIMEOUT: Duration = Duration::ZERO;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StartupStep {
LeaseAcquired,
GraphOpened,
}
pub(crate) struct StartedGraph {
pub(crate) graph: Arc<DirGraph>,
pub(crate) disposition: OpenDisposition,
pub(crate) converted_from: Option<StorageMode>,
pub(crate) writer_lease: Option<GraphWriterLease>,
}
pub(crate) fn start_graph(
path: &Path,
requested_mode: Option<StorageMode>,
readonly: bool,
record: &mut dyn FnMut(StartupStep),
) -> Result<StartedGraph> {
let writer_lease = if readonly {
None
} else {
let lease = GraphWriterLease::acquire(path, WRITER_LEASE_TIMEOUT).with_context(|| {
format!(
"acquiring the writer lease for {}; pass --readonly to serve this graph \
alongside its writer",
path.display()
)
})?;
record(StartupStep::LeaseAcquired);
Some(lease)
};
let opened = open_or_create_graph_in_mode(path, requested_mode)
.with_context(|| format!("opening or creating {}", path.display()))?;
record(StartupStep::GraphOpened);
Ok(StartedGraph {
graph: opened.graph,
disposition: opened.disposition,
converted_from: opened.converted_from,
writer_lease,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
struct ScratchDir(PathBuf);
impl ScratchDir {
fn new(tag: &str) -> Self {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"kglite-bolt-startup-{tag}-{}-{nonce}",
std::process::id()
));
std::fs::create_dir_all(&dir).expect("create scratch dir");
Self(dir)
}
fn graph_path(&self) -> PathBuf {
self.0.join("graph.kgl")
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn writable_startup_takes_the_lease_before_opening_the_graph() {
let scratch = ScratchDir::new("order");
let mut steps = Vec::new();
let started = start_graph(
&scratch.graph_path(),
Some(StorageMode::Memory),
false,
&mut |step| steps.push(step),
)
.expect("writable startup on a free path");
assert_eq!(
steps,
vec![StartupStep::LeaseAcquired, StartupStep::GraphOpened]
);
assert!(
started.writer_lease.is_some(),
"a writable server must retain the lease it acquired"
);
}
#[test]
fn second_writable_startup_fails_naming_the_holder() {
let scratch = ScratchDir::new("contended");
let path = scratch.graph_path();
let held = GraphWriterLease::acquire(&path, Duration::ZERO).expect("first writer");
let mut steps = Vec::new();
let error = match start_graph(&path, Some(StorageMode::Memory), false, &mut |step| {
steps.push(step)
}) {
Ok(_) => panic!("a second writable server must not open a leased graph"),
Err(error) => error,
};
let message = format!("{error:#}");
assert!(
message.contains(&format!("pid {}", std::process::id())),
"refusal must name the holding process: {message}"
);
assert!(
message.contains("only one process may write a graph at a time"),
"refusal must explain the constraint: {message}"
);
assert!(
steps.is_empty(),
"a refused writer must not have touched the graph: {steps:?}"
);
drop(held);
}
use kglite::api::storage::live_storage_mode;
fn seed_graph(path: &Path, mode: StorageMode) {
let mut graph = Arc::new(
kglite::api::storage::new_dir_graph_in_mode(mode, None).expect("portable mode"),
);
kglite::api::io::save_graph(&mut graph, &path.to_string_lossy()).expect("seed save");
}
fn start(path: &Path, requested: Option<StorageMode>) -> Result<StartedGraph> {
start_graph(path, requested, false, &mut |_| {})
}
#[test]
fn no_storage_flag_serves_the_mode_the_checkpoint_recorded() {
let scratch = ScratchDir::new("recorded");
let path = scratch.graph_path();
seed_graph(&path, StorageMode::Mapped);
let started = start(&path, None).expect("startup on a mapped checkpoint");
assert_eq!(
live_storage_mode(&started.graph),
StorageMode::Mapped,
"with no --storage the file decides, and this one recorded mapped"
);
}
#[test]
fn explicit_matching_storage_proceeds_without_converting() {
let scratch = ScratchDir::new("agree");
let path = scratch.graph_path();
seed_graph(&path, StorageMode::Mapped);
let started = start(&path, Some(StorageMode::Mapped)).expect("agreeing request");
assert_eq!(live_storage_mode(&started.graph), StorageMode::Mapped);
assert_eq!(
started.converted_from, None,
"nothing was converted, so nothing may be reported as converted"
);
}
#[test]
fn explicit_mismatching_portable_storage_converts_and_reports_it() {
for (recorded, requested) in [
(StorageMode::Memory, StorageMode::Mapped),
(StorageMode::Mapped, StorageMode::Memory),
] {
let scratch = ScratchDir::new("convert");
let path = scratch.graph_path();
seed_graph(&path, recorded);
let mut steps = Vec::new();
let started = start_graph(&path, Some(requested), false, &mut |step| steps.push(step))
.expect("portable conversion at startup");
assert_eq!(
live_storage_mode(&started.graph),
requested,
"the server must serve the mode the operator asked for"
);
assert_eq!(
started.converted_from,
Some(recorded),
"a conversion the operator did not see is as bad as an ignored flag"
);
assert_eq!(
steps,
vec![StartupStep::LeaseAcquired, StartupStep::GraphOpened],
"converting must not disturb the acquire-then-open order"
);
}
}
#[test]
fn disk_request_on_a_portable_graph_is_refused_with_the_core_reason() {
let scratch = ScratchDir::new("disk-request");
let path = scratch.graph_path();
seed_graph(&path, StorageMode::Memory);
let error = match start(&path, Some(StorageMode::Disk)) {
Ok(_) => panic!("a disk request on a `.kgl` must not be silently ignored"),
Err(error) => format!("{error:#}"),
};
assert!(
error.contains("enable_disk_mode()") && error.contains("directory"),
"the refusal must carry the core reason and remedy: {error}"
);
start(&path, None).expect("the path stays openable after a refusal");
}
#[test]
fn readonly_startup_succeeds_beside_a_held_lease() {
let scratch = ScratchDir::new("readonly");
let path = scratch.graph_path();
let held = GraphWriterLease::acquire(&path, Duration::ZERO).expect("writer lease");
let mut steps = Vec::new();
let started = start_graph(&path, Some(StorageMode::Memory), true, &mut |step| {
steps.push(step)
})
.expect("readonly startup beside a live writer");
assert_eq!(steps, vec![StartupStep::GraphOpened]);
assert!(
started.writer_lease.is_none(),
"a readonly server must not take write ownership"
);
drop(held);
}
}