use crate::{
model::{
artifacts::ArtifactChecksumRecord,
attempt_journal::MutationReceiptRequest,
ic_snapshot_download::{IcSnapshotDownloadPlan, IcSnapshotDownloadPlanningError},
ic_snapshot_transfer_read::{
IcSnapshotTransferReadError, IcSnapshotTransferReadPayload,
IcSnapshotTransferReadRequest, IcSnapshotTransferReadResponse,
},
},
ops::persistence::{
AttemptJournalError, AttemptJournalGuard, ExecutionProgressPersistenceError,
ExecutionStageGuard, ExecutionWorkflowPersistenceError, IcSnapshotArtifactError,
IcSnapshotArtifactWriter, read_execution_progress,
},
policy::ic_snapshot_transfer_read::{
IcSnapshotTransferReadAssociationError, IcSnapshotTransferReadReply,
IcSnapshotTransferReadView,
},
ports::ic_snapshot_transfer_read::IcSnapshotTransferReadProvider,
workflow::ic_snapshot_transfer_read::{
IcSnapshotTransferReadExecutionError, read_snapshot_with,
},
};
use thiserror::Error;
pub async fn download_snapshot<E: std::error::Error + 'static>(
stage: &ExecutionStageGuard<'_>,
download: &IcSnapshotDownloadPlan<'_, '_>,
mut writer: IcSnapshotArtifactWriter<'_, '_, '_>,
provider: &mut impl IcSnapshotTransferReadProvider,
mut admit: impl AsyncFnMut(&IcSnapshotTransferReadRequest<'_, '_>) -> Result<(), E>,
mut qualify: impl AsyncFnMut(
&IcSnapshotTransferReadRequest<'_, '_>,
&IcSnapshotTransferReadResponse,
) -> Result<MutationReceiptRequest, E>,
) -> Result<ArtifactChecksumRecord, IcSnapshotDownloadExecutionError<E>> {
download.validate_binding(stage.binding())?;
let plan = download
.plan()
.ok_or(IcSnapshotDownloadPlanningError::NoDataReads)?;
if stage.plan() != plan
|| download
.requests()
.iter()
.any(|request| request.metadata().digest() != writer.coverage().metadata().digest())
|| writer.coverage().covered_region_bytes() != [0; 3]
|| writer.coverage().covered_chunks() != 0
{
return Err(IcSnapshotDownloadExecutionError::OriginalMismatch);
}
writer.validate_transfer_origin(stage.layout()?.root(), plan.digest().hash())?;
let progress = read_execution_progress(stage.layout()?, &plan.digest())?;
if progress.attempts.mutations_used != 0 || progress.attempts.observations_used != 0 {
return Err(IcSnapshotDownloadExecutionError::AlreadyAttempted);
}
for (index, payload) in download.requests().iter().enumerate() {
let sequence =
u64::try_from(index).map_err(|_| IcSnapshotDownloadPlanningError::CountOverflow)?;
let (response, result) = read_snapshot_with(
stage,
sequence,
IcSnapshotTransferReadPayload::Data(payload),
provider,
&mut admit,
async |journal, request, admitted| {
append_qualified(stage, journal, request, admitted, writer, &mut qualify).await
},
)
.await?;
writer = result.map_err(|source| IcSnapshotDownloadExecutionError::AfterReply {
operation_sequence: sequence,
source,
response: Box::new(response),
})?;
}
let progress = read_execution_progress(stage.layout()?, &plan.digest())?;
if progress.applied_operations != download.requests().len() {
return Err(IcSnapshotDownloadExecutionError::AlreadyAttempted);
}
writer.validate_transfer_origin(stage.layout()?.root(), plan.digest().hash())?;
let checksum = writer.finish()?;
if let Err(source) = stage.layout() {
return Err(IcSnapshotDownloadExecutionError::AfterPublication { source, checksum });
}
Ok(checksum)
}
async fn append_qualified<'journal, 'layout, 'metadata, E: std::error::Error + 'static>(
stage: &ExecutionStageGuard<'_>,
journal: &mut AttemptJournalGuard<'_>,
request: &IcSnapshotTransferReadRequest<'_, '_>,
admitted: &IcSnapshotTransferReadView<'_, '_>,
writer: IcSnapshotArtifactWriter<'journal, 'layout, 'metadata>,
qualify: &mut impl AsyncFnMut(
&IcSnapshotTransferReadRequest<'_, '_>,
&IcSnapshotTransferReadResponse,
) -> Result<MutationReceiptRequest, E>,
) -> Result<IcSnapshotArtifactWriter<'journal, 'layout, 'metadata>, IcSnapshotDownloadReplyError<E>>
{
let plan = stage.plan();
let writer = super::qualified_receipt::AppliedReceiptRecorder {
journal,
expected_attempt: request.mutation_attempt(),
expected_request: request.payload().digest().hash(),
}
.record_applied(
request,
admitted.response(),
async |request, reply| qualify(request, reply).await,
IcSnapshotDownloadReplyError::Qualification,
|| IcSnapshotDownloadReplyError::ReceiptRequired,
|| {
writer.validate_transfer_origin(stage.layout()?.root(), plan.digest().hash())?;
let IcSnapshotTransferReadReply::Data(reply) = admitted.reply() else {
return Err(IcSnapshotDownloadReplyError::ReceiptRequired);
};
Ok(writer.append(reply)?)
},
)
.await?;
stage.layout()?;
Ok(writer)
}
#[derive(Debug, Error)]
pub enum IcSnapshotDownloadExecutionError<E: std::error::Error + 'static> {
#[error("snapshot download originals differ")]
OriginalMismatch,
#[error("snapshot download original data stage was already attempted")]
AlreadyAttempted,
#[error(transparent)]
Planning(#[from] IcSnapshotDownloadPlanningError),
#[error(transparent)]
Stage(#[from] ExecutionWorkflowPersistenceError),
#[error(transparent)]
Progress(#[from] ExecutionProgressPersistenceError),
#[error(transparent)]
Read(#[from] IcSnapshotTransferReadExecutionError<E>),
#[error(transparent)]
Artifact(#[from] IcSnapshotArtifactError),
#[error("snapshot download reply rejected for operation {operation_sequence}: {source}")]
AfterReply {
operation_sequence: u64,
source: IcSnapshotDownloadReplyError<E>,
response: Box<IcSnapshotTransferReadResponse>,
},
#[error("snapshot download stage changed after publication: {source}")]
AfterPublication {
source: ExecutionWorkflowPersistenceError,
checksum: ArtifactChecksumRecord,
},
}
#[derive(Debug, Error)]
pub enum IcSnapshotDownloadReplyError<E: std::error::Error + 'static> {
#[error("snapshot download reply qualification failed: {0}")]
Qualification(#[source] E),
#[error("snapshot download requires an explicit original Applied receipt")]
ReceiptRequired,
#[error(transparent)]
Stage(#[from] ExecutionWorkflowPersistenceError),
#[error(transparent)]
Journal(#[from] AttemptJournalError),
#[error(transparent)]
Request(#[from] IcSnapshotTransferReadError),
#[error(transparent)]
Association(#[from] IcSnapshotTransferReadAssociationError),
#[error(transparent)]
Artifact(#[from] IcSnapshotArtifactError),
}
#[cfg(test)]
mod tests;