1use crate::{
2 Error,
3 archive::{Archive, InventoryRows},
4 model::*,
5 source::{ChainSource, CommitSource},
6};
7use std::{
8 path::Path,
9 sync::{
10 Arc, Mutex, MutexGuard,
11 atomic::{AtomicBool, Ordering},
12 },
13 time::{Duration, Instant},
14};
15
16pub struct Engine {
20 index_instance: String,
21 archive: Mutex<Archive>,
22 pub(crate) source: Option<Arc<dyn ChainSource>>,
23 synchronizing: Mutex<()>,
24 retaining: Mutex<Option<bitcoin::Txid>>,
25 projecting_names: Mutex<Option<bitcoin::Txid>>,
26 resolving_dependencies: Mutex<Option<bitcoin::Txid>>,
27}
28
29impl Engine {
30 pub fn with_source(mut self, source: Arc<dyn ChainSource>) -> Result<Self, Error> {
33 if source.genesis()? != self.status()?.chain_genesis {
34 return Err(Error::Invalid("source belongs to another chain".into()));
35 }
36 self.source = Some(source);
37 Ok(self)
38 }
39
40 pub fn open(
41 path: &Path,
42 config: ArchiveConfig,
43 source: Option<Arc<dyn ChainSource>>,
44 ) -> Result<Self, Error> {
45 if let Some(source) = &source {
46 let genesis = config
47 .chain
48 .genesis()
49 .map_err(|e| Error::Invalid(e.to_string()))?
50 .0;
51 if source.genesis()? != genesis {
52 return Err(Error::Invalid("source belongs to another chain".into()));
53 }
54 }
55 Ok(Self {
56 index_instance: hex::encode(rand::random::<[u8; 16]>()),
57 archive: Mutex::new(Archive::open(path, config)?),
58 source,
59 synchronizing: Mutex::new(()),
60 retaining: Mutex::new(None),
61 projecting_names: Mutex::new(None),
62 resolving_dependencies: Mutex::new(None),
63 })
64 }
65
66 pub(crate) fn archive(&self) -> Result<MutexGuard<'_, Archive>, Error> {
67 self.archive
68 .lock()
69 .map_err(|_| Error::Invalid("archive coordinator panicked".into()))
70 }
71
72 pub fn status(&self) -> Result<ArchiveStatus, Error> {
73 self.archive()?.status()
74 }
75
76 pub fn record(&self, txid: bitcoin::Txid) -> Result<Option<ArchivedRecord>, Error> {
77 self.archive()?.record(txid)
78 }
79
80 pub(crate) fn raw_transaction(&self, txid: bitcoin::Txid) -> Result<Option<Vec<u8>>, Error> {
84 self.archive()?.transaction(txid)
85 }
86
87 pub(crate) fn inventory_page(
89 &self,
90 from: u64,
91 to: u64,
92 after: Option<(u64, u32)>,
93 limit: u32,
94 ) -> Result<InventoryRows, Error> {
95 self.archive()?.inventory_page(from, to, after, limit)
96 }
97
98 pub fn wire_page(
103 &self,
104 cursor: Option<&crate::projection::WireCursor>,
105 limit: u32,
106 ) -> Result<crate::projection::WirePage, Error> {
107 self.archive()?
108 .wire_page(&self.index_instance, cursor, limit)
109 }
110
111 pub fn object_status(&self, root: bitcoin::Txid) -> Result<Option<ObjectStatus>, Error> {
112 self.archive()?.object_status(root)
113 }
114
115 pub fn register_names(
118 &self,
119 genesis: bitcoin::Txid,
120 block: ChainPoint,
121 ) -> Result<crate::names::NamesStatus, Error> {
122 self.archive()?.register_names(genesis, block)
123 }
124
125 pub fn names_step(&self, genesis: bitcoin::Txid) -> Result<crate::names::NamesStep, Error> {
129 self.archive()?.names_step(genesis)
130 }
131
132 pub fn names_view(
136 &self,
137 genesis: bitcoin::Txid,
138 cursor: Option<&crate::names::NamesCursor>,
139 limit: u32,
140 ) -> Result<crate::names::NamesPage, Error> {
141 self.archive()?
142 .names_view(&self.index_instance, genesis, cursor, limit)
143 }
144
145 pub fn names_status(
148 &self,
149 genesis: bitcoin::Txid,
150 ) -> Result<Option<crate::names::NamesStatus>, Error> {
151 self.archive()?.names_status(genesis)
152 }
153
154 pub fn names_resolve(
155 &self,
156 genesis: bitcoin::Txid,
157 name: &urma_names::name::Name,
158 ) -> Result<crate::names::NamesResolution, Error> {
159 self.archive()?.names_resolve(genesis, name)
160 }
161
162 pub fn maintain_next_names(
170 &self,
171 cancelled: &AtomicBool,
172 ) -> Result<Option<crate::names::NamesStep>, Error> {
173 let check_cancelled = || {
174 if cancelled.load(Ordering::Acquire) {
175 Err(Error::Busy("Names maintenance cancelled".into()))
176 } else {
177 Ok(())
178 }
179 };
180 check_cancelled()?;
181 let mut cursor = self
182 .projecting_names
183 .try_lock()
184 .map_err(|_| Error::Busy("Names maintenance already running".into()))?;
185 let mut registries = self.archive()?.names_page(*cursor)?;
186 if registries.is_empty() && cursor.is_some() {
187 *cursor = None;
188 registries = self.archive()?.names_page(None)?;
189 }
190 for genesis in registries {
191 check_cancelled()?;
192 *cursor = Some(genesis);
193 match self.names_status(genesis) {
194 Ok(Some(status)) if status.complete_to_tip => continue,
195 Ok(None) => continue,
196 Ok(Some(_)) | Err(Error::Stale) => (),
199 Err(error) => return Err(error),
200 }
201 let mut archive = self.archive()?;
202 check_cancelled()?;
204 return archive.names_step(genesis).map(Some);
205 }
206 Ok(None)
207 }
208
209 pub fn set_pin(&self, root: bitcoin::Txid, pin: bool) -> Result<(), Error> {
213 self.archive()?.set_pin(root, pin)
214 }
215
216 pub fn prune_cache(&self) -> Result<u64, Error> {
219 self.archive()?.prune_cache()
220 }
221
222 pub fn maintain_cache(
229 &self,
230 limit_bytes: u64,
231 cancelled: &AtomicBool,
232 ) -> Result<CacheReceipt, Error> {
233 if limit_bytes > i64::MAX as u64 {
234 return Err(Error::Invalid("cache limit exceeds storage range".into()));
235 }
236 let check = || {
237 if cancelled.load(Ordering::Acquire) {
238 Err(Error::Busy("cache maintenance cancelled".into()))
239 } else {
240 Ok(())
241 }
242 };
243 check()?;
244 let mut archive = self.archive()?;
245 check()?;
246 archive.maintain_cache(limit_bytes)
247 }
248
249 pub fn retain_next_object(
255 &self,
256 sources: &[&dyn crate::retrieval::RecordSource],
257 limits: urma_runtime::multipart::RecoveryLimits,
258 scratch: &Path,
259 cancelled: &AtomicBool,
260 ) -> Result<Option<ObjectStatus>, Error> {
261 if cancelled.load(Ordering::Acquire) {
262 return Err(Error::Busy("object retention cancelled".into()));
263 }
264 let mut cursor = self
265 .retaining
266 .try_lock()
267 .map_err(|_| Error::Busy("object retention already running".into()))?;
268 let mut roots = self.archive()?.root_page(*cursor)?;
269 if roots.is_empty() && cursor.is_some() {
270 *cursor = None;
271 roots = self.archive()?.root_page(None)?;
272 }
273 for root in roots {
274 if cancelled.load(Ordering::Acquire) {
275 return Err(Error::Busy("object retention cancelled".into()));
276 }
277 *cursor = Some(root);
278 if self
279 .object_status(root)?
280 .is_some_and(|status| status.selected_complete())
281 {
282 continue;
283 }
284 let _object = crate::retrieval::recover_inner(
285 self, root, sources, limits, scratch, false, cancelled,
286 )?;
287 return self.object_status(root);
288 }
289 Ok(None)
290 }
291
292 pub(crate) fn complete_object(
293 &self,
294 object: &urma_runtime::multipart::RecoveredObject,
295 generation: u64,
296 members: &[crate::archive::ObjectMember],
297 pin: bool,
298 deadline: Instant,
299 cancelled: &AtomicBool,
300 ) -> Result<(), Error> {
301 self.archive()?.complete_object(
302 object.root(),
303 generation,
304 object.manifest().length,
305 object.manifest().payload_hash,
306 members,
307 pin,
308 deadline,
309 cancelled,
310 )
311 }
312
313 pub(crate) fn chain(&self) -> Result<urma_chain::observation::Chain, Error> {
314 Ok(self.archive()?.config().chain)
315 }
316
317 pub fn retrieve_located_record(
322 &self,
323 txid: bitcoin::Txid,
324 ) -> Result<Option<ArchivedRecord>, Error> {
325 self.retrieve_located_record_before(txid, Instant::now() + Duration::from_secs(30))
326 }
327
328 pub(crate) fn retrieve_located_record_before(
329 &self,
330 txid: bitcoin::Txid,
331 deadline: Instant,
332 ) -> Result<Option<ArchivedRecord>, Error> {
333 check_deadline(deadline)?;
334 let cached = self.record(txid)?;
335 check_deadline(deadline)?;
336 if cached.is_some() {
337 return Ok(cached);
338 }
339 let (location, chain) = {
340 let archive = self.archive()?;
341 (archive.location(txid)?, archive.config().chain)
342 };
343 let (Some(location), Some(source)) = (location, &self.source) else {
344 return Ok(None);
345 };
346 check_deadline(deadline)?;
347 let raw = source.block(location.height);
348 check_deadline(deadline)?;
349 let raw = raw?;
350 let decoded =
351 urma_chain::decode::esplora_block_with_transactions(&raw, chain, location.hash)
352 .map_err(|e| Error::Invalid(format!("located block: {e}")))?;
353 let (block, raw) = decoded.into_parts();
354 let Some(position) = block.txdata.iter().position(|t| t.compute_txid() == txid) else {
355 return Err(Error::Invalid(
356 "verified block contradicts stored locator".into(),
357 ));
358 };
359 let reveal = &block.txdata[position];
360 if reveal.input.len() != 1 {
361 return Err(Error::Invalid("located transaction is not a reveal".into()));
362 }
363 let parent = reveal.input[0].previous_output.txid;
364 let commit =
365 if let Some(index) = block.txdata.iter().position(|t| t.compute_txid() == parent) {
366 Some(raw[index].clone())
367 } else if let Some(raw) = self.archive()?.transaction(parent)? {
368 Some(raw)
369 } else {
370 let parent_location = self.archive()?.location(parent)?;
371 match parent_location {
372 None => None,
373 Some(parent_location) => {
374 check_deadline(deadline)?;
375 let bytes = source.block(parent_location.height);
376 check_deadline(deadline)?;
377 let bytes = bytes?;
378 let parent_block = urma_chain::decode::esplora_block_with_transactions(
379 &bytes,
380 chain,
381 parent_location.hash,
382 )
383 .map_err(|e| Error::Invalid(format!("located parent block: {e}")))?;
384 parent_block
385 .block()
386 .txdata
387 .iter()
388 .position(|t| t.compute_txid() == parent)
389 .map(|i| parent_block.raw_transactions()[i].clone())
390 }
391 }
392 };
393 check_deadline(deadline)?;
394 let Some(commit) = commit else {
395 return Ok(None);
396 };
397 let proof = urma_chain::inclusion::BlockProofBuilder::new(&block)
398 .and_then(|builder| {
399 builder.proof(position as u32, raw[position].clone(), raw[0].clone())
400 })
401 .map_err(|e| Error::Invalid(e.to_string()))?;
402 check_deadline(deadline)?;
403 let header = source.header(location.height);
404 check_deadline(deadline)?;
405 if header?.block_hash() != location.hash {
406 return Err(Error::Stale);
407 }
408 self.admit_record_before(
409 RecordPackage {
410 version: API_VERSION,
411 chain_genesis: chain
412 .genesis()
413 .map_err(|e| Error::Invalid(e.to_string()))?
414 .0,
415 block: location,
416 reveal: proof,
417 commit,
418 },
419 Some(deadline),
420 )?;
421 let record = self.record(txid)?;
422 check_deadline(deadline)?;
423 Ok(record)
424 }
425
426 pub fn resolve_commit(&self, raw: &[u8]) -> Result<usize, Error> {
431 self.resolve_commit_inner(raw, None, &AtomicBool::new(false))
432 }
433
434 pub fn resolve_next_dependency(
448 &self,
449 source: &dyn CommitSource,
450 cancelled: &AtomicBool,
451 ) -> Result<usize, Error> {
452 check_dependency_cancelled(cancelled)?;
453 let mut cursor = self
454 .resolving_dependencies
455 .try_lock()
456 .map_err(|_| Error::Busy("dependency acquisition already running".into()))?;
457 check_dependency_cancelled(cancelled)?;
458 let chain = source.chain(); let selection = {
460 let archive = self.archive()?;
461 check_dependency_cancelled(cancelled)?;
462 if chain
463 .genesis()
464 .map_err(|error| Error::Invalid(error.to_string()))?
465 != archive
466 .config()
467 .chain
468 .genesis()
469 .map_err(|error| Error::Invalid(error.to_string()))?
470 {
471 return Err(Error::Invalid(
472 "commit source belongs to another chain".into(),
473 ));
474 }
475 let mut selection = archive.next_missing_dependency(*cursor)?;
476 if selection.is_none() && cursor.is_some() {
477 selection = archive.next_missing_dependency(None)?;
478 }
479 selection
480 };
481 check_dependency_cancelled(cancelled)?;
482 let Some((requested, generation)) = selection else {
483 *cursor = None;
484 return Ok(0);
485 };
486 *cursor = Some(requested);
487 let fetched = source.fetch(requested, cancelled);
488 check_dependency_cancelled(cancelled)?;
489 {
490 let archive = self.archive()?;
491 check_dependency_cancelled(cancelled)?;
492 archive.check_generation(generation)?;
493 }
494 let Some(raw) = fetched? else {
495 return Ok(0);
496 };
497 self.resolve_commit_inner(&raw, Some((requested, generation)), cancelled)
498 }
499
500 fn resolve_commit_inner(
501 &self,
502 raw: &[u8],
503 expected: Option<(bitcoin::Txid, u64)>,
504 cancelled: &AtomicBool,
505 ) -> Result<usize, Error> {
506 check_dependency_cancelled(cancelled)?;
507 let (chain, retention) = {
508 let archive = self.archive()?;
509 check_dependency_cancelled(cancelled)?;
510 if let Some((_, generation)) = expected {
511 archive.check_generation(generation)?;
512 }
513 (archive.config().chain, archive.config().retention)
514 };
515 let commit = urma_chain::decode::transaction(raw, chain)
518 .map_err(|e| Error::Invalid(format!("dependency commit: {e}")))?;
519 if expected.is_some_and(|(id, _)| id != commit.compute_txid()) {
520 return Err(Error::Invalid("commit source returned another TXID".into()));
521 }
522 check_dependency_cancelled(cancelled)?;
523 let pending = {
524 let archive = self.archive()?;
525 check_dependency_cancelled(cancelled)?;
526 if let Some((_, generation)) = expected {
527 archive.check_generation(generation)?;
528 }
529 archive.pending_for(commit.compute_txid())?
530 };
531 let mut completed = Vec::new();
532 for (ticket, position, proof) in pending {
533 check_dependency_cancelled(cancelled)?;
534 let header = {
535 let archive = self.archive()?;
536 check_dependency_cancelled(cancelled)?;
537 if let Some((_, generation)) = expected {
538 archive.check_generation(generation)?;
539 }
540 archive
541 .header_at(ticket.point().height)?
542 .ok_or(Error::Stale)?
543 };
544 if header.block_hash() != ticket.point().hash {
545 return Err(Error::Stale);
546 }
547 let included = proof
548 .verify(chain, &header)
549 .map_err(|e| Error::Invalid(format!("stored dependency proof: {e}")))?;
550 if included.position() != position {
551 return Err(Error::Invalid("stored dependency position mismatch".into()));
552 }
553 check_dependency_cancelled(cancelled)?;
554 let reveal = included.transaction();
555 let verified = urma_core::multipart::VerifiedRecord::verify(
559 reveal.compute_txid(),
560 reveal,
561 &commit,
562 );
563 let record = match verified {
564 Ok(verified) => {
565 let kind = urma_core::format::RecordKind::parse(verified.record_bytes())
566 .map_err(|e| Error::Invalid(e.to_string()))?;
567 if retention == Retention::Pruned
568 && matches!(
569 kind,
570 urma_core::format::RecordKind::DataPart
571 | urma_core::format::RecordKind::LeafManifest
572 )
573 {
574 None
575 } else {
576 Some(crate::scan::Record {
577 position,
578 wtxid: reveal.compute_wtxid(),
579 verified,
580 commit: commit.clone(),
581 raw_commit: raw.to_vec(),
582 proof,
583 })
584 }
585 }
586 Err(_) => None,
587 };
588 check_dependency_cancelled(cancelled)?;
589 completed.push((ticket, position, record));
590 }
591 check_dependency_cancelled(cancelled)?;
592 let mut archive = self.archive()?;
593 check_dependency_cancelled(cancelled)?;
594 if let Some((_, generation)) = expected {
595 archive.check_generation(generation)?;
596 }
597 archive.finish_dependencies(completed)
598 }
599
600 pub fn admit_record(&self, package: RecordPackage) -> Result<RecordReceipt, Error> {
605 self.admit_record_before(package, None)
606 }
607
608 pub(crate) fn admit_record_before(
609 &self,
610 package: RecordPackage,
611 deadline: Option<Instant>,
612 ) -> Result<RecordReceipt, Error> {
613 if let Some(deadline) = deadline {
614 check_deadline(deadline)?;
615 }
616 let (ticket, chain, header) = {
617 let archive = self.archive()?;
618 let chain = archive.config().chain;
619 if package.version != API_VERSION
620 || package.chain_genesis
621 != chain
622 .genesis()
623 .map_err(|e| Error::Invalid(e.to_string()))?
624 .0
625 {
626 return Err(Error::Invalid(
627 "record package version or network mismatch".into(),
628 ));
629 }
630 let ticket = archive.ticket(package.block.height)?;
631 if ticket.point() != package.block {
632 return Err(Error::Stale);
633 }
634 let header = archive
635 .header_at(package.block.height)?
636 .ok_or(Error::Stale)?;
637 (ticket, chain, header)
638 };
639 let included = package
640 .reveal
641 .verify(chain, &header)
642 .map_err(|e| Error::Invalid(format!("selective inclusion: {e}")))?;
643 let commit = urma_chain::decode::transaction(&package.commit, chain)
644 .map_err(|e| Error::Invalid(format!("selective commit: {e}")))?;
645 let reveal = included.transaction();
646 let verified =
647 urma_core::multipart::VerifiedRecord::verify(reveal.compute_txid(), reveal, &commit)
648 .map_err(|e| Error::Invalid(format!("selective URMA proof: {e}")))?;
649 urma_core::envelope::extract_reveal(reveal)
653 .map_err(|e| Error::Invalid(format!("selective V0 record: {e}")))?;
654 let record = crate::scan::Record {
655 position: included.position(),
656 wtxid: reveal.compute_wtxid(),
657 verified,
658 commit,
659 raw_commit: package.commit,
660 proof: package.reveal,
661 };
662 let mut archive = self.archive()?;
663 if let Some(deadline) = deadline {
664 check_deadline(deadline)?;
665 }
666 archive.admit_record(ticket, record)
667 }
668
669 pub fn synchronize_headers(&self) -> Result<ArchiveStatus, Error> {
673 self.synchronize_headers_until(&AtomicBool::new(false))
674 }
675
676 pub fn synchronize_headers_until(
679 &self,
680 cancelled: &AtomicBool,
681 ) -> Result<ArchiveStatus, Error> {
682 let budget = Budget::new(cancelled);
683 budget.check()?;
684 let _sync = self
685 .synchronizing
686 .try_lock()
687 .map_err(|_| Error::Busy("synchronization already running".into()))?;
688 let source = self
689 .source
690 .as_ref()
691 .ok_or_else(|| Error::Invalid("node is offline; no chain source configured".into()))?;
692 let before = self.status()?;
693 let remote = source.tip()?;
694 budget.check()?;
695 let floor = before.application_start.height;
696 if remote.height < floor {
697 return Err(Error::Invalid(
698 "source tip precedes application start".into(),
699 ));
700 }
701 if source.header(floor)?.block_hash() != before.application_start.hash {
702 return Err(Error::Invalid(
703 "source does not contain configured application start".into(),
704 ));
705 }
706 let mut start = floor;
707 if let Some(local) = before.selected_tip {
708 let mut height = local.height.min(remote.height);
709 loop {
710 budget.check()?;
711 let chosen = source.header(height)?;
712 if self.archive()?.header_at(height)? == Some(chosen) {
713 if height == remote.height && remote.height < local.height {
714 return Err(Error::Invalid("verifier tip regressed on the same prefix; archived view retained, synchronization suspended".into()));
715 }
716 start = height + 1;
717 break;
718 }
719 if height == floor {
720 break;
721 }
722 height -= 1;
723 }
724 }
725 if start > remote.height {
726 return self.status();
727 }
728 let end = remote.height.min(start.saturating_add(1999));
729 let mut headers = Vec::new();
730 for height in start..=end {
731 budget.check()?;
732 headers.push(source.header(height)?);
733 }
734 budget.check()?;
737 if source.header(end)?
738 != *headers
739 .last()
740 .ok_or_else(|| Error::Invalid("empty header batch".into()))?
741 {
742 return Err(Error::Stale);
743 }
744 budget.check()?;
745 let mut archive = self.archive()?;
746 let now = archive.status()?;
747 if now.generation != before.generation || now.selected_tip != before.selected_tip {
748 return Err(Error::Stale);
749 }
750 budget.check()?;
751 archive.select(start, &headers)?;
752 archive.status()
753 }
754
755 pub fn scan_next(&self) -> Result<ArchiveStatus, Error> {
758 self.scan_next_until(&AtomicBool::new(false))
759 }
760
761 pub fn resolve_cached_dependency(&self) -> Result<usize, Error> {
764 let raw = self.archive()?.cached_dependency()?;
765 match raw {
766 Some(raw) => self.resolve_commit(&raw),
767 None => Ok(0),
768 }
769 }
770
771 pub fn scan_next_until(&self, cancelled: &AtomicBool) -> Result<ArchiveStatus, Error> {
772 let budget = Budget::new(cancelled);
773 budget.check()?;
774 let _sync = self
775 .synchronizing
776 .try_lock()
777 .map_err(|_| Error::Busy("synchronization already running".into()))?;
778 let source = self
779 .source
780 .as_ref()
781 .ok_or_else(|| Error::Invalid("node is offline; no chain source configured".into()))?;
782 let (ticket, chain, retention) = {
783 let archive = self.archive()?;
784 let Some(ticket) = archive.next_scan()? else {
785 return archive.status();
786 };
787 (ticket, archive.config().chain, archive.config().retention)
788 };
789 let raw = source.block(ticket.point().height)?;
790 budget.check()?;
791 let batch = crate::scan::ScanBatch::verify(chain, ticket, &raw, retention, |txid| {
792 budget.check()?;
793 if let Some(raw) = self.archive()?.transaction(txid)? {
794 return Ok(Some(raw));
795 }
796 let location = self.archive()?.location(txid)?;
797 let Some(location) = location else {
798 return Ok(None);
799 };
800 let raw = source.block(location.height)?;
801 budget.check()?;
802 let decoded =
803 urma_chain::decode::esplora_block_with_transactions(&raw, chain, location.hash)
804 .map_err(|e| Error::Invalid(format!("dependency block: {e}")))?;
805 budget.check()?;
806 if source.header(location.height)?.block_hash() != location.hash {
807 return Err(Error::Stale);
808 }
809 budget.check()?;
810 Ok(decoded
811 .block()
812 .txdata
813 .iter()
814 .position(|tx| tx.compute_txid() == txid)
815 .map(|i| decoded.raw_transactions()[i].clone()))
816 })?;
817 budget.check()?;
818 if source.header(ticket.point().height)?.block_hash() != ticket.point().hash {
819 return Err(Error::Stale);
820 }
821 budget.check()?;
822 let mut archive = self.archive()?;
823 budget.check()?;
824 archive.commit_scan(batch)?;
825 archive.status()
826 }
827}
828
829fn check_dependency_cancelled(cancelled: &AtomicBool) -> Result<(), Error> {
830 if cancelled.load(Ordering::Acquire) {
831 return Err(Error::Busy("dependency acquisition cancelled".into()));
832 }
833 Ok(())
834}
835
836struct Budget<'a> {
837 deadline: Instant,
838 cancelled: &'a AtomicBool,
839}
840impl<'a> Budget<'a> {
841 fn new(cancelled: &'a AtomicBool) -> Self {
842 Self {
843 deadline: Instant::now() + Duration::from_secs(30),
844 cancelled,
845 }
846 }
847 fn check(&self) -> Result<(), Error> {
848 if self.cancelled.load(Ordering::Acquire) || Instant::now() >= self.deadline {
849 return Err(Error::Busy(
850 "synchronization cancelled or time budget exhausted".into(),
851 ));
852 }
853 Ok(())
854 }
855}
856
857pub(crate) fn check_deadline(deadline: Instant) -> Result<(), Error> {
858 if Instant::now() >= deadline {
859 Err(Error::Busy("record acquisition deadline".into()))
860 } else {
861 Ok(())
862 }
863}