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