use std::{
collections::HashMap,
fs::{self, File, OpenOptions},
io::{self, Read, Write},
path::{Component, Path, PathBuf},
sync::{
Arc, Mutex, OnceLock, Weak,
atomic::{AtomicU64, Ordering},
},
};
use alloy_eips::{BlockId, BlockNumberOrTag, RpcBlockHash};
use alloy_primitives::{Address, B256, U256, keccak256};
use foundry_fork_db::BlockchainDb;
use revm::{database::Cache, primitives::hardfork::SpecId, state::AccountInfo};
use serde::{Deserialize, Serialize};
use super::{
BlockEnvSource, CodeSeedState, EvmCache, ImmutableDataCache, TrackedMapping, versioned,
};
const CHECKPOINT_MAGIC: &[u8; 8] = b"EFCCKPT\0";
const CHECKPOINT_VERSION: u32 = 6;
const CHECKPOINT_LABEL: &str = "durable reactive checkpoint";
const CHECKPOINT_CHECKSUM_BYTES: usize = 32;
const CHECKPOINT_HEADER_BYTES: u64 =
CHECKPOINT_MAGIC.len() as u64 + std::mem::size_of::<u32>() as u64;
const MAX_TEMP_CREATE_ATTEMPTS: usize = 128;
pub const DEFAULT_MAX_DURABLE_CHECKPOINT_BYTES: u64 = 512 * 1024 * 1024;
static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);
static CHECKPOINT_COORDINATORS: OnceLock<
Mutex<HashMap<PathBuf, Weak<CheckpointWriteCoordinator>>>,
> = OnceLock::new();
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DurableCheckpointIdentity {
pub chain_id: u64,
pub subscriber_id: String,
pub handler_set_id: String,
}
impl DurableCheckpointIdentity {
pub fn new(
chain_id: u64,
subscriber_id: impl Into<String>,
handler_set_id: impl Into<String>,
) -> Self {
Self {
chain_id,
subscriber_id: subscriber_id.into(),
handler_set_id: handler_set_id.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DurableCheckpointBlock {
pub number: u64,
pub hash: B256,
pub parent_hash: Option<B256>,
pub timestamp: Option<u64>,
}
impl DurableCheckpointBlock {
pub const fn new(number: u64, hash: B256) -> Self {
Self {
number,
hash,
parent_hash: None,
timestamp: None,
}
}
pub const fn with_parent_hash(mut self, parent_hash: B256) -> Self {
self.parent_hash = Some(parent_hash);
self
}
pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = Some(timestamp);
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DurableCheckpointMetadata {
pub identity: DurableCheckpointIdentity,
pub block: DurableCheckpointBlock,
pub delivery_token: Option<Vec<u8>>,
pub delivery_witness: Option<B256>,
pub subscriber_checkpoint: Option<Vec<u8>>,
pub runtime_checkpoint: Option<Vec<u8>>,
}
impl DurableCheckpointMetadata {
pub fn new(identity: DurableCheckpointIdentity, block: DurableCheckpointBlock) -> Self {
Self {
identity,
block,
delivery_token: None,
delivery_witness: None,
subscriber_checkpoint: None,
runtime_checkpoint: None,
}
}
pub fn with_delivery_token(mut self, delivery_token: impl Into<Vec<u8>>) -> Self {
self.delivery_token = Some(delivery_token.into());
self
}
pub fn with_delivery_witness(mut self, delivery_witness: B256) -> Self {
self.delivery_witness = Some(delivery_witness);
self
}
pub fn with_subscriber_checkpoint(mut self, checkpoint: impl Into<Vec<u8>>) -> Self {
self.subscriber_checkpoint = Some(checkpoint.into());
self
}
pub fn with_runtime_checkpoint(mut self, checkpoint: impl Into<Vec<u8>>) -> Self {
self.runtime_checkpoint = Some(checkpoint.into());
self
}
}
#[derive(Clone, Debug)]
pub struct DurableCheckpointStore {
path: PathBuf,
coordinator: Arc<CheckpointWriteCoordinator>,
max_checkpoint_bytes: u64,
}
#[derive(Debug, Default)]
struct CheckpointWriteCoordinator {
latest_generation: AtomicU64,
writer: Mutex<()>,
}
impl PartialEq for DurableCheckpointStore {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for DurableCheckpointStore {}
impl DurableCheckpointStore {
pub fn new(path: impl Into<PathBuf>) -> Self {
let path = normalized_checkpoint_path(&path.into());
Self {
coordinator: checkpoint_coordinator(&path),
path,
max_checkpoint_bytes: DEFAULT_MAX_DURABLE_CHECKPOINT_BYTES,
}
}
pub fn with_max_checkpoint_bytes(mut self, max_checkpoint_bytes: u64) -> Self {
self.max_checkpoint_bytes = max_checkpoint_bytes;
self
}
pub fn max_checkpoint_bytes(&self) -> u64 {
self.max_checkpoint_bytes
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn save(
&self,
cache: &EvmCache,
metadata: DurableCheckpointMetadata,
) -> Result<(), DurableCheckpointError> {
validate_capture_identity(cache, &metadata)?;
let generation = self.reserve_generation()?;
let snapshot = DurableCheckpointSnapshot::capture(cache, metadata);
persist_snapshot(
&self.path,
snapshot,
&self.coordinator,
generation,
self.max_checkpoint_bytes,
)
}
pub async fn save_async(
&self,
cache: &EvmCache,
metadata: DurableCheckpointMetadata,
) -> Result<(), DurableCheckpointError> {
validate_capture_identity(cache, &metadata)?;
let generation = self.reserve_generation()?;
let snapshot = DurableCheckpointSnapshot::capture(cache, metadata);
let path = self.path.clone();
let coordinator = Arc::clone(&self.coordinator);
let max_checkpoint_bytes = self.max_checkpoint_bytes;
tokio::task::spawn_blocking(move || {
persist_snapshot(
&path,
snapshot,
&coordinator,
generation,
max_checkpoint_bytes,
)
})
.await
.map_err(DurableCheckpointError::TaskJoin)?
}
fn reserve_generation(&self) -> Result<u64, DurableCheckpointError> {
self.coordinator
.latest_generation
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |generation| {
generation.checked_add(1)
})
.map(|previous| previous + 1)
.map_err(|_| DurableCheckpointError::GenerationExhausted)
}
pub fn load(&self) -> Result<Option<LoadedDurableCheckpoint>, DurableCheckpointError> {
let file = match File::open(&self.path) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(source) => {
return Err(DurableCheckpointError::Read {
path: self.path.clone(),
source,
});
}
};
let reported_bytes = file
.metadata()
.map_err(|source| DurableCheckpointError::Read {
path: self.path.clone(),
source,
})?
.len();
if reported_bytes > self.max_checkpoint_bytes {
return Err(DurableCheckpointError::CheckpointTooLarge {
path: self.path.clone(),
bytes: reported_bytes,
max_bytes: self.max_checkpoint_bytes,
});
}
let mut data = Vec::new();
file.take(self.max_checkpoint_bytes.saturating_add(1))
.read_to_end(&mut data)
.map_err(|source| DurableCheckpointError::Read {
path: self.path.clone(),
source,
})?;
if data.len() as u64 > self.max_checkpoint_bytes {
return Err(DurableCheckpointError::CheckpointTooLarge {
path: self.path.clone(),
bytes: data.len() as u64,
max_bytes: self.max_checkpoint_bytes,
});
}
let Some(checksum_start) = data.len().checked_sub(CHECKPOINT_CHECKSUM_BYTES) else {
return Err(DurableCheckpointError::InvalidFormat {
path: self.path.clone(),
});
};
let encoded = &data[..checksum_start];
let expected = B256::from_slice(&data[checksum_start..]);
let actual = keccak256(encoded);
if actual != expected {
return Err(DurableCheckpointError::ChecksumMismatch {
path: self.path.clone(),
});
}
let snapshot = versioned::decode(
encoded,
CHECKPOINT_MAGIC,
CHECKPOINT_VERSION,
CHECKPOINT_LABEL,
)
.ok_or_else(|| DurableCheckpointError::InvalidFormat {
path: self.path.clone(),
})?;
Ok(Some(LoadedDurableCheckpoint { snapshot }))
}
}
fn checkpoint_coordinator(path: &Path) -> Arc<CheckpointWriteCoordinator> {
let key = normalized_checkpoint_path(path);
let coordinators = CHECKPOINT_COORDINATORS.get_or_init(|| Mutex::new(HashMap::new()));
let mut coordinators = coordinators
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(coordinator) = coordinators.get(&key).and_then(Weak::upgrade) {
return coordinator;
}
coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
let coordinator = Arc::new(CheckpointWriteCoordinator::default());
coordinators.insert(key, Arc::downgrade(&coordinator));
coordinator
}
fn normalized_checkpoint_path(path: &Path) -> PathBuf {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.map(|directory| directory.join(path))
.unwrap_or_else(|_| path.to_path_buf())
};
let Some(file_name) = absolute.file_name() else {
return normalize_existing_path_prefix(&absolute);
};
let parent = absolute.parent().unwrap_or_else(|| Path::new("."));
normalize_existing_path_prefix(parent).join(file_name)
}
fn normalize_existing_path_prefix(absolute: &Path) -> PathBuf {
let components: Vec<_> = absolute.components().collect();
for split in (1..=components.len()).rev() {
let prefix: PathBuf = components[..split]
.iter()
.map(|component| component.as_os_str())
.collect();
let Ok(mut resolved) = prefix.canonicalize() else {
continue;
};
for component in &components[split..] {
match component {
Component::Prefix(prefix) => resolved.push(prefix.as_os_str()),
Component::RootDir => resolved.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
let _ = resolved.pop();
}
Component::Normal(part) => resolved.push(part),
}
}
return resolved;
}
absolute.to_path_buf()
}
fn validate_capture_identity(
cache: &EvmCache,
metadata: &DurableCheckpointMetadata,
) -> Result<(), DurableCheckpointError> {
if metadata.identity.chain_id != cache.chain_id {
return Err(DurableCheckpointError::CacheChainMismatch {
cache_chain_id: cache.chain_id,
checkpoint_chain_id: metadata.identity.chain_id,
});
}
Ok(())
}
fn persist_snapshot(
path: &Path,
snapshot: DurableCheckpointSnapshot,
coordinator: &CheckpointWriteCoordinator,
generation: u64,
max_checkpoint_bytes: u64,
) -> Result<(), DurableCheckpointError> {
let _writer = coordinator
.writer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let latest = coordinator.latest_generation.load(Ordering::Acquire);
if generation != latest {
return Err(DurableCheckpointError::WriteSuperseded { generation, latest });
}
let payload_bytes = bincode::serialized_size(&snapshot).map_err(|source| {
DurableCheckpointError::Encode(crate::errors::PersistenceError::serialize(
CHECKPOINT_LABEL,
source,
))
})?;
let encoded_bytes = payload_bytes
.checked_add(CHECKPOINT_HEADER_BYTES)
.and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES as u64))
.ok_or(DurableCheckpointError::CheckpointSizeOverflow {
path: path.to_path_buf(),
})?;
if encoded_bytes > max_checkpoint_bytes {
return Err(DurableCheckpointError::CheckpointTooLarge {
path: path.to_path_buf(),
bytes: encoded_bytes,
max_bytes: max_checkpoint_bytes,
});
}
let mut data = versioned::encode(
CHECKPOINT_MAGIC,
CHECKPOINT_VERSION,
&snapshot,
CHECKPOINT_LABEL,
)
.map_err(DurableCheckpointError::Encode)?;
let checksum = keccak256(&data);
data.extend_from_slice(checksum.as_slice());
debug_assert_eq!(data.len() as u64, encoded_bytes);
atomic_replace(path, &data)
}
pub struct LoadedDurableCheckpoint {
snapshot: DurableCheckpointSnapshot,
}
impl LoadedDurableCheckpoint {
pub fn metadata(&self) -> &DurableCheckpointMetadata {
&self.snapshot.metadata
}
pub fn restore_into(
self,
cache: &mut EvmCache,
expected: &DurableCheckpointIdentity,
) -> Result<DurableCheckpointMetadata, DurableCheckpointError> {
if &self.snapshot.metadata.identity != expected {
return Err(DurableCheckpointError::IdentityMismatch {
expected: expected.clone(),
actual: self.snapshot.metadata.identity.clone(),
});
}
if cache.chain_id != expected.chain_id {
return Err(DurableCheckpointError::CacheChainMismatch {
cache_chain_id: cache.chain_id,
checkpoint_chain_id: expected.chain_id,
});
}
Ok(self.snapshot.restore(cache))
}
}
#[derive(Serialize, Deserialize)]
struct DurableCheckpointSnapshot {
metadata: DurableCheckpointMetadata,
state: EvmCacheStateSnapshot,
}
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct EvmCacheStateSnapshot {
backend_accounts: Vec<(Address, AccountInfo)>,
backend_storage: Vec<(Address, Vec<(U256, U256)>)>,
backend_block_hashes: Vec<(U256, B256)>,
overlay: Cache,
token_decimals: HashMap<Address, u8>,
immutable_cache: ImmutableDataCache,
code_seeds: HashMap<Address, CodeSeedState>,
erc20_balance_slots: HashMap<Address, TrackedMapping>,
block: PersistedBlockId,
block_number: Option<u64>,
basefee: Option<u64>,
coinbase: Option<Address>,
prevrandao: Option<B256>,
block_gas_limit: Option<u64>,
timestamp_override: Option<u64>,
block_env_source: Option<BlockEnvSource>,
spec_id: SpecId,
snapshot_generation: u64,
}
#[derive(Clone, Copy, Serialize, Deserialize)]
enum PersistedBlockId {
Hash {
hash: B256,
require_canonical: Option<bool>,
},
Latest,
Finalized,
Safe,
Earliest,
Pending,
Number(u64),
}
impl From<BlockId> for PersistedBlockId {
fn from(block: BlockId) -> Self {
match block {
BlockId::Hash(hash) => Self::Hash {
hash: hash.block_hash,
require_canonical: hash.require_canonical,
},
BlockId::Number(BlockNumberOrTag::Latest) => Self::Latest,
BlockId::Number(BlockNumberOrTag::Finalized) => Self::Finalized,
BlockId::Number(BlockNumberOrTag::Safe) => Self::Safe,
BlockId::Number(BlockNumberOrTag::Earliest) => Self::Earliest,
BlockId::Number(BlockNumberOrTag::Pending) => Self::Pending,
BlockId::Number(BlockNumberOrTag::Number(number)) => Self::Number(number),
}
}
}
impl From<PersistedBlockId> for BlockId {
fn from(block: PersistedBlockId) -> Self {
match block {
PersistedBlockId::Hash {
hash,
require_canonical,
} => BlockId::Hash(RpcBlockHash::from_hash(hash, require_canonical)),
PersistedBlockId::Latest => BlockId::latest(),
PersistedBlockId::Finalized => BlockId::finalized(),
PersistedBlockId::Safe => BlockId::safe(),
PersistedBlockId::Earliest => BlockId::earliest(),
PersistedBlockId::Pending => BlockId::pending(),
PersistedBlockId::Number(number) => BlockId::number(number),
}
}
}
impl DurableCheckpointSnapshot {
fn capture(cache: &EvmCache, metadata: DurableCheckpointMetadata) -> Self {
let mut state = EvmCacheStateSnapshot::capture(cache);
state.align_to_checkpoint_block(&metadata.block);
Self { metadata, state }
}
fn restore(self, cache: &mut EvmCache) -> DurableCheckpointMetadata {
let block_hash = self.metadata.block.hash;
self.state.restore(cache);
let block = alloy_eips::BlockId::from((block_hash, Some(true)));
cache.block = block;
let _ = cache.backend.set_pinned_block(block);
self.metadata
}
}
impl EvmCacheStateSnapshot {
pub(crate) fn capture(cache: &EvmCache) -> Self {
let (backend_accounts, backend_storage, backend_block_hashes) =
capture_backend_maps(&cache.blockchain_db);
Self {
backend_accounts,
backend_storage,
backend_block_hashes,
overlay: cache.db.cache.clone(),
token_decimals: cache.token_decimals.clone(),
immutable_cache: cache.immutable_cache.clone(),
code_seeds: cache.code_seeds.clone(),
erc20_balance_slots: cache.erc20_balance_slots.clone(),
block: cache.block.into(),
block_number: cache.block_number,
basefee: cache.basefee,
coinbase: cache.coinbase,
prevrandao: cache.prevrandao,
block_gas_limit: cache.block_gas_limit,
timestamp_override: cache.timestamp_override,
block_env_source: cache.block_env_source,
spec_id: cache.spec_id,
snapshot_generation: cache.snapshot_generation,
}
}
fn align_to_checkpoint_block(&mut self, block: &DurableCheckpointBlock) {
let preserve_full_env = matches!(
self.block_env_source,
Some(BlockEnvSource::VerifiedHash { number, hash })
if number == block.number
&& hash == block.hash
&& block
.timestamp
.zip(self.timestamp_override)
.is_none_or(|(expected, actual)| expected == actual)
);
self.block = PersistedBlockId::Hash {
hash: block.hash,
require_canonical: Some(true),
};
self.block_number = Some(block.number);
if !preserve_full_env {
self.timestamp_override = block.timestamp;
self.basefee = None;
self.coinbase = None;
self.prevrandao = None;
self.block_gas_limit = None;
self.block_env_source = None;
}
}
pub(crate) fn restore(self, cache: &mut EvmCache) {
{
let mut accounts = cache.blockchain_db.accounts().write();
accounts.clear();
accounts.extend(self.backend_accounts);
}
{
let mut storage = cache.blockchain_db.storage().write();
storage.clear();
storage.extend(
self.backend_storage
.into_iter()
.map(|(address, slots)| (address, slots.into_iter().collect())),
);
}
{
let mut hashes = cache.blockchain_db.block_hashes().write();
hashes.clear();
hashes.extend(self.backend_block_hashes);
}
cache.db.cache = self.overlay;
cache.token_decimals = self.token_decimals;
cache.immutable_cache = self.immutable_cache;
cache.code_seeds = self.code_seeds;
cache.erc20_balance_slots = self.erc20_balance_slots;
let block = BlockId::from(self.block);
cache.block = block;
let _ = cache.backend.set_pinned_block(block);
cache.block_number = self.block_number;
cache.basefee = self.basefee;
cache.coinbase = self.coinbase;
cache.prevrandao = self.prevrandao;
cache.block_gas_limit = self.block_gas_limit;
cache.timestamp_override = self.timestamp_override;
cache.block_env_source = self.block_env_source;
cache.spec_id = self.spec_id;
cache.snapshot_generation = self.snapshot_generation;
cache.base = None;
cache.base_dirty.clear();
cache.base_full_rebuild = true;
cache.base_storage_lens.clear();
}
}
type BackendMapsSnapshot = (
Vec<(Address, AccountInfo)>,
Vec<(Address, Vec<(U256, U256)>)>,
Vec<(U256, B256)>,
);
fn capture_backend_maps(blockchain_db: &BlockchainDb) -> BackendMapsSnapshot {
let accounts = blockchain_db.accounts().read();
let storage = blockchain_db.storage().read();
let block_hashes = blockchain_db.block_hashes().read();
let backend_accounts = accounts
.iter()
.map(|(address, info)| (*address, info.clone()))
.collect();
let backend_storage = storage
.iter()
.map(|(address, slots)| {
(
*address,
slots.iter().map(|(key, value)| (*key, *value)).collect(),
)
})
.collect();
let backend_block_hashes = block_hashes
.iter()
.map(|(number, hash)| (*number, *hash))
.collect();
(backend_accounts, backend_storage, backend_block_hashes)
}
#[cfg(test)]
mod tests {
use std::{
fs,
sync::{Arc, Barrier},
thread,
time::{Duration, Instant},
};
use foundry_fork_db::{BlockchainDb, cache::BlockchainDbMeta};
use super::capture_backend_maps;
#[cfg(unix)]
use super::create_unique_temp_file;
#[test]
fn backend_capture_retains_earlier_guards_while_waiting_for_later_maps() {
let blockchain_db = Arc::new(BlockchainDb::new(BlockchainDbMeta::default(), None));
let storage_guard = blockchain_db.storage().write();
let start = Arc::new(Barrier::new(2));
let worker_db = Arc::clone(&blockchain_db);
let worker_start = Arc::clone(&start);
let capture = thread::spawn(move || {
worker_start.wait();
capture_backend_maps(&worker_db)
});
start.wait();
let deadline = Instant::now() + Duration::from_secs(2);
let retained_accounts_guard = loop {
if blockchain_db.accounts().try_write().is_none() {
break true;
}
if Instant::now() >= deadline {
break false;
}
thread::yield_now();
};
assert!(
retained_accounts_guard,
"capture must retain the accounts guard while awaiting storage"
);
drop(storage_guard);
capture.join().expect("capture thread");
}
#[cfg(unix)]
#[test]
fn stale_temp_candidate_is_skipped_without_blocking_checkpoint_progress() {
use std::os::unix::fs::PermissionsExt;
let root = std::env::temp_dir().join(format!(
"evm-fork-cache-stale-temp-{}-{}",
std::process::id(),
super::NEXT_TEMP_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
fs::create_dir_all(&root).expect("create test directory");
let destination = root.join("checkpoint.bin");
let stale = root.join(".checkpoint.bin.first");
let fresh = root.join(".checkpoint.bin.second");
fs::write(&stale, b"stale crash residue").expect("precreate first candidate");
let mut candidates = [stale.clone(), fresh.clone()].into_iter();
let (selected, file) = create_unique_temp_file(&destination, || {
candidates.next().expect("bounded test candidates")
})
.expect("collision must retry with the next candidate");
drop(file);
assert_eq!(selected, fresh);
assert_eq!(
fs::metadata(&selected)
.expect("fresh temp metadata")
.permissions()
.mode()
& 0o777,
0o600,
"checkpoint temp files contain provider cursors and must be owner-only"
);
assert_eq!(
fs::read(&stale).expect("stale file remains"),
b"stale crash residue"
);
fs::remove_dir_all(root).expect("remove test directory");
}
}
#[cfg(unix)]
fn atomic_replace(path: &Path, data: &[u8]) -> Result<(), DurableCheckpointError> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(parent).map_err(|source| DurableCheckpointError::CreateDir {
path: parent.to_path_buf(),
source,
})?;
let (temp_path, mut file) = create_unique_temp_file(path, || next_temp_path(path))?;
let result = (|| {
file.write_all(data)
.and_then(|()| file.sync_all())
.map_err(|source| DurableCheckpointError::Write {
path: temp_path.clone(),
source,
})?;
fs::rename(&temp_path, path).map_err(|source| DurableCheckpointError::Rename {
from: temp_path.clone(),
to: path.to_path_buf(),
source,
})?;
sync_parent_directory(parent)?;
Ok(())
})();
if result.is_err() {
let _ = fs::remove_file(&temp_path);
}
result
}
#[cfg(not(unix))]
fn atomic_replace(path: &Path, _data: &[u8]) -> Result<(), DurableCheckpointError> {
Err(DurableCheckpointError::AtomicReplaceUnsupported {
path: path.to_path_buf(),
})
}
#[cfg(unix)]
fn create_unique_temp_file(
destination: &Path,
mut next_candidate: impl FnMut() -> PathBuf,
) -> Result<(PathBuf, File), DurableCheckpointError> {
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
for _ in 0..MAX_TEMP_CREATE_ATTEMPTS {
let candidate = next_candidate();
match OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&candidate)
{
Ok(file) => {
if let Err(source) = file.set_permissions(fs::Permissions::from_mode(0o600)) {
drop(file);
let _ = fs::remove_file(&candidate);
return Err(DurableCheckpointError::Write {
path: candidate,
source,
});
}
return Ok((candidate, file));
}
Err(source) if source.kind() == io::ErrorKind::AlreadyExists => continue,
Err(source) => {
return Err(DurableCheckpointError::Write {
path: candidate,
source,
});
}
}
}
Err(DurableCheckpointError::TemporaryPathExhausted {
path: destination.to_path_buf(),
attempts: MAX_TEMP_CREATE_ATTEMPTS,
})
}
#[cfg(unix)]
fn sync_parent_directory(parent: &Path) -> Result<(), DurableCheckpointError> {
File::open(parent)
.and_then(|directory| directory.sync_all())
.map_err(|source| DurableCheckpointError::SyncDirectory {
path: parent.to_path_buf(),
source,
})
}
#[cfg(not(unix))]
fn sync_parent_directory(_parent: &Path) -> Result<(), DurableCheckpointError> {
Ok(())
}
#[cfg(unix)]
fn next_temp_path(path: &Path) -> PathBuf {
let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
let name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("checkpoint");
path.with_file_name(format!(".{name}.tmp-{}-{id}", std::process::id()))
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DurableCheckpointError {
#[error("atomic durable checkpoint replacement for {path:?} is unsupported on this platform")]
AtomicReplaceUnsupported {
path: PathBuf,
},
#[error(transparent)]
Encode(#[from] crate::errors::PersistenceError),
#[error("durable checkpoint writer task failed: {0}")]
TaskJoin(#[source] tokio::task::JoinError),
#[error("durable checkpoint writer generation exhausted")]
GenerationExhausted,
#[error(
"durable checkpoint write generation {generation} was superseded by generation {latest}"
)]
WriteSuperseded {
generation: u64,
latest: u64,
},
#[error("failed to read durable checkpoint {path:?}: {source}")]
Read {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("durable checkpoint {path:?} has an invalid or unsupported format")]
InvalidFormat {
path: PathBuf,
},
#[error("durable checkpoint {path:?} failed its integrity checksum")]
ChecksumMismatch {
path: PathBuf,
},
#[error(
"durable checkpoint {path:?} is {bytes} bytes, exceeding the configured {max_bytes}-byte limit"
)]
CheckpointTooLarge {
path: PathBuf,
bytes: u64,
max_bytes: u64,
},
#[error("durable checkpoint {path:?} size exceeds supported accounting")]
CheckpointSizeOverflow {
path: PathBuf,
},
#[error("failed to create durable checkpoint directory {path:?}: {source}")]
CreateDir {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to write durable checkpoint {path:?}: {source}")]
Write {
path: PathBuf,
#[source]
source: io::Error,
},
#[error(
"failed to allocate a unique temporary file for durable checkpoint {path:?} after {attempts} attempts"
)]
TemporaryPathExhausted {
path: PathBuf,
attempts: usize,
},
#[error("failed to replace durable checkpoint {to:?} from {from:?}: {source}")]
Rename {
from: PathBuf,
to: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to sync durable checkpoint directory {path:?}: {source}")]
SyncDirectory {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("durable checkpoint identity mismatch: expected {expected:?}, found {actual:?}")]
IdentityMismatch {
expected: DurableCheckpointIdentity,
actual: DurableCheckpointIdentity,
},
#[error(
"durable checkpoint chain {checkpoint_chain_id} does not match cache chain {cache_chain_id}"
)]
CacheChainMismatch {
cache_chain_id: u64,
checkpoint_chain_id: u64,
},
}