use super::format::{digest, History};
use super::{
invalid, validate_identifier, CanonicalHeadV1, CanonicalStreamV1, MAX_CANONICAL_HISTORY_BYTES,
};
use super::{CanonicalBatchV1 as Batch, DurabilityReceiptV1, DurabilityTargetV1};
use crate::sync::persistence::{atomic_write, reject_symlink};
use crate::sync::{SyncMessage, SyncResult};
use appcore_distributed_contracts::ServiceLeaderLease;
use fs2::FileExt;
use std::fs::{File, OpenOptions};
use std::io::Read;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct FileCanonicalHistoryV1 {
path: PathBuf,
stream: CanonicalStreamV1,
}
pub struct CanonicalHistoryTransactionV1<'a> {
path: &'a Path,
history: History,
failed: bool,
}
impl FileCanonicalHistoryV1 {
pub fn create(root: &Path, name: &str, stream: CanonicalStreamV1) -> SyncResult<Self> {
let store = Self::location(root, name, stream)?;
let _lock = store.lock()?;
if store.path.try_exists().map_err(io_error)? {
return Err(invalid("canonical history already exists"));
}
let history = History {
format_version: 1,
stream: store.stream.clone(),
batches: Vec::new(),
};
save(&store.path, &history)?;
Ok(store)
}
pub fn open_existing(root: &Path, name: &str, stream: CanonicalStreamV1) -> SyncResult<Self> {
let store = Self::location(root, name, stream)?;
let _lock = store.lock()?;
store.load()?;
Ok(store)
}
pub fn head(&self) -> SyncResult<CanonicalHeadV1> {
let _lock = self.lock()?;
Ok(self.load()?.head())
}
pub fn durability_target(&self, required: &CanonicalHeadV1) -> SyncResult<DurabilityTargetV1> {
let _lock = self.lock()?;
target_at_head(&self.load()?, required)
}
pub fn durability_receipt(
&self,
target: &DurabilityTargetV1,
local_core_id: &str,
) -> SyncResult<DurabilityReceiptV1> {
target.validate()?;
validate_identifier(local_core_id)?;
let _lock = self.lock()?;
let observed = target_at_head(&self.load()?, &target.head)?;
if &observed != target {
return Err(invalid("durability target metadata mismatch"));
}
OpenOptions::new()
.read(true)
.write(true)
.open(&self.path)
.map_err(io_error)?
.sync_all()
.map_err(io_error)?;
crate::sync::persistence::sync_parent(self.path.parent().unwrap())?;
Ok(DurabilityReceiptV1 {
schema_version: 1,
target: observed,
persisted_by_core_id: local_core_id.into(),
})
}
pub fn append_authenticated(
&self,
lease: &ServiceLeaderLease,
message: SyncMessage,
) -> SyncResult<CanonicalHeadV1> {
let _lock = self.lock()?;
let mut transaction = CanonicalHistoryTransactionV1 {
path: &self.path,
history: self.load()?,
failed: false,
};
transaction.append_authenticated(lease, message)
}
pub fn replicate_authenticated(&self, batch: Batch) -> SyncResult<CanonicalHeadV1> {
let _lock = self.lock()?;
let mut transaction = CanonicalHistoryTransactionV1 {
path: &self.path,
history: self.load()?,
failed: false,
};
transaction.append_batch(batch)
}
pub fn batches(&self, offset: usize, limit: usize) -> SyncResult<Vec<Batch>> {
if limit == 0 || limit > 64 {
return Err(invalid("invalid canonical page limit"));
}
let _lock = self.lock()?;
let history = self.load()?;
if offset > history.batches.len() {
return Err(invalid("invalid canonical batch offset"));
}
Ok(history
.batches
.into_iter()
.skip(offset)
.take(limit)
.collect())
}
pub fn with_verified_head<T>(
&self,
required: &CanonicalHeadV1,
action: impl FnOnce(&mut CanonicalHistoryTransactionV1<'_>) -> SyncResult<T>,
) -> SyncResult<T> {
let _lock = self.lock()?;
let history = self.load()?;
if &history.head() != required {
return Err(invalid("canonical promotion head mismatch"));
}
action(&mut CanonicalHistoryTransactionV1 {
path: &self.path,
history,
failed: false,
})
}
fn location(root: &Path, name: &str, stream: CanonicalStreamV1) -> SyncResult<Self> {
stream.validate()?;
validate_identifier(name)?;
if name == "." || name == ".." {
return Err(invalid("invalid canonical file name"));
}
let root = root.canonicalize().map_err(io_error)?;
if !root.is_dir() {
return Err(invalid("canonical storage root is not a directory"));
}
Ok(Self {
path: root.join(name),
stream,
})
}
fn lock(&self) -> SyncResult<File> {
let lock = self.path.with_file_name(format!(
".{}.lock",
self.path.file_name().unwrap().to_string_lossy()
));
reject_symlink(&lock)?;
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(lock)
.map_err(io_error)?;
file.try_lock_exclusive()
.map_err(|_| invalid("canonical history busy or unavailable"))?;
Ok(file)
}
fn load(&self) -> SyncResult<History> {
reject_symlink(&self.path)?;
let mut bytes = Vec::new();
File::open(&self.path)
.map_err(io_error)?
.take((MAX_CANONICAL_HISTORY_BYTES + 1) as u64)
.read_to_end(&mut bytes)
.map_err(io_error)?;
if bytes.len() > MAX_CANONICAL_HISTORY_BYTES {
return Err(invalid("canonical file size exceeded"));
}
#[derive(serde::Deserialize)]
struct Version {
#[serde(default)]
format_version: u16,
}
let version: Version =
serde_json::from_slice(&bytes).map_err(|_| invalid("invalid canonical file"))?;
if version.format_version != 1 {
return Err(invalid("NO MORE SUPPORTED PLEASE UPDATE"));
}
let history: History =
serde_json::from_slice(&bytes).map_err(|_| invalid("invalid canonical file"))?;
history.validate(&self.stream)?;
Ok(history)
}
}
impl CanonicalHistoryTransactionV1<'_> {
pub fn append_authenticated(
&mut self,
lease: &ServiceLeaderLease,
message: SyncMessage,
) -> SyncResult<CanonicalHeadV1> {
if self.failed {
return Err(invalid(
"canonical transaction requires reopen after failure",
));
}
if CanonicalStreamV1::from_lease(lease)? != self.history.stream
|| lease.epoch == 0
|| lease.expires_at_ms <= lease.acquired_at_ms
{
return Err(invalid("canonical grant scope mismatch"));
}
if let Some(existing) = self
.history
.batches
.iter()
.find(|old| old.message.batch_id == message.batch_id)
{
return if existing.message == message
&& existing.epoch == lease.epoch
&& existing.writer == lease.holder_core_id.as_str()
{
Ok(self.history.head())
} else {
Err(invalid("divergent canonical replay"))
};
}
let mut batch = Batch {
epoch: lease.epoch,
writer: lease.holder_core_id.as_str().into(),
message,
digest: String::new(),
};
batch.digest = digest(&self.history.stream, self.history.batches.last(), &batch)?;
self.append_batch(batch)
}
fn append_batch(&mut self, batch: Batch) -> SyncResult<CanonicalHeadV1> {
if self.failed {
return Err(invalid(
"canonical transaction requires reopen after failure",
));
}
if let Some(existing) = self
.history
.batches
.iter()
.find(|old| old.message.batch_id == batch.message.batch_id)
{
return if existing == &batch {
Ok(self.history.head())
} else {
Err(invalid("divergent canonical replay"))
};
}
self.history.batches.push(batch);
let result = self
.history
.validate(&self.history.stream)
.and_then(|()| save(self.path, &self.history));
if let Err(error) = result {
self.failed = true;
self.history.batches.pop();
return Err(error);
}
Ok(self.history.head())
}
}
fn target_at_head(history: &History, required: &CanonicalHeadV1) -> SyncResult<DurabilityTargetV1> {
if required.stream != history.stream {
return Err(invalid("durability stream mismatch"));
}
let batch = history
.batches
.iter()
.find(|batch| batch.message.sequence_end == required.sequence)
.ok_or(invalid("durability target not retained"))?;
if required.batch_hash.as_deref() != Some(batch.message.events_hash.as_str())
|| required.history_hash.as_deref() != Some(batch.digest.as_str())
{
return Err(invalid("durability target head mismatch"));
}
Ok(DurabilityTargetV1 {
batch_id: batch.message.batch_id.clone(),
head: required.clone(),
writer_epoch: batch.epoch,
writer_core_id: batch.writer.clone(),
})
}
fn save(path: &Path, history: &History) -> SyncResult<()> {
let bytes = serde_json::to_vec(history).map_err(|_| invalid("canonical encoding failed"))?;
if bytes.len() > MAX_CANONICAL_HISTORY_BYTES {
return Err(invalid("canonical file size exceeded"));
}
atomic_write(path, &bytes)
}
fn io_error(error: std::io::Error) -> crate::sync::SyncError {
crate::sync::SyncError::ReplicationFailed(error.to_string())
}