use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::core::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
use anyhow::{bail, ensure, Context, Result};
use super::HmsCore;
use crate::core::documents::{
self, Backing, CheckpointBase, DocumentStore, ServingImage, TextSegment,
};
pub(super) const FILE: &str = "documents.ckpt";
const SEALED_MAGIC: &[u8; 8] = b"HMSCKPE\x01";
const SEALED_HEADER: usize = 40;
pub(super) enum Opened {
Absent,
Served,
Overlaid,
}
#[derive(Default)]
pub(super) struct ImageState {
serving: RwLock<Option<Arc<ServingImage>>>,
hydrating: Mutex<()>,
pub(super) other_state: AtomicBool,
opened: AtomicBool,
}
fn word(header: &[u8], index: usize) -> u64 {
let at = 8 + 8 * index;
u64::from_le_bytes(header[at..at + 8].try_into().expect("eight bytes"))
}
impl HmsCore {
pub(super) fn documents(&self) -> RwLockReadGuard<'_, DocumentStore> {
self.hydrate_documents()
.expect("rebuilding the document store from its checkpoint");
self.documents.read()
}
pub(super) fn documents_mut(&self) -> RwLockWriteGuard<'_, DocumentStore> {
self.hydrate_documents()
.expect("rebuilding the document store from its checkpoint");
self.documents.write()
}
pub(super) fn capture_from_checkpoint<T>(
&self,
capture: impl FnOnce(&DocumentStore) -> Option<T>,
) -> Option<T> {
self.image.serving.read().as_ref()?;
let _transaction = self.mutation_gate.read();
let store = self.documents.read();
if !store.has_base() {
return None;
}
capture(&store)
}
pub(super) fn checkpoint_accepts(&self, mutations: &[super::Mutation]) -> bool {
self.image.serving.read().is_some()
&& mutations.iter().all(|mutation| {
matches!(
mutation,
super::Mutation::Document(_)
| super::Mutation::DeleteDocument { .. }
| super::Mutation::EncodeDocument { .. }
)
})
}
fn checkpoint_schema(&self) -> bool {
self.schema >= crate::core::format::CHECKPOINT_SCHEMA_VERSION
}
fn fresh_text_segment(&self) -> Result<Arc<TextSegment>> {
Ok(Arc::new(TextSegment::create(
&self.storage_path.join("documents.text"),
#[cfg(feature = "security")]
self.encryption.clone(),
)?))
}
pub(super) fn hydrate_documents(&self) -> Result<()> {
if self.image.serving.read().is_none() {
return Ok(());
}
let _hydrating = self.image.hydrating.lock();
if self.image.serving.read().is_none() {
return Ok(());
}
let mut documents = self.documents.write();
let (image, dead, written) = documents
.overlay()
.context("store served from a checkpoint without it")?;
let mut store = image.hydrate_excluding(Some(self.fresh_text_segment()?), &dead)?;
for id in &dead {
store.remove_unindexed_scrubbed(id)?;
}
store.insert_batch(written);
*documents = store;
*self.image.serving.write() = None;
Ok(())
}
pub(super) fn open_checkpoint(&self) -> Result<Opened> {
let path = self.arena.data_dir().join(FILE);
if path.symlink_metadata().is_err() {
return Ok(Opened::Absent);
}
ensure!(
self.checkpoint_schema(),
"journal generation holds a checkpoint but the store schema predates checkpoints"
);
let image = Arc::new(
self.load_checkpoint(&path)
.with_context(|| format!("document checkpoint {}", path.display()))?,
);
*self.documents.write() = DocumentStore::over(CheckpointBase::new(Arc::clone(&image)));
*self.image.serving.write() = Some(image);
Ok(if self.arena.stats().used_bytes == 0 {
Opened::Served
} else {
Opened::Overlaid
})
}
fn generation_crc(name: &str) -> u32 {
crc32fast::hash(name.as_bytes())
}
fn load_checkpoint(&self, path: &Path) -> Result<ServingImage> {
let mut file = std::fs::File::open(path)?;
let mut outer = [0u8; SEALED_HEADER];
file.read_exact(&mut outer)?;
let generation = Self::generation_crc(&self.arena.checkpoint().0);
ensure!(
(word(&outer, 1) as u32, word(&outer, 2)) == (generation, self.dimensions as u64),
"checkpoint belongs to another generation or dimension count"
);
#[cfg(feature = "security")]
let sealed_key = self.encryption.clone();
#[cfg(not(feature = "security"))]
let sealed_key: Option<()> = None;
let bytes = if &outer[..8] == SEALED_MAGIC {
#[cfg(feature = "security")]
{
let Some(cipher) = &sealed_key else {
bail!("sealed checkpoint in a store opened without encryption");
};
let mut sealed = Vec::new();
file.read_to_end(&mut sealed)?;
Backing::Owned(cipher.decrypt_with_aad(&sealed, &outer)?)
}
#[cfg(not(feature = "security"))]
bail!("sealed checkpoint needs the security feature");
} else {
ensure!(
&outer[..8] == documents::CHECKPOINT_MAGIC,
"not a document checkpoint of this version"
);
ensure!(
sealed_key.is_none(),
"plaintext checkpoint in an encrypted store"
);
let mut hasher = crc32fast::Hasher::new();
hasher.update(&outer[documents::CHECKPOINT_CRC_START..]);
let mut buffer = vec![0u8; 1 << 16];
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
file.seek(SeekFrom::Start(0))?;
let mut header = vec![0u8; documents::CHECKPOINT_HEADER_LEN];
file.read_exact(&mut header)?;
ensure!(
hasher.finalize() == documents::checkpoint_crc(&header),
"checkpoint checksum mismatch"
);
Backing::Mapped(unsafe { memmap2::Mmap::map(&file)? })
};
ensure!(
bytes.len() >= documents::CHECKPOINT_HEADER_LEN
&& documents::checkpoint_crc(&bytes)
== crc32fast::hash(&bytes[documents::CHECKPOINT_CRC_START..])
&& documents::checkpoint_binding(&bytes) == (generation, self.dimensions as u64),
"checkpoint checksum mismatch"
);
ServingImage::new(bytes)
}
pub(super) fn encode_checkpoint(
&self,
store: &DocumentStore,
generation: &str,
) -> Result<Option<Vec<u8>>> {
if !self.checkpoint_schema()
|| !self.config.document_checkpoint.enabled
|| self.image.other_state.load(Ordering::Acquire)
{
return Ok(None);
}
let Some(image) =
documents::encode_checkpoint(store, Self::generation_crc(generation), self.dimensions)?
else {
return Ok(None);
};
self.seal_checkpoint(image).map(Some)
}
pub(super) fn takes_checkpoint(&self) -> bool {
if !self.checkpoint_schema()
|| !self.config.document_checkpoint.enabled
|| self.image.other_state.load(Ordering::Acquire)
{
return false;
}
self.shards.read().count() == 0
&& documents::checkpoint_representable(&self.documents.read())
}
pub(super) fn checkpoint_if_due(&self) -> Result<()> {
if !self.image.opened.load(Ordering::Acquire) {
return Ok(());
}
let used = self.arena.stats().used_bytes;
if used == 0 || used < self.config.document_checkpoint.min_journal_bytes {
return Ok(());
}
if !self.takes_checkpoint() {
return Ok(());
}
self.compact()
}
#[cfg(feature = "security")]
fn seal_checkpoint(&self, mut image: Vec<u8>) -> Result<Vec<u8>> {
use zeroize::Zeroize;
let Some(cipher) = &self.encryption else {
return Ok(image);
};
let mut outer = image[..SEALED_HEADER].to_vec();
outer[..8].copy_from_slice(SEALED_MAGIC);
outer[8..16].fill(0);
let sealed = cipher.encrypt_with_aad(&image, &outer);
image.zeroize();
outer.extend_from_slice(&sealed?);
Ok(outer)
}
#[cfg(not(feature = "security"))]
fn seal_checkpoint(&self, image: Vec<u8>) -> Result<Vec<u8>> {
Ok(image)
}
#[doc(hidden)]
pub fn serves_from_checkpoint(&self) -> bool {
self.image.serving.read().is_some()
}
pub(super) fn mark_opened(&self) {
self.image.opened.store(true, Ordering::Release);
}
}