use crate::{
ActivationAdapter, ActivationEvidence, ActivationPhaseV2, ActivationReceiptV2,
ActivationRequest, FileRecoveryStore, RecoveryAction, RecoveryDecision, StagedArtifact,
UpdateError, UpdateResult,
};
pub trait UpdateRecoveryGate: Send + Sync {
fn drain_and_snapshot(&self, request: &ActivationRequest) -> UpdateResult<()>;
fn seal(&self, request: &ActivationRequest) -> UpdateResult<()>;
}
pub struct HostUpdateFlow<'a> {
journal: &'a FileRecoveryStore,
adapter: &'a dyn ActivationAdapter,
gate: &'a dyn UpdateRecoveryGate,
}
impl<'a> HostUpdateFlow<'a> {
pub fn new(
journal: &'a FileRecoveryStore,
adapter: &'a dyn ActivationAdapter,
gate: &'a dyn UpdateRecoveryGate,
) -> Self {
Self {
journal,
adapter,
gate,
}
}
pub fn prepare(
&self,
request: &ActivationRequest,
staged: &StagedArtifact,
at_ms: u64,
) -> UpdateResult<()> {
request.validate()?;
if staged.descriptor != request.descriptor {
return conflict();
}
validate_upgrade(request)?;
match self.journal.inspect_recovery()? {
RecoveryDecision::Idle => self.journal.prepare(ActivationReceiptV2 {
format_version: crate::ACTIVATION_V2_FORMAT_VERSION,
attempt_id: request.attempt_id.clone(),
phase: ActivationPhaseV2::Prepared,
activated: request.descriptor.clone(),
previous: request.previous.clone(),
created_at_ms: at_ms,
updated_at_ms: at_ms,
host_binding: request.host_binding.clone(),
failure_reason: None,
})?,
RecoveryDecision::PendingActivation { receipt }
| RecoveryDecision::Committed { receipt } => {
check_request(&receipt, request)?;
if receipt.phase != ActivationPhaseV2::Prepared {
return Ok(());
}
}
_ => return conflict(),
}
self.gate.drain_and_snapshot(request)?;
self.adapter.prepare(request, staged)
}
pub fn activate(
&self,
request: &ActivationRequest,
staged: &StagedArtifact,
at_ms: u64,
) -> UpdateResult<ActivationEvidence> {
let receipt = self.activation_receipt()?;
if staged.descriptor != request.descriptor {
return conflict();
}
check_request(&receipt, request)?;
if matches!(
receipt.phase,
ActivationPhaseV2::ActivatedPendingHealth | ActivationPhaseV2::Committed
) {
return Ok(ActivationEvidence {
attempt_id: receipt.attempt_id,
descriptor: receipt.activated,
host_binding: receipt.host_binding,
});
}
check_timestamp(&receipt, at_ms)?;
self.gate.drain_and_snapshot(request)?;
self.adapter.prepare(request, staged)?;
self.gate.seal(request)?;
let evidence = self.adapter.activate(request, staged)?;
check_evidence(&receipt, &evidence)?;
self.journal.mark_activated(&receipt.attempt_id, at_ms)?;
Ok(evidence)
}
pub fn confirm(&self, evidence: &ActivationEvidence, at_ms: u64) -> UpdateResult<()> {
let receipt = self.activation_receipt()?;
check_evidence(&receipt, evidence)?;
if receipt.phase == ActivationPhaseV2::Committed {
return Ok(());
}
if receipt.phase != ActivationPhaseV2::ActivatedPendingHealth {
return conflict();
}
check_timestamp(&receipt, at_ms)?;
if self.adapter.healthcheck(evidence).is_err() {
self.journal.replay(RecoveryAction::RequireRollback {
attempt_id: receipt.attempt_id,
reason: "host health gate failed".into(),
at_ms,
})?;
return Err(UpdateError::Health("host health gate failed".into()));
}
self.adapter.commit(&receipt)?;
self.journal.replay(RecoveryAction::ConfirmHealthy {
attempt_id: receipt.attempt_id,
activated_sha256: evidence.descriptor.sha256().into(),
at_ms,
})?;
Ok(())
}
pub fn rollback(&self) -> UpdateResult<()> {
match self.journal.inspect_recovery()? {
RecoveryDecision::RollbackRequired { receipt } => self.adapter.rollback(&receipt),
RecoveryDecision::RolledBack { .. } => Ok(()),
_ => conflict(),
}
}
fn activation_receipt(&self) -> UpdateResult<ActivationReceiptV2> {
match self.journal.inspect_recovery()? {
RecoveryDecision::PendingActivation { receipt }
| RecoveryDecision::Committed { receipt } => Ok(receipt),
_ => conflict(),
}
}
}
fn check_request(receipt: &ActivationReceiptV2, request: &ActivationRequest) -> UpdateResult<()> {
request.validate()?;
if receipt.attempt_id != request.attempt_id
|| receipt.activated != request.descriptor
|| receipt.previous != request.previous
|| receipt.host_binding != request.host_binding
{
return conflict();
}
Ok(())
}
fn check_evidence(
receipt: &ActivationReceiptV2,
evidence: &ActivationEvidence,
) -> UpdateResult<()> {
evidence.validate()?;
if receipt.attempt_id != evidence.attempt_id
|| receipt.activated != evidence.descriptor
|| receipt.host_binding != evidence.host_binding
{
return conflict();
}
Ok(())
}
fn validate_upgrade(request: &ActivationRequest) -> UpdateResult<()> {
if let Some(previous) = &request.previous {
if previous.application_id() != request.descriptor.application_id()
|| previous.build_id() == request.descriptor.build_id()
|| previous.target() != request.descriptor.target()
|| semver::Version::parse(previous.application_version())
.map_err(|_| UpdateError::Incompatible("invalid previous version".into()))?
>= semver::Version::parse(request.descriptor.application_version())
.map_err(|_| UpdateError::Incompatible("invalid candidate version".into()))?
{
return Err(UpdateError::Incompatible(
"candidate must advance the same application and target".into(),
));
}
}
Ok(())
}
fn conflict<T>() -> UpdateResult<T> {
Err(UpdateError::Recovery(
"host operation conflicts with activation evidence".into(),
))
}
fn check_timestamp(receipt: &ActivationReceiptV2, at_ms: u64) -> UpdateResult<()> {
if at_ms < receipt.updated_at_ms {
return conflict();
}
Ok(())
}