1use crate::{Error, engine::Engine, model::RecordPackage};
5use bitcoin::Txid;
6use std::{
7 collections::BTreeMap,
8 path::Path,
9 sync::atomic::{AtomicBool, Ordering},
10 time::{Duration, Instant},
11};
12use urma_core::multipart::{Geometry, VerifiedRecord};
13use urma_runtime::multipart::{
14 Candidate, FetchError, MultipartSource, RecordRequest, RecoveredObject, RecoveryError,
15 RecoveryLimits,
16};
17
18pub trait RecordSource {
21 fn fetch(&self, txid: Txid) -> Result<Option<RecordPackage>, Error>;
22}
23
24#[derive(Clone, Copy)]
26pub struct FetchLimits {
27 pub sources: usize,
28 pub timeout: Duration,
29}
30impl Default for FetchLimits {
31 fn default() -> Self {
32 Self {
33 sources: 4,
34 timeout: Duration::from_secs(30),
35 }
36 }
37}
38
39fn verify_cached(
40 engine: &Engine,
41 expected: Txid,
42 package: &RecordPackage,
43) -> Result<VerifiedRecord, Error> {
44 let chain = engine.chain()?;
45 let reveal = urma_chain::decode::transaction(&package.reveal.transaction, chain)
46 .map_err(|e| Error::Invalid(e.to_string()))?;
47 let commit = urma_chain::decode::transaction(&package.commit, chain)
48 .map_err(|e| Error::Invalid(e.to_string()))?;
49 VerifiedRecord::verify(expected, &reveal, &commit).map_err(|e| Error::Invalid(e.to_string()))
50}
51
52pub fn fetch_record(
56 engine: &Engine,
57 txid: Txid,
58 sources: &[&dyn RecordSource],
59 limits: FetchLimits,
60) -> Result<Option<VerifiedRecord>, Error> {
61 let began = Instant::now();
62 let deadline = began
63 .checked_add(limits.timeout)
64 .ok_or_else(|| Error::Invalid("record deadline overflow".into()))?;
65 crate::engine::check_deadline(deadline)?;
66 if let Some(record) = engine.record(txid)? {
67 let verified = verify_cached(engine, txid, &record.package)?;
68 crate::engine::check_deadline(deadline)?;
69 return Ok(Some(verified));
70 }
71 let mut last = None;
72 for source in sources.iter().take(limits.sources) {
73 if began.elapsed() >= limits.timeout {
74 return Err(Error::Busy("record acquisition deadline".into()));
75 }
76 let package = match source.fetch(txid) {
77 Ok(Some(package)) => package,
78 Ok(None) => continue,
79 Err(error) => {
80 last = Some(error);
81 continue;
82 }
83 };
84 let late = began.elapsed() >= limits.timeout;
87 match verify_cached(engine, txid, &package) {
90 Err(error) if late => return Err(error),
91 Err(error) => {
92 last = Some(error);
93 continue;
94 }
95 Ok(verified) => {
96 match engine.admit_record_before(package, (!late).then_some(deadline)) {
97 Ok(_) => return Ok(Some(verified)),
98 Err(error) => {
99 last = Some(error);
102 }
103 }
104 }
105 }
106 }
107 if began.elapsed() >= limits.timeout {
108 return Err(Error::Busy("record acquisition deadline".into()));
109 }
110 let fallback = engine.retrieve_located_record_before(txid, deadline);
111 crate::engine::check_deadline(deadline)?;
112 match fallback {
113 Ok(Some(record)) => {
114 if began.elapsed() >= limits.timeout {
115 return Err(Error::Busy("record acquisition deadline".into()));
116 }
117 let verified = verify_cached(engine, txid, &record.package)?;
118 crate::engine::check_deadline(deadline)?;
119 Ok(Some(verified))
120 }
121 Err(error) => Err(error),
122 Ok(None) => match last {
123 Some(error) => Err(error),
124 None => Ok(None),
125 },
126 }
127}
128
129#[derive(Clone, Copy)]
134pub(crate) struct Pace {
135 limits: RecoveryLimits,
136 slice: Duration,
137 fetch: FetchLimits,
138}
139
140impl Pace {
141 fn on_demand(limits: RecoveryLimits) -> Self {
142 Self {
143 limits,
144 slice: limits.timeout,
145 fetch: FetchLimits {
146 sources: limits.attempts.max(1) as usize,
147 timeout: limits.timeout,
148 },
149 }
150 }
151
152 pub(crate) fn background(slice: Duration) -> Self {
157 Self {
158 limits: RecoveryLimits {
159 max_payload_bytes: Geometry::MAX_OBJECT_BYTES,
160 max_nodes: Geometry::MAX_NODES,
161 attempts: 1,
162 timeout: RecoveryLimits::TIMEOUT,
163 },
164 slice,
165 fetch: FetchLimits {
166 sources: usize::MAX,
167 ..FetchLimits::default()
168 },
169 }
170 }
171}
172
173pub(crate) enum Recovery {
176 Complete(RecoveredObject),
177 Sliced(RecoveryError),
178}
179
180fn completed(recovery: Recovery) -> Result<RecoveredObject, Error> {
181 match recovery {
182 Recovery::Complete(object) => Ok(object),
183 Recovery::Sliced(cause) => {
184 Err(Error::Busy(format!("object acquisition deadline: {cause}")))
185 }
186 }
187}
188
189pub fn recover(
194 engine: &Engine,
195 root: Txid,
196 sources: &[&dyn RecordSource],
197 limits: RecoveryLimits,
198 scratch: &Path,
199) -> Result<RecoveredObject, Error> {
200 completed(recover_inner(
201 engine,
202 root,
203 sources,
204 Pace::on_demand(limits),
205 scratch,
206 false,
207 &AtomicBool::new(false),
208 )?)
209}
210
211pub fn recover_and_pin(
215 engine: &Engine,
216 root: Txid,
217 sources: &[&dyn RecordSource],
218 limits: RecoveryLimits,
219 scratch: &Path,
220) -> Result<RecoveredObject, Error> {
221 completed(recover_inner(
222 engine,
223 root,
224 sources,
225 Pace::on_demand(limits),
226 scratch,
227 true,
228 &AtomicBool::new(false),
229 )?)
230}
231
232pub(crate) fn recover_inner(
233 engine: &Engine,
234 root: Txid,
235 sources: &[&dyn RecordSource],
236 pace: Pace,
237 scratch: &Path,
238 pin: bool,
239 cancelled: &AtomicBool,
240) -> Result<Recovery, Error> {
241 check_cancelled(cancelled)?;
242 let generation = engine.status()?.generation;
243 let began = Instant::now();
244 let deadline = began
245 .checked_add(pace.limits.timeout)
246 .ok_or_else(|| Error::Invalid("object deadline overflow".into()))?;
247 let root = fetch_record(engine, root, sources, pace.fetch)?
248 .ok_or_else(|| Error::Invalid("root is not available from the supplied sources".into()))?;
249 check_cancelled(cancelled)?;
250 let mut remaining = pace.limits;
251 remaining.timeout = pace.limits.timeout.saturating_sub(began.elapsed());
252 let mut adapter = Adapter {
253 engine,
254 sources,
255 pace,
256 began,
257 network: Duration::ZERO,
258 sliced: false,
259 members: BTreeMap::from([(root.txid(), crate::archive::ObjectMember::from(&root))]),
260 cancelled,
261 };
262 let object = match urma_runtime::multipart::reconstruct(&root, &mut adapter, remaining, scratch)
263 {
264 Ok(object) => object,
265 Err(cause) if adapter.sliced => return Ok(Recovery::Sliced(cause)),
266 Err(error) => return Err(Error::Source(Box::new(error))),
267 };
268 check_cancelled(cancelled)?;
269 engine.complete_object(
272 &object,
273 generation,
274 &adapter.members.into_values().collect::<Vec<_>>(),
275 pin,
276 deadline,
277 cancelled,
278 )?;
279 Ok(Recovery::Complete(object))
280}
281
282fn source_error(message: String) -> FetchError {
283 FetchError::Source(urma_runtime::error::Error::Io(std::io::Error::other(
284 message,
285 )))
286}
287
288struct Adapter<'a> {
289 engine: &'a Engine,
290 sources: &'a [&'a dyn RecordSource],
291 pace: Pace,
292 began: Instant,
293 network: Duration,
294 sliced: bool,
295 members: BTreeMap<Txid, crate::archive::ObjectMember>,
296 cancelled: &'a AtomicBool,
297}
298
299impl Adapter<'_> {
300 fn stored(&self, txid: Txid) -> Result<Option<VerifiedRecord>, Error> {
302 self.engine
303 .record(txid)?
304 .map(|record| verify_cached(self.engine, txid, &record.package))
305 .transpose()
306 }
307
308 fn fetch_missing(&mut self, txid: Txid) -> Result<VerifiedRecord, FetchError> {
309 if self.network >= self.pace.slice {
310 self.sliced = true;
311 return Err(source_error("object acquisition slice ended".into()));
312 }
313 let timeout = self.pace.fetch.timeout.min(
314 self.pace
315 .limits
316 .timeout
317 .saturating_sub(self.began.elapsed()),
318 );
319 let started = Instant::now();
320 let fetched = fetch_record(
323 self.engine,
324 txid,
325 self.sources,
326 FetchLimits {
327 timeout,
328 ..self.pace.fetch
329 },
330 );
331 self.network += started.elapsed();
332 match fetched {
333 Ok(Some(record)) => Ok(record),
334 Ok(None) => Err(FetchError::Unavailable),
335 Err(error) => Err(source_error(error.to_string())),
336 }
337 }
338}
339
340impl MultipartSource for Adapter<'_> {
341 fn fetch(
342 &mut self,
343 request: &RecordRequest,
344 _rejected: &[String],
345 ) -> Result<Candidate, FetchError> {
346 if self.cancelled.load(Ordering::Acquire) {
347 return Err(source_error("object retention cancelled".into()));
348 }
349 let txid = request.reference.txid;
350 let record = match self.stored(txid) {
351 Ok(Some(record)) => record,
352 Ok(None) => self.fetch_missing(txid)?,
353 Err(error) => return Err(source_error(error.to_string())),
354 };
355 self.members
356 .insert(record.txid(), crate::archive::ObjectMember::from(&record));
357 Ok(Candidate {
358 origin: "node-selected-view".into(),
359 record,
360 })
361 }
362}
363
364fn check_cancelled(cancelled: &AtomicBool) -> Result<(), Error> {
365 if cancelled.load(Ordering::Acquire) {
366 Err(Error::Busy("object retention cancelled".into()))
367 } else {
368 Ok(())
369 }
370}