appcore_update/
host_flow.rs1use crate::{
13 ActivationAdapter, ActivationEvidence, ActivationPhaseV2, ActivationReceiptV2,
14 ActivationRequest, FileRecoveryStore, RecoveryAction, RecoveryDecision, StagedArtifact,
15 UpdateError, UpdateResult,
16};
17
18pub trait UpdateRecoveryGate: Send + Sync {
22 fn drain_and_snapshot(&self, request: &ActivationRequest) -> UpdateResult<()>;
24 fn seal(&self, request: &ActivationRequest) -> UpdateResult<()>;
26}
27
28pub struct HostUpdateFlow<'a> {
32 journal: &'a FileRecoveryStore,
33 adapter: &'a dyn ActivationAdapter,
34 gate: &'a dyn UpdateRecoveryGate,
35}
36impl<'a> HostUpdateFlow<'a> {
37 pub fn new(
39 journal: &'a FileRecoveryStore,
40 adapter: &'a dyn ActivationAdapter,
41 gate: &'a dyn UpdateRecoveryGate,
42 ) -> Self {
43 Self {
44 journal,
45 adapter,
46 gate,
47 }
48 }
49 pub fn prepare(
54 &self,
55 request: &ActivationRequest,
56 staged: &StagedArtifact,
57 at_ms: u64,
58 ) -> UpdateResult<()> {
59 request.validate()?;
60 if staged.descriptor != request.descriptor {
61 return conflict();
62 }
63 validate_upgrade(request)?;
64 match self.journal.inspect_recovery()? {
65 RecoveryDecision::Idle => self.journal.prepare(ActivationReceiptV2 {
66 format_version: crate::ACTIVATION_V2_FORMAT_VERSION,
67 attempt_id: request.attempt_id.clone(),
68 phase: ActivationPhaseV2::Prepared,
69 activated: request.descriptor.clone(),
70 previous: request.previous.clone(),
71 created_at_ms: at_ms,
72 updated_at_ms: at_ms,
73 host_binding: request.host_binding.clone(),
74 failure_reason: None,
75 })?,
76 RecoveryDecision::PendingActivation { receipt }
77 | RecoveryDecision::Committed { receipt } => {
78 check_request(&receipt, request)?;
79 if receipt.phase != ActivationPhaseV2::Prepared {
80 return Ok(());
81 }
82 }
83 _ => return conflict(),
84 }
85 self.gate.drain_and_snapshot(request)?;
86 self.adapter.prepare(request, staged)
87 }
88 pub fn activate(
92 &self,
93 request: &ActivationRequest,
94 staged: &StagedArtifact,
95 at_ms: u64,
96 ) -> UpdateResult<ActivationEvidence> {
97 let receipt = self.activation_receipt()?;
98 if staged.descriptor != request.descriptor {
99 return conflict();
100 }
101 check_request(&receipt, request)?;
102 if matches!(
103 receipt.phase,
104 ActivationPhaseV2::ActivatedPendingHealth | ActivationPhaseV2::Committed
105 ) {
106 return Ok(ActivationEvidence {
107 attempt_id: receipt.attempt_id,
108 descriptor: receipt.activated,
109 host_binding: receipt.host_binding,
110 });
111 }
112 check_timestamp(&receipt, at_ms)?;
113 self.gate.drain_and_snapshot(request)?;
114 self.adapter.prepare(request, staged)?;
115 self.gate.seal(request)?;
116 let evidence = self.adapter.activate(request, staged)?;
117 check_evidence(&receipt, &evidence)?;
118 self.journal.mark_activated(&receipt.attempt_id, at_ms)?;
119 Ok(evidence)
120 }
121 pub fn confirm(&self, evidence: &ActivationEvidence, at_ms: u64) -> UpdateResult<()> {
126 let receipt = self.activation_receipt()?;
127 check_evidence(&receipt, evidence)?;
128 if receipt.phase == ActivationPhaseV2::Committed {
129 return Ok(());
130 }
131 if receipt.phase != ActivationPhaseV2::ActivatedPendingHealth {
132 return conflict();
133 }
134 check_timestamp(&receipt, at_ms)?;
135 if self.adapter.healthcheck(evidence).is_err() {
136 self.journal.replay(RecoveryAction::RequireRollback {
137 attempt_id: receipt.attempt_id,
138 reason: "host health gate failed".into(),
139 at_ms,
140 })?;
141 return Err(UpdateError::Health("host health gate failed".into()));
142 }
143 self.adapter.commit(&receipt)?;
144 self.journal.replay(RecoveryAction::ConfirmHealthy {
145 attempt_id: receipt.attempt_id,
146 activated_sha256: evidence.descriptor.sha256().into(),
147 at_ms,
148 })?;
149 Ok(())
150 }
151 pub fn rollback(&self) -> UpdateResult<()> {
154 match self.journal.inspect_recovery()? {
155 RecoveryDecision::RollbackRequired { receipt } => self.adapter.rollback(&receipt),
156 RecoveryDecision::RolledBack { .. } => Ok(()),
157 _ => conflict(),
158 }
159 }
160 fn activation_receipt(&self) -> UpdateResult<ActivationReceiptV2> {
161 match self.journal.inspect_recovery()? {
162 RecoveryDecision::PendingActivation { receipt }
163 | RecoveryDecision::Committed { receipt } => Ok(receipt),
164 _ => conflict(),
165 }
166 }
167}
168fn check_request(receipt: &ActivationReceiptV2, request: &ActivationRequest) -> UpdateResult<()> {
169 request.validate()?;
170 if receipt.attempt_id != request.attempt_id
171 || receipt.activated != request.descriptor
172 || receipt.previous != request.previous
173 || receipt.host_binding != request.host_binding
174 {
175 return conflict();
176 }
177 Ok(())
178}
179fn check_evidence(
180 receipt: &ActivationReceiptV2,
181 evidence: &ActivationEvidence,
182) -> UpdateResult<()> {
183 evidence.validate()?;
184 if receipt.attempt_id != evidence.attempt_id
185 || receipt.activated != evidence.descriptor
186 || receipt.host_binding != evidence.host_binding
187 {
188 return conflict();
189 }
190 Ok(())
191}
192fn validate_upgrade(request: &ActivationRequest) -> UpdateResult<()> {
193 if let Some(previous) = &request.previous {
194 if previous.application_id() != request.descriptor.application_id()
195 || previous.build_id() == request.descriptor.build_id()
196 || previous.target() != request.descriptor.target()
197 || semver::Version::parse(previous.application_version())
198 .map_err(|_| UpdateError::Incompatible("invalid previous version".into()))?
199 >= semver::Version::parse(request.descriptor.application_version())
200 .map_err(|_| UpdateError::Incompatible("invalid candidate version".into()))?
201 {
202 return Err(UpdateError::Incompatible(
203 "candidate must advance the same application and target".into(),
204 ));
205 }
206 }
207 Ok(())
208}
209fn conflict<T>() -> UpdateResult<T> {
210 Err(UpdateError::Recovery(
211 "host operation conflicts with activation evidence".into(),
212 ))
213}
214
215fn check_timestamp(receipt: &ActivationReceiptV2, at_ms: u64) -> UpdateResult<()> {
216 if at_ms < receipt.updated_at_ms {
217 return conflict();
218 }
219 Ok(())
220}