use super::*;
use eredu_core::intervention::{
AdmittedInterventionPlan, InterventionDiscovery, InterventionEstimator, InterventionPlan,
};
use std::sync::Arc;
#[derive(Clone)]
pub struct CaptureCheckpoint {
owner: Arc<()>,
artifact_identity: String,
plan: AdmittedCapturePlan,
intervention: Option<AdmittedInterventionPlan>,
prediction: u64,
phase: CapturePhase,
has_step: bool,
usage: CaptureUsage,
}
pub struct CaptureForkRequest<'a> {
pub discovery: &'a CaptureDiscovery,
pub max_predictions: u64,
pub limits: CaptureLimits,
pub intervention: Option<InterventionForkRequest<'a>>,
}
pub struct InterventionForkRequest<'a> {
pub discovery: &'a InterventionDiscovery,
pub session_id: &'a str,
pub replacement: Option<InterventionPlan>,
pub estimator: Arc<dyn InterventionEstimator>,
}
pub struct PreparedCaptureRestore<'a> {
run: &'a mut CaptureSession,
prediction: u64,
phase: CapturePhase,
has_step: bool,
}
impl PreparedCaptureRestore<'_> {
pub fn commit(self) {
self.run.prediction = self.prediction;
self.run.phase = self.phase;
self.run.has_step = self.has_step;
self.run.capture_seconds = 0.0;
self.run.checkpoint_ready = true;
}
}
impl CaptureSession {
pub fn checkpoint_storage_bytes(&self, discovery: &CaptureDiscovery) -> Option<u64> {
checkpoint_storage_bytes(
&self.plan,
self.interventions.as_ref().map(|run| &run.plan),
&discovery.artifact_identity,
)
}
pub fn checkpoint(
&self,
discovery: &CaptureDiscovery,
) -> Result<CaptureCheckpoint, CaptureError> {
if !self.checkpoint_ready
|| self.records.is_some()
|| self
.interventions
.as_ref()
.is_some_and(|run| run.records.is_some() || run.routing_pending.is_some())
{
return Err(CaptureError::Invalid(
"capture checkpoint requires a successful, drained record boundary".into(),
));
}
let checked = self.plan.plan().clone().admit(
&discovery.catalog,
&discovery.support,
&discovery.support.capture,
self.plan.request(),
)?;
if checked.identity() != self.plan.identity()
|| self
.interventions
.as_ref()
.is_some_and(|run| run.plan.artifact_identity() != discovery.artifact_identity)
{
return Err(CaptureError::Invalid(
"checkpoint source/admission mismatch".into(),
));
}
Ok(CaptureCheckpoint {
owner: Arc::clone(&self.owner),
artifact_identity: discovery.artifact_identity.clone(),
plan: self.plan.clone(),
intervention: self.interventions.as_ref().map(|run| run.plan.clone()),
prediction: self.prediction,
phase: self.phase,
has_step: self.has_step,
usage: self.ledger.total(),
})
}
pub fn validate_restore(&self, checkpoint: &CaptureCheckpoint) -> Result<(), CaptureError> {
if !Arc::ptr_eq(&self.owner, &checkpoint.owner)
|| self.plan.identity() != checkpoint.plan.identity()
|| self.interventions.as_ref().map(|run| run.plan.identity())
!= checkpoint.intervention.as_ref().map(|plan| plan.identity())
{
return Err(CaptureError::Invalid(
"capture checkpoint belongs to another run".into(),
));
}
if self.records.is_some()
|| self
.interventions
.as_ref()
.is_some_and(|run| run.records.is_some() || run.routing_pending.is_some())
{
return Err(CaptureError::Invalid(
"restore requires drained records and resolved routing".into(),
));
}
Ok(())
}
pub fn restore(&mut self, checkpoint: &CaptureCheckpoint) -> Result<(), CaptureError> {
self.prepare_restore(checkpoint)?.commit();
Ok(())
}
pub fn prepare_restore(
&mut self,
checkpoint: &CaptureCheckpoint,
) -> Result<PreparedCaptureRestore<'_>, CaptureError> {
self.validate_restore(checkpoint)?;
Ok(PreparedCaptureRestore {
run: self,
prediction: checkpoint.prediction,
phase: checkpoint.phase,
has_step: checkpoint.has_step,
})
}
pub fn cumulative_usage(&self) -> CaptureUsage {
self.ledger.total()
}
}
impl CaptureCheckpoint {
pub fn logical_storage_bytes(&self) -> Option<u64> {
checkpoint_storage_bytes(
&self.plan,
self.intervention.as_ref(),
&self.artifact_identity,
)
}
pub fn fork_storage_bytes(&self, request: &CaptureForkRequest<'_>) -> Option<u64> {
use crate::execution_control::storage::heap_bytes;
let mut bytes = self
.logical_storage_bytes()?
.checked_add(u64::try_from(std::mem::size_of::<CaptureSession>()).ok()?)?;
for selection in &self.plan.plan().selections {
let point = request
.discovery
.catalog
.points
.iter()
.find(|point| point.path == selection.path)?;
bytes = bytes
.checked_add(u64::try_from(std::mem::size_of_val(point)).ok()?)?
.checked_add(heap_bytes(point)?)?;
}
if let Some(child) = &request.intervention {
let plan = child
.replacement
.as_ref()
.or_else(|| self.intervention.as_ref().map(|plan| plan.plan()))?;
bytes = bytes
.checked_add(u64::try_from(std::mem::size_of::<AdmittedInterventionPlan>()).ok()?)?
.checked_add(heap_bytes(plan)?)?
.checked_add(u64::try_from(child.discovery.artifact_identity.len()).ok()?)?
.checked_add(u64::try_from(child.session_id.len()).ok()?)?
.checked_add(64)?;
for operation in &plan.operations {
let point = child
.discovery
.points
.iter()
.find(|point| point.path == operation.target)?;
bytes = bytes
.checked_add(u64::try_from(std::mem::size_of_val(point)).ok()?)?
.checked_add(heap_bytes(point)?)?;
}
}
Some(bytes)
}
pub fn next_prediction(&self) -> u64 {
if self.has_step {
self.prediction + 1
} else {
0
}
}
pub fn inherited_usage(&self) -> CaptureUsage {
self.usage
}
pub fn artifact_identity(&self) -> &str {
&self.artifact_identity
}
pub fn intervention_plan(&self) -> Option<&AdmittedInterventionPlan> {
self.intervention.as_ref()
}
pub fn fork(
&self,
request: CaptureForkRequest<'_>,
estimate: impl FnMut(
&[u64],
&CaptureSelection,
&ResolvedCaptureSlice,
) -> Result<CaptureUsage, CaptureError>,
) -> Result<CaptureSession, CaptureError> {
if request.discovery.artifact_identity != self.artifact_identity {
return Err(CaptureError::Invalid(
"child prepared source differs from checkpoint".into(),
));
}
let mut geometry = self.plan.request();
geometry.max_predictions = request.max_predictions;
let mut plan = self.plan.plan().clone();
plan.limits = request.limits;
let plan = plan.admit(
&request.discovery.catalog,
&request.discovery.support,
&request.discovery.support.capture,
geometry,
)?;
super::validate_continuation(
&plan,
request.discovery,
self.next_prediction(),
self.usage,
estimate,
)?;
let intervention = match request.intervention {
Some(child) => {
if child.session_id.is_empty()
|| self
.intervention
.as_ref()
.is_some_and(|parent| parent.session_id() == child.session_id)
|| child.discovery.artifact_identity != self.artifact_identity
{
return Err(CaptureError::Invalid(
"child intervention identity/source mismatch".into(),
));
}
let operations = child
.replacement
.or_else(|| self.intervention.as_ref().map(|p| p.plan().clone()))
.ok_or_else(|| {
CaptureError::Invalid("child intervention plan is absent".into())
})?;
let admitted = operations.admit(child.discovery, geometry, child.session_id)?;
crate::intervention::validate_continuation(
&plan,
&admitted,
child.discovery,
child.estimator.as_ref(),
self.next_prediction(),
self.usage,
)?;
Some((admitted, child.estimator))
}
None if self.intervention.is_some() => {
return Err(CaptureError::Invalid(
"inherited interventions require child discovery and re-admission".into(),
))
}
None => None,
};
let mut child = CaptureSession::new(plan);
if let Some((plan, estimator)) = intervention {
child.enable_interventions(plan, estimator)?;
}
child.ledger = CaptureLedger::with_inherited_usage(&child.plan, self.usage)?;
child.prediction = self.prediction;
child.phase = self.phase;
child.has_step = self.has_step;
Ok(child)
}
}
fn checkpoint_storage_bytes(
plan: &AdmittedCapturePlan,
intervention: Option<&AdmittedInterventionPlan>,
artifact: &str,
) -> Option<u64> {
use crate::execution_control::storage::heap_bytes;
let mut total = u64::try_from(std::mem::size_of::<CaptureCheckpoint>())
.ok()?
.checked_add(u64::try_from(artifact.len()).ok()?)?
.checked_add(heap_bytes(plan.plan())?)?
.checked_add(heap_bytes(plan.points())?)?
.checked_add(u64::try_from(plan.identity().len()).ok()?)?;
if let Some(plan) = intervention {
total = total
.checked_add(heap_bytes(plan.plan())?)?
.checked_add(heap_bytes(plan.points())?)?
.checked_add(u64::try_from(plan.identity().len()).ok()?)?
.checked_add(u64::try_from(plan.artifact_identity().len()).ok()?)?
.checked_add(u64::try_from(plan.session_id().len()).ok()?)?;
}
Some(total)
}