1use crate::{
7 db::{
8 CompareProofAndAdvanceError, DbSession, ExhaustiveReadError, ReadSetRevisionProof,
9 ResumableJobAdvance, ResumableJobAdvanceReceipt, ResumableJobAdvanceRequest,
10 ResumableJobError, ResumableJobId, ResumableJobRecord, ResumableJobState,
11 ResumableJobStatus,
12 integrity::with_resumable_progress_store,
13 resource::budget::{ExecutionBudgetExceeded, HardExecutionContext},
14 },
15 traits::CanisterKind,
16};
17use icydb_diagnostic_code::{
18 DiagnosticExecutionBudgetResource, DiagnosticExecutionBudgetScope, DiagnosticExecutionLane,
19};
20
21const RESUMABLE_JOB_START_SHAPE: u64 = 0x7265_7375_6d65_0101;
22const RESUMABLE_JOB_LOAD_SHAPE: u64 = 0x7265_7375_6d65_0102;
23const RESUMABLE_JOB_ADVANCE_SHAPE: u64 = 0x7265_7375_6d65_0103;
24const RESUMABLE_JOB_ACKNOWLEDGE_SHAPE: u64 = 0x7265_7375_6d65_0104;
25
26impl<C: CanisterKind> DbSession<C> {
27 pub fn start_resumable_job(
30 &self,
31 job_id: ResumableJobId,
32 proof: ReadSetRevisionProof,
33 initial_application_state: Vec<u8>,
34 ) -> Result<ResumableJobState, ResumableJobError> {
35 self.charge_resumable_operation(
36 DiagnosticExecutionLane::Mutation,
37 RESUMABLE_JOB_START_SHAPE,
38 )?;
39 self.verify_durable_read_set_revision_proof(&proof)
40 .map_err(map_exhaustive_error)?;
41 let record = ResumableJobRecord::new(job_id, proof, initial_application_state)?;
42 with_resumable_progress_store::<C, _>(|store| store.insert_resumable(&record))?;
43 Ok(record.state().clone())
44 }
45
46 pub fn resumable_job_state(
48 &self,
49 job_id: ResumableJobId,
50 ) -> Result<ResumableJobState, ResumableJobError> {
51 self.charge_resumable_operation(
52 DiagnosticExecutionLane::TrustedRead,
53 RESUMABLE_JOB_LOAD_SHAPE,
54 )?;
55 with_resumable_progress_store::<C, _>(|store| store.load_resumable(job_id))
56 .map(|record| record.state().clone())
57 }
58
59 pub fn acknowledge_resumable_job(
67 &self,
68 job_id: ResumableJobId,
69 expected_sequence: u64,
70 ) -> Result<(), ResumableJobError> {
71 self.charge_resumable_operation(
72 DiagnosticExecutionLane::Mutation,
73 RESUMABLE_JOB_ACKNOWLEDGE_SHAPE,
74 )?;
75 self.db
76 .ensure_recovered_control_state()
77 .map_err(|_| ResumableJobError::Internal)?;
78 with_resumable_progress_store::<C, _>(|store| {
79 let record = match store.load_resumable(job_id) {
80 Ok(record) => record,
81 Err(ResumableJobError::NotFound) => return Ok(()),
82 Err(error) => return Err(error),
83 };
84 if record.state().sequence != expected_sequence {
85 return Err(ResumableJobError::StaleSequence {
86 expected: expected_sequence,
87 actual: record.state().sequence,
88 });
89 }
90 let terminal = record.state().sequence > 0
91 && matches!(
92 record.state().status,
93 ResumableJobStatus::Completed | ResumableJobStatus::Invalidated
94 );
95 if !terminal {
96 return Err(ResumableJobError::NotTerminal);
97 }
98 store.remove_resumable(job_id)
99 })
100 }
101
102 pub fn compare_proof_and_advance<E>(
120 &self,
121 request: &ResumableJobAdvanceRequest,
122 operation: impl FnOnce(&ResumableJobState) -> Result<ResumableJobAdvance, E>,
123 ) -> Result<ResumableJobAdvanceReceipt, CompareProofAndAdvanceError<E>> {
124 self.charge_resumable_operation(
125 DiagnosticExecutionLane::Mutation,
126 RESUMABLE_JOB_ADVANCE_SHAPE,
127 )?;
128 request.idempotency_key.validate()?;
129 if request.job_id.to_bytes() == [0; 32] {
130 return Err(ResumableJobError::InvalidJobId.into());
131 }
132
133 let record =
134 with_resumable_progress_store::<C, _>(|store| store.load_resumable(request.job_id))?;
135 if let Some(receipt) = exact_replay(&record, request) {
136 return Ok(receipt.clone());
137 }
138 ensure_request_can_advance(&record, request)?;
139
140 if let Err(error) = self.verify_durable_read_set_revision_proof(&record.state().proof) {
141 return Self::persist_or_report_pre_page_invalidation(record, request, error);
142 }
143
144 let advance = operation(record.state()).map_err(CompareProofAndAdvanceError::Operation)?;
145 advance.validate()?;
146
147 if let Err(error) = self.verify_durable_read_set_revision_proof(&record.state().proof) {
148 return Self::persist_or_report_post_page_invalidation(record, request, error);
149 }
150
151 let (candidate, receipt) = record.apply_advance(request, advance)?;
152 Self::replace_resumable_job_if_current(request, &candidate)?;
153 Ok(receipt)
154 }
155
156 fn persist_or_report_pre_page_invalidation<E>(
157 record: ResumableJobRecord,
158 request: &ResumableJobAdvanceRequest,
159 error: ExhaustiveReadError,
160 ) -> Result<ResumableJobAdvanceReceipt, CompareProofAndAdvanceError<E>> {
161 Self::persist_or_report_invalidation(record, request, error)
162 }
163
164 fn persist_or_report_post_page_invalidation<E>(
165 record: ResumableJobRecord,
166 request: &ResumableJobAdvanceRequest,
167 error: ExhaustiveReadError,
168 ) -> Result<ResumableJobAdvanceReceipt, CompareProofAndAdvanceError<E>> {
169 Self::persist_or_report_invalidation(record, request, error)
170 }
171
172 fn persist_or_report_invalidation<E>(
173 record: ResumableJobRecord,
174 request: &ResumableJobAdvanceRequest,
175 error: ExhaustiveReadError,
176 ) -> Result<ResumableJobAdvanceReceipt, CompareProofAndAdvanceError<E>> {
177 match error {
178 ExhaustiveReadError::Revision(error) if error.is_source_change() => {
179 let (invalidated, receipt) = record.invalidate(request)?;
180 Self::replace_resumable_job_if_current(request, &invalidated)?;
181 Ok(receipt)
182 }
183 other => Err(map_exhaustive_error(other).into()),
184 }
185 }
186
187 fn replace_resumable_job_if_current<E>(
188 request: &ResumableJobAdvanceRequest,
189 candidate: &ResumableJobRecord,
190 ) -> Result<(), CompareProofAndAdvanceError<E>> {
191 with_resumable_progress_store::<C, _>(|store| {
192 let current = store.load_resumable(request.job_id)?;
193 if exact_replay(¤t, request).is_some() {
194 return Err(ResumableJobError::StaleSequence {
195 expected: request.expected_sequence,
196 actual: current.state().sequence,
197 });
198 }
199 ensure_request_can_advance(¤t, request)?;
200 store.replace_resumable(candidate)
201 })?;
202 Ok(())
203 }
204
205 fn charge_resumable_operation(
206 &self,
207 lane: DiagnosticExecutionLane,
208 shape: u64,
209 ) -> Result<(), ResumableJobError> {
210 self.db
211 .request_execution_scope()
212 .charge(
213 HardExecutionContext::new(DiagnosticExecutionBudgetScope::Execution, lane, shape),
214 DiagnosticExecutionBudgetResource::QueryExecutions,
215 1,
216 )
217 .map_err(resumable_execution_budget_error)
218 }
219}
220
221fn exact_replay<'a>(
222 record: &'a ResumableJobRecord,
223 request: &ResumableJobAdvanceRequest,
224) -> Option<&'a ResumableJobAdvanceReceipt> {
225 record.last_receipt().filter(|receipt| {
226 receipt.request_sequence == request.expected_sequence
227 && receipt.idempotency_key() == &request.idempotency_key
228 })
229}
230
231fn ensure_request_can_advance(
232 record: &ResumableJobRecord,
233 request: &ResumableJobAdvanceRequest,
234) -> Result<(), ResumableJobError> {
235 if record.state().status == ResumableJobStatus::Invalidated {
236 return Err(ResumableJobError::Invalidated);
237 }
238 if record.state().status == ResumableJobStatus::Completed {
239 return Err(ResumableJobError::Completed);
240 }
241 if record.state().sequence != request.expected_sequence {
242 return Err(ResumableJobError::StaleSequence {
243 expected: request.expected_sequence,
244 actual: record.state().sequence,
245 });
246 }
247 Ok(())
248}
249
250fn map_exhaustive_error(error: ExhaustiveReadError) -> ResumableJobError {
251 match error {
252 ExhaustiveReadError::Revision(error) => ResumableJobError::SourceProof(error),
253 ExhaustiveReadError::Query(_) => ResumableJobError::Internal,
254 }
255}
256
257const fn resumable_execution_budget_error(error: ExecutionBudgetExceeded) -> ResumableJobError {
258 ResumableJobError::ExecutionBudgetExceeded {
259 resource: error.resource().raw(),
260 limit: error.limit(),
261 observed: error.observed(),
262 scope: error.scope().raw(),
263 lane: error.lane().raw(),
264 normalized_shape_fingerprint_prefix: error.normalized_shape_fingerprint_prefix(),
265 }
266}