use crate::{
model::{
application_ingress::{
ApplicationIngressError, ApplicationIngressPayload, ApplicationMutationRequest,
ApplicationObservationRequest,
},
artifacts::ArtifactChecksumRecord,
attempt_journal::{
AttemptJournalRecord, MutationOutcomeRecord, MutationReceiptRequest,
ObservationOutcomeRecord, ObservationReceiptRequest,
},
operation_plan::OperationPlanError,
},
ops::persistence::{AttemptJournalError, ExecutionStageCheckpointError, ExecutionStageGuard},
workflow::{ExecutionReservationError, execution_workflow::RetainedStageEvidence},
};
use std::future::Future;
#[derive(Debug)]
pub enum ApplicationRequest<'a> {
Mutation(ApplicationMutationRequest<'a>),
Observation(ApplicationObservationRequest<'a>),
}
impl ApplicationRequest<'_> {
#[must_use]
pub const fn payload(&self) -> &ApplicationIngressPayload {
match self {
Self::Mutation(r) => r.payload(),
Self::Observation(r) => r.payload(),
}
}
fn validate(&self, journal: &AttemptJournalRecord) -> Result<(), ApplicationIngressError> {
match self {
Self::Mutation(r) => r.validate_journal(journal),
Self::Observation(r) => r.validate_journal(journal),
}
}
fn record_qualified<E: std::error::Error + 'static>(
&self,
journal: &mut crate::ops::persistence::AttemptJournalGuard<'_>,
qualified: &ApplicationQualification,
) -> Result<(), ApplicationExecutionCause<E>> {
match self {
ApplicationRequest::Mutation(r) => {
if qualified.outcome != ObservationOutcomeRecord::Applied {
return Err(ApplicationExecutionCause::MutationOutcome);
}
super::qualified_receipt::record_applied(
journal,
MutationReceiptRequest {
attempt: r.mutation_attempt(),
request: self.payload().digest(),
outcome: MutationOutcomeRecord::Applied,
evidence: qualified.evidence.clone(),
},
r.mutation_attempt(),
self.payload().digest().hash(),
ApplicationExecutionCause::MutationOutcome,
|| Ok(()),
)?;
}
ApplicationRequest::Observation(r) => {
journal.record_observation(ObservationReceiptRequest {
attempt: r.observation_attempt(),
request: self.payload().digest(),
outcome: qualified.outcome,
evidence: qualified.evidence.clone(),
})?;
}
}
Ok(())
}
}
pub trait ApplicationReply: std::fmt::Debug + 'static {
fn bytes(&self) -> &[u8];
}
impl ApplicationReply for Vec<u8> {
fn bytes(&self) -> &[u8] {
self
}
}
#[derive(Debug)]
pub struct ApplicationQualification {
pub outcome: ObservationOutcomeRecord,
pub evidence: ArtifactChecksumRecord,
pub learned: ArtifactChecksumRecord,
}
impl ApplicationQualification {
fn checkpoint(
self,
stage: &ExecutionStageGuard<'_>,
) -> Result<
(ObservationOutcomeRecord, Option<RetainedStageEvidence>),
ExecutionStageCheckpointError,
> {
let retained = if self.outcome == ObservationOutcomeRecord::Applied {
stage.reconcile_checkpoint(self.learned.clone())?;
Some(RetainedStageEvidence::new(
stage.binding().digest(),
self.learned,
))
} else {
None
};
Ok((self.outcome, retained))
}
}
pub trait ApplicationIngressProvider {
type Error: std::error::Error + 'static;
type Reply: ApplicationReply;
fn check(&mut self, request: &ApplicationRequest<'_>) -> Result<(), Self::Error>;
fn admit(
&mut self,
request: &ApplicationRequest<'_>,
) -> impl Future<Output = Result<(), Self::Error>>;
fn submit(
&mut self,
request: &ApplicationRequest<'_>,
journal: &AttemptJournalRecord,
) -> impl Future<Output = Result<Self::Reply, Self::Error>>;
fn qualify(
&mut self,
request: &ApplicationRequest<'_>,
reply: &Self::Reply,
) -> impl Future<Output = Result<ApplicationQualification, Self::Error>>;
}
#[derive(Debug)]
pub struct ApplicationExecution<R> {
pub reply: R,
pub outcome: ObservationOutcomeRecord,
pub retained: Option<RetainedStageEvidence>,
}
#[derive(Debug, thiserror::Error)]
pub enum ApplicationExecutionCause<E: std::error::Error + 'static> {
#[error(transparent)]
Reservation(#[from] ExecutionReservationError),
#[error(transparent)]
Plan(#[from] OperationPlanError),
#[error(transparent)]
Request(#[from] ApplicationIngressError),
#[error(transparent)]
Journal(#[from] AttemptJournalError),
#[error(transparent)]
Checkpoint(#[from] ExecutionStageCheckpointError),
#[error("application ingress callback failed: {0}")]
Application(#[source] E),
#[error("application ingress requires a singleton stage")]
NonSingletonStage,
#[error("original pending mutation is absent")]
NoPendingMutation,
#[error("direct application mutation requires qualified Applied evidence")]
MutationOutcome,
#[error("application reply exceeds original ceiling")]
ReplyTooLarge,
}
#[derive(Debug, thiserror::Error)]
pub enum ApplicationExecutionError<E: std::error::Error + 'static, R: std::fmt::Debug> {
#[error(transparent)]
BeforeReply(#[from] ApplicationExecutionCause<E>),
#[error("application reply could not settle: {source}")]
AfterReply {
source: ApplicationExecutionCause<E>,
reply: Box<R>,
},
}
pub async fn execute<P: ApplicationIngressProvider>(
stage: &ExecutionStageGuard<'_>,
sequence: u64,
payload: &ApplicationIngressPayload,
provider: &mut P,
) -> Result<ApplicationExecution<P::Reply>, ApplicationExecutionError<P::Error, P::Reply>> {
run(stage, sequence, payload, provider, false).await
}
pub async fn reconcile<P: ApplicationIngressProvider>(
stage: &ExecutionStageGuard<'_>,
sequence: u64,
payload: &ApplicationIngressPayload,
provider: &mut P,
) -> Result<ApplicationExecution<P::Reply>, ApplicationExecutionError<P::Error, P::Reply>> {
run(stage, sequence, payload, provider, true).await
}
async fn run<P: ApplicationIngressProvider>(
stage: &ExecutionStageGuard<'_>,
sequence: u64,
payload: &ApplicationIngressPayload,
provider: &mut P,
observation: bool,
) -> Result<ApplicationExecution<P::Reply>, ApplicationExecutionError<P::Error, P::Reply>> {
type Cause<E> = ApplicationExecutionCause<E>;
if stage.plan().operations().len() != 1 {
return Err(Cause::NonSingletonStage.into());
}
let authority = stage
.plan()
.attempt_authority(sequence)
.map_err(Cause::from)?;
if observation {
payload.validate_observation_binding(authority.binding())
} else {
payload.validate_mutation_binding(authority.binding())
}
.map_err(Cause::from)?;
let (reply, result) = super::ic_mutation::dispatch_reserved(
stage,
&authority,
provider,
|journal| {
if observation {
let mutation = journal
.record()?
.view()
.pending_mutation
.ok_or(Cause::NoPendingMutation)?;
journal
.reserve_planned_observation(
&stage.plan().digest(),
mutation,
payload.digest().hash(),
)
.map_err(ExecutionReservationError::from)?;
} else {
journal
.reserve_planned_mutation(&stage.plan().digest())
.map_err(ExecutionReservationError::from)?;
}
Ok::<_, Cause<P::Error>>(())
},
|journal| {
Ok(if observation {
ApplicationRequest::Observation(ApplicationObservationRequest::new(
stage.plan(),
sequence,
journal,
payload,
)?)
} else {
ApplicationRequest::Mutation(ApplicationMutationRequest::new(
stage.plan(),
sequence,
journal,
payload,
)?)
})
},
async |provider, request, journal| {
provider.check(request).map_err(Cause::Application)?;
provider.admit(request).await.map_err(Cause::Application)?;
stage.layout().map_err(ExecutionReservationError::from)?;
request.validate(journal)?;
provider.check(request).map_err(Cause::Application)?;
provider
.submit(request, journal)
.await
.map_err(Cause::Application)
},
async |provider, journal, request, reply| {
stage.layout().map_err(ExecutionReservationError::from)?;
request.validate(journal.record()?)?;
provider.check(request).map_err(Cause::Application)?;
if reply.bytes().len() > payload.reply_limit() {
return Err(Cause::ReplyTooLarge);
}
let qualified = provider
.qualify(request, reply)
.await
.map_err(Cause::Application)?;
stage.layout().map_err(ExecutionReservationError::from)?;
request.validate(journal.record()?)?;
provider.check(request).map_err(Cause::Application)?;
request.record_qualified(journal, &qualified)?;
Ok(qualified)
},
)
.await?;
let result = result.and_then(|qualified| qualified.checkpoint(stage).map_err(Cause::from));
let (reply, (outcome, retained)) =
super::qualified_receipt::retain_reply(reply, result, |source, reply| {
ApplicationExecutionError::AfterReply { source, reply }
})?;
Ok(ApplicationExecution {
reply,
outcome,
retained,
})
}
#[cfg(test)]
mod tests;