1use crate::errors::FxfsError;
160use crate::filesystem::{ApplyContext, ApplyMode, FlushReason, ForceMajor, JournalingObject};
161use crate::log::*;
162use crate::metrics;
163use crate::object_handle::{INVALID_OBJECT_ID, ReadObjectHandle};
164use crate::object_store::allocator::{Allocator, Reservation};
165use crate::object_store::directory::Directory;
166use crate::object_store::journal::{self, BorrowedMetadata, JournalCheckpoint};
167use crate::object_store::transaction::{
168 AssocObj, AssociatedObject, MetadataReservation, Mutation, ObjectMutationIterator, Transaction,
169 TxnMutation,
170};
171use crate::object_store::tree_cache::TreeCache;
172use crate::object_store::volume::{VOLUMES_DIRECTORY, list_volumes};
173use crate::object_store::{ObjectDescriptor, ObjectStore};
174use crate::round::round_div;
175use crate::serialized_types::{LATEST_VERSION, Version};
176use anyhow::{Context, Error, anyhow, bail, ensure};
177use fuchsia_inspect::{Property as _, UintProperty};
178use fuchsia_sync::RwLock;
179use futures::FutureExt as _;
180use rustc_hash::FxHashMap as HashMap;
181use std::cmp::Reverse;
182use std::collections::hash_map::Entry;
183use std::num::Saturating;
184use std::sync::{Arc, OnceLock};
185
186pub const fn reserved_space_from_journal_usage(journal_usage: u64) -> u64 {
196 journal_usage * 4
197}
198
199#[derive(Clone, Copy, Debug)]
202pub enum ReplayMetadataReservation {
203 Borrowed(u64),
205
206 Reserved(u64),
209}
210
211pub struct ObjectManager {
213 inner: RwLock<Inner>,
214 metadata_reservation: OnceLock<Reservation>,
215 volume_directory: OnceLock<Directory<ObjectStore>>,
216 on_new_store: Option<Box<dyn Fn(&ObjectStore) + Send + Sync>>,
217}
218
219#[derive(Clone, Debug)]
222struct CheckpointUsage {
223 checkpoint: JournalCheckpoint,
224
225 usage: u64,
228
229 borrowed_usage: u64,
232}
233
234impl CheckpointUsage {
235 fn new(checkpoint: JournalCheckpoint) -> Self {
236 Self { checkpoint, usage: 0, borrowed_usage: 0 }
237 }
238}
239
240#[derive(Debug)]
243enum Checkpoints {
244 Current(CheckpointUsage),
245 Old(CheckpointUsage),
246 Both(CheckpointUsage, CheckpointUsage),
247}
248
249impl Checkpoints {
250 fn earliest(&self) -> &JournalCheckpoint {
252 match self {
253 Checkpoints::Old(x) | Checkpoints::Both(x, _) | Checkpoints::Current(x) => {
254 &x.checkpoint
255 }
256 }
257 }
258
259 #[cfg(test)]
262 fn usage(&self) -> u64 {
263 match self {
264 Checkpoints::Current(x) | Checkpoints::Old(x) => {
265 debug_assert!(x.borrowed_usage <= x.usage);
266 x.usage
267 }
268 Checkpoints::Both(x, y) => {
269 debug_assert!(x.borrowed_usage <= x.usage);
270 debug_assert!(y.borrowed_usage <= y.usage);
271 x.usage.saturating_add(y.usage)
272 }
273 }
274 }
275
276 fn borrowed_usage(&self) -> u64 {
278 match self {
279 Checkpoints::Current(x) | Checkpoints::Old(x) => {
280 debug_assert!(x.borrowed_usage <= x.usage);
281 x.borrowed_usage
282 }
283 Checkpoints::Both(x, y) => {
284 debug_assert!(x.borrowed_usage <= x.usage);
285 debug_assert!(y.borrowed_usage <= y.usage);
286 x.borrowed_usage.saturating_add(y.borrowed_usage)
287 }
288 }
289 }
290
291 fn add_usage(&mut self, amount: u64, is_borrowed: bool) {
293 match self {
294 Checkpoints::Current(x) | Checkpoints::Both(_, x) => {
295 x.usage = x.usage.saturating_add(amount);
296 if is_borrowed {
297 x.borrowed_usage = x.borrowed_usage.saturating_add(amount);
298 }
299 debug_assert!(x.borrowed_usage <= x.usage);
300 }
301
302 Checkpoints::Old(_) => unreachable!(),
307 }
308 }
309}
310
311struct Inner {
314 stores: HashMap<u64, Arc<ObjectStore>>,
315 root_parent_store_object_id: u64,
316 root_store_object_id: u64,
317 allocator_object_id: u64,
318 allocator: Option<Arc<Allocator>>,
319
320 journal_checkpoints: HashMap<u64, Checkpoints>,
323
324 layer_sizes: HashMap<u64, u64>,
327
328 store_borrowed: HashMap<u64, u64>,
331
332 last_end_offset: u64,
335
336 replay_metadata_reservation: ReplayMetadataReservation,
338
339 max_transaction_size: (u64, UintProperty),
341
342 reserved_space: u64,
344}
345
346impl Inner {
347 fn system_tree_headroom(layer_size: u64) -> u64 {
351 layer_size / 2
352 }
353
354 fn tree_scratch(layer_size: u64) -> u64 {
357 layer_size
358 }
359
360 fn child_headroom_system_share(layer_size: u64) -> u64 {
363 layer_size / 8
364 }
365
366 fn child_headroom_store_share(layer_size: u64) -> u64 {
369 layer_size * 3 / 8
370 }
371
372 fn tree_headroom(&self, object_id: u64, layer_size: u64) -> u64 {
374 if self.is_child_store(object_id) {
375 Self::child_headroom_system_share(layer_size)
376 + Self::child_headroom_store_share(layer_size)
377 } else {
378 Self::system_tree_headroom(layer_size)
379 }
380 }
381
382 fn tree_reservation(&self, object_id: u64, layer_size: u64) -> u64 {
384 Self::tree_scratch(layer_size) + self.tree_headroom(object_id, layer_size)
385 }
386
387 fn is_system_store(&self, store_id: u64) -> bool {
388 store_id == self.root_store_object_id || store_id == self.root_parent_store_object_id
389 }
390
391 fn is_child_store(&self, object_id: u64) -> bool {
392 !self.is_system_store(object_id) && object_id != self.allocator_object_id
393 }
394
395 fn earliest_journal_offset(&self) -> Option<u64> {
396 self.journal_checkpoints.values().map(|c| c.earliest().file_offset).min()
397 }
398
399 fn max_tree_scratch(&self) -> u64 {
402 self.layer_sizes.values().copied().map(Self::tree_scratch).max().unwrap_or(0)
403 }
404
405 fn system_headroom(&self) -> u64 {
407 let mut headroom = self.reserved_space;
408 for (&object_id, &layer_size) in &self.layer_sizes {
409 if self.is_child_store(object_id) {
410 headroom += Self::child_headroom_system_share(layer_size);
411 } else {
412 headroom += Self::system_tree_headroom(layer_size);
413 }
414 }
415 headroom
416 }
417
418 fn total_journal_usage(&self) -> u64 {
420 self.earliest_journal_offset().map_or(0, |o| self.last_end_offset.saturating_sub(o))
421 }
422
423 #[cfg(test)]
425 fn required_store_reservation(&self, store_id: u64) -> u64 {
426 let layer_size = self.layer_sizes.get(&store_id).copied().unwrap_or(0);
427 let journal_usage = self.journal_checkpoints.get(&store_id).map_or(0, |c| c.usage());
428 Self::child_headroom_store_share(layer_size)
429 + reserved_space_from_journal_usage(journal_usage)
430 }
431
432 fn total_store_borrowed(&self, store_id: u64) -> u64 {
436 let max_layer_borrowed =
437 Self::child_headroom_store_share(self.layer_sizes.get(&store_id).copied().unwrap_or(0));
438 let layer_borrowed = std::cmp::min(
439 self.store_borrowed.get(&store_id).copied().unwrap_or(0),
440 max_layer_borrowed,
441 );
442 let journal_borrowed = reserved_space_from_journal_usage(
443 self.journal_checkpoints.get(&store_id).map_or(0, |c| c.borrowed_usage()),
444 );
445 layer_borrowed.saturating_add(journal_borrowed)
446 }
447
448 fn prune_stores_borrowed(&mut self) {
450 self.store_borrowed.retain(|&id, _| {
451 self.stores.contains_key(&id)
452 && id != self.root_store_object_id
453 && id != self.root_parent_store_object_id
454 && id != self.allocator_object_id
455 });
456 }
457
458 fn required_reservation(&self) -> u64 {
461 let mut child_headroom = 0;
462 for (&object_id, &layer_size) in &self.layer_sizes {
463 if self.is_child_store(object_id) {
464 child_headroom += Self::child_headroom_store_share(layer_size);
465 }
466 }
467 self.max_tree_scratch()
468 + self.system_headroom()
469 + child_headroom
470 + reserved_space_from_journal_usage(self.total_journal_usage())
471 }
472
473 fn borrowed_metadata_space(&self, metadata_reservation: Option<&Reservation>) -> u64 {
476 let required = self.required_reservation();
477 if let Some(reservation) = metadata_reservation {
478 required.saturating_sub(reservation.amount())
479 } else {
480 match self.replay_metadata_reservation {
481 ReplayMetadataReservation::Borrowed(borrowed) => borrowed,
482 ReplayMetadataReservation::Reserved(reserved) => required.saturating_sub(reserved),
483 }
484 }
485 }
486
487 fn record_journal_usage(&mut self, object_usages: &[(u64, u64)], is_borrowed: bool) {
489 for &(object_id, bytes) in object_usages {
490 if self.is_child_store(object_id)
491 && let Some(checkpoint) = self.journal_checkpoints.get_mut(&object_id)
492 {
493 checkpoint.add_usage(bytes, is_borrowed);
494 }
495 }
496 }
497
498 fn can_compact(&self, object_id: u64) -> bool {
501 self.journal_checkpoints.contains_key(&object_id)
502 || self
503 .stores
504 .get(&object_id)
505 .is_some_and(|s| s.can_flush_unlocked() && s.tree().immutable_layer_count() > 1)
506 }
507
508 fn should_force_major_compaction(&self, object_id: u64, total_reserved: u64) -> ForceMajor {
511 let over_threshold = if self.is_child_store(object_id) {
512 let b_s = self.total_store_borrowed(object_id);
517 let store_headroom = Self::child_headroom_store_share(
518 self.layer_sizes.get(&object_id).copied().unwrap_or(0),
519 );
520 b_s > store_headroom / 2
521 } else if object_id == self.root_store_object_id {
522 let total_borrowed = self.required_reservation().saturating_sub(total_reserved);
525 let store_borrowed_sum = self
526 .store_borrowed
527 .keys()
528 .fold(0u64, |acc, &id| acc.saturating_add(self.total_store_borrowed(id)));
529 let b_sys = total_borrowed.saturating_sub(store_borrowed_sum);
530 b_sys > self.system_headroom() / 2
531 } else {
532 false
533 };
534 if over_threshold && self.can_compact(object_id) {
535 ForceMajor::True
536 } else {
537 ForceMajor::False
538 }
539 }
540
541 fn journaling_object(&self, object_id: u64) -> Option<Arc<dyn JournalingObject>> {
542 if object_id == self.allocator_object_id {
543 Some(self.allocator.clone().unwrap() as Arc<dyn JournalingObject>)
544 } else {
545 self.stores.get(&object_id).map(|x| x.clone() as Arc<dyn JournalingObject>)
546 }
547 }
548
549 fn end_flush_checkpoints(&mut self, object_id: u64) {
551 if let Entry::Occupied(mut o) = self.journal_checkpoints.entry(object_id) {
552 match o.get_mut() {
553 Checkpoints::Old(_) => {
554 o.remove();
555 }
556 Checkpoints::Both(_, current) => {
557 *o.get_mut() = Checkpoints::Current(current.clone());
558 }
559 Checkpoints::Current(_) => {}
560 }
561 }
562 }
563
564 fn apply_mutation(
567 &mut self,
568 object_id: u64,
569 mutation: &Mutation,
570 context: &ApplyContext<'_, '_>,
571 ) -> bool {
572 match mutation {
573 Mutation::BeginFlush => {
574 if let Some(entry) = self.journal_checkpoints.get_mut(&object_id) {
575 match entry {
576 Checkpoints::Current(x) => {
577 *entry = Checkpoints::Old(x.clone());
578 }
579
580 Checkpoints::Both(x, y) => {
583 *entry = Checkpoints::Old(CheckpointUsage {
584 checkpoint: x.checkpoint.clone(),
585 usage: x.usage.saturating_add(y.usage),
586 borrowed_usage: x.borrowed_usage.saturating_add(y.borrowed_usage),
587 });
588 }
589 Checkpoints::Old(_) => {}
590 }
591 }
592 }
593 Mutation::EndFlush(borrowed) => {
594 self.end_flush_checkpoints(object_id);
595 if self.is_child_store(object_id)
600 && context.checkpoint.file_offset >= self.last_end_offset
601 {
602 self.store_borrowed.insert(object_id, *borrowed);
603 }
604 }
605 Mutation::DeleteVolume => {
606 if let Some(store) = self.stores.remove(&object_id) {
607 store.mark_deleted();
608 }
609 self.layer_sizes.remove(&object_id);
610 self.store_borrowed.remove(&object_id);
611 self.journal_checkpoints.remove(&object_id);
612 return false;
613 }
614 _ => {
615 if object_id != self.root_parent_store_object_id {
616 self.journal_checkpoints
617 .entry(object_id)
618 .and_modify(|entry| {
619 if let Checkpoints::Old(x) = entry {
620 *entry = Checkpoints::Both(
621 x.clone(),
622 CheckpointUsage::new(context.checkpoint.clone()),
623 );
624 }
625 })
626 .or_insert_with(|| {
627 Checkpoints::Current(CheckpointUsage::new(context.checkpoint.clone()))
628 });
629 }
630 }
631 }
632 true
633 }
634}
635
636impl ObjectManager {
637 pub fn new(on_new_store: Option<Box<dyn Fn(&ObjectStore) + Send + Sync>>) -> ObjectManager {
638 ObjectManager {
639 inner: RwLock::new(Inner {
640 stores: HashMap::default(),
641 root_parent_store_object_id: INVALID_OBJECT_ID,
642 root_store_object_id: INVALID_OBJECT_ID,
643 allocator_object_id: INVALID_OBJECT_ID,
644 allocator: None,
645 journal_checkpoints: HashMap::default(),
646 layer_sizes: HashMap::default(),
647 store_borrowed: HashMap::default(),
648 last_end_offset: 0,
649 replay_metadata_reservation: ReplayMetadataReservation::Reserved(u64::MAX),
650 max_transaction_size: (0, metrics::detail().create_uint("max_transaction_size", 0)),
651 reserved_space: journal::RESERVED_SPACE,
652 }),
653 metadata_reservation: OnceLock::new(),
654 volume_directory: OnceLock::new(),
655 on_new_store,
656 }
657 }
658
659 pub fn required_reservation(&self) -> u64 {
660 self.inner.read().required_reservation()
661 }
662
663 pub fn tree_reservation(&self, object_id: u64, layer_size: u64) -> u64 {
665 self.inner.read().tree_reservation(object_id, layer_size)
666 }
667
668 pub fn root_parent_store_object_id(&self) -> u64 {
669 self.inner.read().root_parent_store_object_id
670 }
671
672 pub fn root_parent_store(&self) -> Arc<ObjectStore> {
673 let inner = self.inner.read();
674 inner.stores.get(&inner.root_parent_store_object_id).unwrap().clone()
675 }
676
677 pub fn set_root_parent_store(&self, store: Arc<ObjectStore>) {
678 if let Some(on_new_store) = &self.on_new_store {
679 on_new_store(&store);
680 }
681 let mut inner = self.inner.write();
682 let store_id = store.store_object_id();
683 inner.stores.insert(store_id, store);
684 inner.root_parent_store_object_id = store_id;
685 }
686
687 pub fn root_store_object_id(&self) -> u64 {
688 self.inner.read().root_store_object_id
689 }
690
691 pub fn root_store(&self) -> Arc<ObjectStore> {
692 let inner = self.inner.read();
693 inner.stores.get(&inner.root_store_object_id).unwrap().clone()
694 }
695
696 pub fn set_root_store(&self, store: Arc<ObjectStore>) {
697 if let Some(on_new_store) = &self.on_new_store {
698 on_new_store(&store);
699 }
700 let mut inner = self.inner.write();
701 let store_id = store.store_object_id();
702 inner.stores.insert(store_id, store);
703 inner.root_store_object_id = store_id;
704 }
705
706 pub fn is_system_store(&self, store_id: u64) -> bool {
708 self.inner.read().is_system_store(store_id)
709 }
710
711 pub fn store(&self, store_object_id: u64) -> Option<Arc<ObjectStore>> {
713 self.inner.read().stores.get(&store_object_id).cloned()
714 }
715
716 pub fn compaction_bytes_written(&self) -> u64 {
719 let inner = self.inner.read();
720 let mut total = 0;
721 if let Some(allocator) = &inner.allocator {
722 total += allocator.tree().compaction_bytes_written();
723 }
724 for store in inner.stores.values() {
725 total += store.tree().compaction_bytes_written();
726 }
727 total
728 }
729
730 pub async fn on_replay_complete(&self) -> Result<(), Error> {
733 let root_store = self.root_store();
734
735 let root_directory = Directory::open(&root_store, root_store.root_directory_object_id())
736 .await
737 .context("Unable to open root volume directory")?;
738
739 match root_directory.lookup(VOLUMES_DIRECTORY).await? {
740 None => bail!("Root directory not found"),
741 Some((object_id, ObjectDescriptor::Directory, _)) => {
742 let volume_directory = Directory::open(&root_store, object_id)
743 .await
744 .context("Unable to open volumes directory")?;
745 self.volume_directory.set(volume_directory).unwrap();
746 }
747 _ => {
748 bail!(
749 anyhow!(FxfsError::Inconsistent)
750 .context("Unexpected type for volumes directory")
751 )
752 }
753 }
754
755 let object_ids = list_volumes(self.volume_directory.get().unwrap())
756 .await
757 .context("Failed to list volumes")?;
758
759 for store_id in object_ids {
760 self.open_store(&root_store, store_id).await?;
761 }
762
763 self.init_metadata_reservation()
766 .await
767 .context("Insufficient free space for metadata reservation.")?;
768
769 Ok(())
770 }
771
772 pub fn volume_directory(&self) -> &Directory<ObjectStore> {
773 self.volume_directory.get().unwrap()
774 }
775
776 pub fn set_volume_directory(&self, volume_directory: Directory<ObjectStore>) {
777 self.volume_directory.set(volume_directory).unwrap();
778 }
779
780 pub fn add_store(&self, store: Arc<ObjectStore>) {
781 if let Some(on_new_store) = &self.on_new_store {
782 on_new_store(&store);
783 }
784 let mut inner = self.inner.write();
785 let store_object_id = store.store_object_id();
786 assert_ne!(store_object_id, inner.root_parent_store_object_id);
787 assert_ne!(store_object_id, inner.root_store_object_id);
788 assert_ne!(store_object_id, inner.allocator_object_id);
789 inner.stores.insert(store_object_id, store);
790 inner.store_borrowed.entry(store_object_id).or_default();
791 }
792
793 pub fn forget_store(&self, store_object_id: u64) {
794 let mut inner = self.inner.write();
795 assert_ne!(store_object_id, inner.allocator_object_id);
796 inner.stores.remove(&store_object_id);
797 inner.layer_sizes.remove(&store_object_id);
798 inner.store_borrowed.remove(&store_object_id);
799 inner.journal_checkpoints.remove(&store_object_id);
800 }
801
802 pub fn set_allocator(&self, allocator: Arc<Allocator>) {
803 let mut inner = self.inner.write();
804 assert!(!inner.stores.contains_key(&allocator.object_id()));
805 inner.allocator_object_id = allocator.object_id();
806 inner.allocator = Some(allocator);
807 }
808
809 pub fn allocator(&self) -> Arc<Allocator> {
810 self.inner.read().allocator.clone().unwrap()
811 }
812
813 pub fn apply_mutation(
815 &self,
816 object_id: u64,
817 mutation: Mutation,
818 context: &ApplyContext<'_, '_>,
819 associated_object: AssocObj<'_>,
820 ) -> Result<(), Error> {
821 debug!(oid = object_id, mutation:?; "applying mutation");
822 associated_object.map(|o| o.will_apply_mutation(&mutation, object_id, self));
823 let object = {
824 let mut inner = self.inner.write();
825 if !inner.apply_mutation(object_id, &mutation, context) {
826 return Ok(());
827 }
828 inner.journaling_object(object_id).unwrap()
829 };
830 object.apply_mutation(mutation, context, associated_object)
831 }
832
833 pub async fn replay_mutations(
838 &self,
839 mutations: Vec<(u64, Mutation)>,
840 object_journal_usage: &[(u64, u64)],
841 journal_offsets: &HashMap<u64, u64>,
842 context: &ApplyContext<'_, '_>,
843 end_offset: u64,
844 borrowed_metadata: BorrowedMetadata,
845 ) -> Result<(), Error> {
846 debug!(checkpoint = context.checkpoint.file_offset; "REPLAY");
847 let allocator_object_id = self.inner.read().allocator_object_id;
848
849 for (object_id, mutation) in mutations {
850 if let Some(&offset) = journal_offsets.get(&object_id)
854 && context.checkpoint.file_offset < offset
855 {
856 self.inner.write().apply_mutation(object_id, &mutation, context);
857 continue;
858 }
859
860 if object_id != allocator_object_id {
862 self.open_store(&self.root_store(), object_id).await?;
863 }
864
865 self.apply_mutation(object_id, mutation, context, AssocObj::None)?;
866 }
867
868 let mut inner = self.inner.write();
869 let is_borrowed = !matches!(borrowed_metadata, BorrowedMetadata::None);
870 inner.record_journal_usage(object_journal_usage, is_borrowed);
871
872 if end_offset > inner.last_end_offset {
873 let old_last_end_offset = std::mem::replace(&mut inner.last_end_offset, end_offset);
874 let txn_space = inner.earliest_journal_offset().map_or(0, |min| {
875 reserved_space_from_journal_usage(
876 end_offset.saturating_sub(std::cmp::max(old_last_end_offset, min)),
877 )
878 });
879 match borrowed_metadata {
880 BorrowedMetadata::None => {
881 if let ReplayMetadataReservation::Reserved(ref mut reserved) =
882 inner.replay_metadata_reservation
883 {
884 *reserved = reserved
885 .checked_add(txn_space)
886 .ok_or(FxfsError::Inconsistent)
887 .context("reserved overflow on funded txn")?;
888 }
889 }
890 BorrowedMetadata::Borrowed(reserved) => {
891 inner.replay_metadata_reservation =
892 ReplayMetadataReservation::Reserved(reserved);
893 }
894 BorrowedMetadata::LegacyBorrowed(borrowed) => {
895 match inner.replay_metadata_reservation {
896 ReplayMetadataReservation::Borrowed(ref mut b) => {
897 *b = borrowed
898 .checked_add(txn_space)
899 .ok_or(FxfsError::Inconsistent)
900 .context("legacy borrowed overflow")?;
901 }
902 ReplayMetadataReservation::Reserved(_) => {
903 bail!(
904 anyhow!(FxfsError::Inconsistent)
905 .context("unexpected LegacyBorrowed in Reserved mode")
906 );
907 }
908 }
909 }
910 }
911 }
912 Ok(())
913 }
914
915 async fn open_store(&self, parent: &Arc<ObjectStore>, object_id: u64) -> Result<(), Error> {
917 if self.inner.read().stores.contains_key(&object_id) {
918 return Ok(());
919 }
920 let store = ObjectStore::open(parent, object_id, Some(Box::new(TreeCache::new())))
921 .await
922 .with_context(|| format!("Failed to open store {object_id}"))?;
923 if let Some(on_new_store) = &self.on_new_store {
924 on_new_store(&store);
925 }
926 let mut inner = self.inner.write();
927 assert!(inner.stores.insert(object_id, store).is_none());
928 inner.store_borrowed.entry(object_id).or_default();
929 Ok(())
930 }
931
932 pub fn apply_transaction(
935 &self,
936 transaction: &mut Transaction<'_>,
937 checkpoint: &JournalCheckpoint,
938 ) -> Result<(), Error> {
939 let old_amount = self.metadata_reservation().amount();
941 let old_required = self.inner.read().required_reservation();
942
943 debug!(checkpoint = checkpoint.file_offset; "BEGIN TXN");
944 let mutations = transaction.take_mutations();
945 let context =
946 ApplyContext { mode: ApplyMode::Live(transaction), checkpoint: checkpoint.clone() };
947 for TxnMutation { object_id, mutation, associated_object, .. } in mutations {
948 self.apply_mutation(object_id, mutation, &context, associated_object)?;
949 }
950 debug!("END TXN");
951
952 if !transaction.metadata_reservation.is_borrowed() {
953 debug_assert_eq!(self.metadata_reservation().amount(), old_amount);
956 debug_assert_eq!(self.inner.read().required_reservation(), old_required);
957 }
958
959 Ok(())
960 }
961
962 pub fn did_commit_transaction(
965 &self,
966 transaction: &mut Transaction<'_>,
967 end_offset: u64,
968 object_usages: &[(u64, u64)],
969 ) {
970 let reservation = self.metadata_reservation();
971 let mut inner = self.inner.write();
972 let old_last_end_offset = std::mem::replace(&mut inner.last_end_offset, end_offset);
973 let journal_usage = end_offset - old_last_end_offset;
974
975 if journal_usage > inner.max_transaction_size.0 {
976 inner.max_transaction_size.0 = journal_usage;
977 inner.max_transaction_size.1.set(journal_usage);
978 }
979
980 let txn_space = inner.earliest_journal_offset().map_or(0, |min| {
981 reserved_space_from_journal_usage(
982 end_offset.saturating_sub(std::cmp::max(old_last_end_offset, min)),
983 )
984 });
985 let is_borrowed = transaction.metadata_reservation.is_borrowed();
986 match &mut transaction.metadata_reservation {
987 MetadataReservation::Borrowed => {}
988 MetadataReservation::Hold(hold) => {
989 let txn_reservation = *hold.owner();
991 assert_ne!(
992 txn_reservation as *const _, reservation as *const _,
993 "MetadataReservation::Borrowed should be used."
994 );
995 txn_reservation.commit(txn_space);
996 if txn_reservation.owner_object_id() != reservation.owner_object_id() {
997 assert_eq!(
998 reservation.owner_object_id(),
999 None,
1000 "Should not be mixing attributed owners."
1001 );
1002 inner
1003 .allocator
1004 .as_ref()
1005 .unwrap()
1006 .disown_reservation(txn_reservation.owner_object_id(), txn_space);
1007 }
1008 assert!(
1009 hold.amount() >= txn_space,
1010 "Transaction was larger than metadata reservation"
1011 );
1012 hold.forget_some(txn_space);
1013 reservation.add(txn_space);
1014 }
1015 MetadataReservation::Reservation(txn_reservation) => {
1016 txn_reservation.move_to(reservation, txn_space);
1018 }
1019 }
1020 inner.record_journal_usage(object_usages, is_borrowed);
1021 }
1022
1023 pub fn drop_transaction(&self, transaction: &mut Transaction<'_>) {
1028 for TxnMutation { object_id, mutation, .. } in transaction.take_mutations() {
1029 self.journaling_object(object_id).map(|o| o.drop_mutation(mutation, transaction));
1030 }
1031 }
1032
1033 pub fn journal_file_offsets(&self) -> (HashMap<u64, u64>, Option<JournalCheckpoint>) {
1036 let inner = self.inner.read();
1037 let mut min_checkpoint = None;
1038 let mut offsets = HashMap::default();
1039 for (&object_id, checkpoint) in &inner.journal_checkpoints {
1040 let checkpoint = checkpoint.earliest();
1041 match &mut min_checkpoint {
1042 None => min_checkpoint = Some(checkpoint),
1043 Some(min_checkpoint) => {
1044 if checkpoint.file_offset < min_checkpoint.file_offset {
1045 *min_checkpoint = checkpoint;
1046 }
1047 }
1048 }
1049 offsets.insert(object_id, checkpoint.file_offset);
1050 }
1051 (offsets, min_checkpoint.cloned())
1052 }
1053
1054 pub fn journal_checkpoint(&self, object_id: u64) -> Option<JournalCheckpoint> {
1057 self.inner
1058 .read()
1059 .journal_checkpoints
1060 .get(&object_id)
1061 .map(|checkpoints| checkpoints.earliest().clone())
1062 }
1063
1064 pub fn needs_flush(&self, object_id: u64) -> bool {
1067 self.inner.read().journal_checkpoints.contains_key(&object_id)
1068 }
1069
1070 pub async fn flush(&self, reason: FlushReason) -> Result<Version, Error> {
1073 let objects = {
1074 let inner = self.inner.read();
1075 let total_reserved = self.metadata_reservation.get().map_or(0, |r| r.amount());
1076 let mut objects = Vec::new();
1077 let candidate_ids = inner
1078 .stores
1079 .keys()
1080 .copied()
1081 .filter(|&id| id != inner.root_parent_store_object_id)
1082 .chain(std::iter::once(inner.allocator_object_id));
1083 for object_id in candidate_ids {
1084 let has_checkpoint = inner.journal_checkpoints.contains_key(&object_id);
1085 let object_reason = match reason {
1086 FlushReason::Journal(ForceMajor::False) => {
1087 let force_major =
1088 inner.should_force_major_compaction(object_id, total_reserved);
1089 if !has_checkpoint && force_major == ForceMajor::False {
1090 continue;
1091 }
1092 FlushReason::Journal(force_major)
1093 }
1094 FlushReason::Journal(ForceMajor::True) => {
1095 if !inner.can_compact(object_id) {
1096 continue;
1097 }
1098 FlushReason::Journal(ForceMajor::True)
1099 }
1100 other => {
1101 if !has_checkpoint {
1102 continue;
1103 }
1104 other
1105 }
1106 };
1107 objects.push((
1108 object_id,
1109 inner.journaling_object(object_id).unwrap(),
1110 object_reason,
1111 ));
1112 }
1113
1114 objects.sort_unstable_by_key(|(oid, _, _)| Reverse(*oid));
1118 objects
1119 };
1120
1121 let mut earliest_version: Version = LATEST_VERSION;
1123 for (object_id, object, object_reason) in objects {
1124 let object_earliest_version = object
1125 .flush(object_reason)
1126 .await
1127 .with_context(|| format!("Failed to flush oid {object_id}"))?;
1128 if object_earliest_version < earliest_version {
1129 earliest_version = object_earliest_version;
1130 }
1131 }
1132
1133 Ok(earliest_version)
1134 }
1135
1136 pub fn journaling_object(&self, object_id: u64) -> Option<Arc<dyn JournalingObject>> {
1137 self.inner.read().journaling_object(object_id)
1138 }
1139
1140 async fn legacy_required_reservation(&self) -> Result<u64, Error> {
1145 const LEGACY_RESERVED_SPACE: u64 = 1_048_576;
1146
1147 let (stores, allocator, total_journal_usage) = {
1148 let inner = self.inner.read();
1149 (
1150 inner.stores.values().cloned().collect::<Vec<_>>(),
1151 inner.allocator.clone().unwrap(),
1152 inner.total_journal_usage(),
1153 )
1154 };
1155
1156 let mut max_tree_reservation = allocator
1157 .tree()
1158 .immutable_layer_set()
1159 .layers
1160 .iter()
1161 .map(|l| l.handle().map(ReadObjectHandle::get_size).unwrap_or(0))
1162 .sum::<u64>()
1163 * 3
1164 / 2;
1165
1166 for store in stores {
1167 let total_layer_size = if store.is_locked() {
1168 let info = store.load_store_info().await?;
1169 let parent_store = store.parent_store().unwrap();
1170 let mut size = 0;
1171 for &oid in &info.layers {
1172 size += parent_store.get_file_size(oid).await?;
1173 }
1174 if info.encrypted_mutations_object_id != INVALID_OBJECT_ID {
1175 size +=
1176 parent_store.get_file_size(info.encrypted_mutations_object_id).await? * 3;
1177 }
1178 size
1179 } else {
1180 store
1181 .tree()
1182 .immutable_layer_set()
1183 .layers
1184 .iter()
1185 .map(|l| l.handle().map(ReadObjectHandle::get_size).unwrap_or(0))
1186 .sum()
1187 };
1188 max_tree_reservation = std::cmp::max(max_tree_reservation, total_layer_size * 3 / 2);
1189 }
1190
1191 Ok(max_tree_reservation + total_journal_usage * 4 + LEGACY_RESERVED_SPACE)
1192 }
1193
1194 pub async fn init_metadata_reservation(&self) -> Result<(), Error> {
1197 let legacy_required = if matches!(
1198 self.inner.read().replay_metadata_reservation,
1199 ReplayMetadataReservation::Borrowed(_)
1200 ) {
1201 Some(self.legacy_required_reservation().await?)
1202 } else {
1203 None
1204 };
1205 let (amount, required, replay_state, allocator) = {
1206 let mut inner = self.inner.write();
1207 let required = inner.required_reservation();
1208 let replay_state = inner.replay_metadata_reservation;
1209 let amount = match replay_state {
1210 ReplayMetadataReservation::Borrowed(legacy_borrowed) => {
1217 let legacy_required = legacy_required.unwrap();
1218 ensure!(legacy_required >= legacy_borrowed, FxfsError::Inconsistent);
1219 let amount = std::cmp::min(legacy_required - legacy_borrowed, required);
1220 let borrowed = required - amount;
1221 let max_scratch = inner.max_tree_scratch();
1222 let total_journal_space =
1223 reserved_space_from_journal_usage(inner.total_journal_usage());
1224 let flushed_headroom =
1225 required.saturating_sub(max_scratch + total_journal_space);
1226 let total_unflushed_borrowed: u64 = inner
1227 .journal_checkpoints
1228 .values()
1229 .map(|c| reserved_space_from_journal_usage(c.borrowed_usage()))
1230 .sum();
1231 let flushed_borrowed = borrowed.saturating_sub(total_unflushed_borrowed);
1232 inner.store_borrowed.clear();
1233 let store_ids: Vec<u64> = inner.stores.keys().copied().collect();
1234 for id in store_ids {
1235 if inner.is_child_store(id) {
1236 let layer_size = inner.layer_sizes.get(&id).copied().unwrap_or(0);
1237 let h_s = Inner::child_headroom_store_share(layer_size);
1238 let layer_borrowed = if flushed_headroom > 0 {
1239 std::cmp::min(
1240 h_s,
1241 ((h_s as u128 * flushed_borrowed as u128)
1242 / flushed_headroom as u128)
1243 as u64,
1244 )
1245 } else {
1246 0
1247 };
1248 inner.store_borrowed.insert(id, layer_borrowed);
1249 }
1250 }
1251 inner.prune_stores_borrowed();
1252 inner.replay_metadata_reservation = ReplayMetadataReservation::Reserved(amount);
1253 amount
1254 }
1255
1256 ReplayMetadataReservation::Reserved(reserved_total) => {
1261 inner.prune_stores_borrowed();
1262 std::cmp::min(reserved_total, required)
1263 }
1264 };
1265 (amount, required, replay_state, inner.allocator.as_ref().cloned().unwrap())
1266 };
1267 if self.root_parent_store().filesystem().options().read_only {
1268 return Ok(());
1270 }
1271 self.metadata_reservation
1272 .set(allocator.clone().reserve(None, amount).with_context(|| {
1273 format!(
1274 "Failed to reserve {} bytes (required: {}, replay: {:?}), free={}, \
1275 owner_bytes={}",
1276 amount,
1277 required,
1278 replay_state,
1279 Saturating(allocator.get_disk_bytes()) - allocator.get_used_bytes(),
1280 allocator.owner_bytes_debug(),
1281 )
1282 })?)
1283 .unwrap();
1284 Ok(())
1285 }
1286
1287 pub fn metadata_reservation(&self) -> &Reservation {
1288 self.metadata_reservation.get().unwrap()
1289 }
1290
1291 pub fn is_metadata_reservation(&self, reservation: &Reservation) -> bool {
1292 self.metadata_reservation.get().is_some_and(|r| std::ptr::eq(r, reservation))
1293 }
1294
1295 pub fn reconcile_metadata_reservation(&self, checkpoint_offset: u64) -> HashMap<u64, u64> {
1300 let reservation = self.metadata_reservation();
1301 let mut inner = self.inner.write();
1302 inner.last_end_offset = checkpoint_offset;
1303 let required = inner.required_reservation();
1304 inner.prune_stores_borrowed();
1305 let mut result = inner.store_borrowed.clone();
1306
1307 let to_give_back = reservation.amount().saturating_sub(required);
1311 if to_give_back > 0 {
1312 reservation.give_back(to_give_back);
1313 }
1314 result.insert(0, reservation.amount());
1315 result
1316 }
1317
1318 pub fn update_layer_size(&self, object_id: u64, layer_size: u64) {
1320 self.inner.write().layer_sizes.insert(object_id, layer_size);
1321 }
1322
1323 pub fn store_borrowed_for_flush(
1327 &self,
1328 object_id: u64,
1329 old_size: u64,
1330 new_size: u64,
1331 total_layer_size: u64,
1332 ) -> u64 {
1333 let inner = self.inner.read();
1334 if !inner.is_child_store(object_id) {
1335 return 0;
1336 }
1337 let current = inner.store_borrowed.get(&object_id).copied().unwrap_or(0);
1338 let updated = if new_size >= old_size {
1339 let borrowed_usage = match inner.journal_checkpoints.get(&object_id) {
1340 Some(Checkpoints::Old(old) | Checkpoints::Both(old, _)) => old.borrowed_usage,
1341 _ => 0,
1342 };
1343 let added = std::cmp::min(
1344 reserved_space_from_journal_usage(borrowed_usage),
1345 inner.tree_reservation(object_id, new_size - old_size),
1346 );
1347 current.saturating_add(added)
1348 } else {
1349 let released = inner.tree_reservation(object_id, old_size - new_size);
1350 current.saturating_sub(released)
1351 };
1352 std::cmp::min(updated, Inner::child_headroom_store_share(total_layer_size))
1353 }
1354
1355 pub fn reservation(&self, object_id: u64) -> Option<u64> {
1356 let inner = self.inner.read();
1357 inner
1358 .layer_sizes
1359 .get(&object_id)
1360 .copied()
1361 .map(|layer_size| inner.tree_reservation(object_id, layer_size))
1362 }
1363
1364 pub fn max_store_reservation(&self) -> u64 {
1366 let inner = self.inner.read();
1367 inner
1368 .layer_sizes
1369 .iter()
1370 .map(|(&id, &layer_size)| inner.tree_reservation(id, layer_size))
1371 .max()
1372 .unwrap_or(0)
1373 }
1374
1375 pub fn set_reserved_space(&self, amount: u64) {
1376 self.inner.write().reserved_space = amount;
1377 }
1378
1379 pub fn last_end_offset(&self) -> u64 {
1380 self.inner.read().last_end_offset
1381 }
1382
1383 pub fn set_last_end_offset(&self, v: u64) {
1384 self.inner.write().last_end_offset = v;
1385 }
1386
1387 pub fn borrowed_metadata_space(&self) -> u64 {
1389 self.inner.read().borrowed_metadata_space(self.metadata_reservation.get())
1390 }
1391
1392 pub fn stores_borrowed(&self) -> HashMap<u64, u64> {
1394 let inner = self.inner.read();
1395 inner.store_borrowed.keys().map(|&id| (id, inner.total_store_borrowed(id))).collect()
1396 }
1397
1398 pub fn init_replay_metadata_reservation(&self, version: Version, reserved: &HashMap<u64, u64>) {
1403 let mut inner = self.inner.write();
1404 inner.store_borrowed.clear();
1405 if version.major < 61 {
1406 inner.replay_metadata_reservation =
1407 ReplayMetadataReservation::Borrowed(reserved.get(&0).copied().unwrap_or(0));
1408 } else {
1409 let total = reserved.get(&0).copied().unwrap_or(0);
1410 for (&object_id, &amount) in reserved {
1411 if object_id != 0 {
1412 inner.store_borrowed.insert(object_id, amount);
1413 }
1414 }
1415 inner.replay_metadata_reservation = ReplayMetadataReservation::Reserved(total);
1416 }
1417 }
1418
1419 pub fn write_mutations(
1420 &self,
1421 object_id: u64,
1422 mutations: ObjectMutationIterator<'_, '_>,
1423 writer: journal::Writer<'_>,
1424 ) {
1425 self.journaling_object(object_id).unwrap().write_mutations(mutations, writer);
1426 }
1427
1428 pub fn unlocked_stores(&self) -> Vec<Arc<ObjectStore>> {
1429 let inner = self.inner.read();
1430 let mut stores = Vec::new();
1431 for store in inner.stores.values() {
1432 if !store.is_locked() {
1433 stores.push(store.clone());
1434 }
1435 }
1436 stores
1437 }
1438
1439 pub fn track_statistics(self: &Arc<Self>, parent: &fuchsia_inspect::Node, name: &str) {
1442 let this = Arc::downgrade(self);
1443 parent.record_lazy_child(name, move || {
1444 let this_clone = this.clone();
1445 async move {
1446 let inspector = fuchsia_inspect::Inspector::default();
1447 if let Some(this) = this_clone.upgrade() {
1448 let (required, borrowed, earliest_checkpoint) = {
1449 let inner = this.inner.read();
1451 let required = inner.required_reservation();
1452 let borrowed =
1453 inner.borrowed_metadata_space(this.metadata_reservation.get());
1454 (required, borrowed, inner.earliest_journal_offset())
1455 };
1456 let root = inspector.root();
1457 if let Some(reservation) = this.metadata_reservation.get() {
1458 root.record_uint("metadata_reservation", reservation.amount());
1459 }
1460 root.record_uint("required_reservation", required);
1461 root.record_uint("borrowed_reservation", borrowed);
1462 if let Some(earliest_checkpoint) = earliest_checkpoint {
1463 root.record_uint("earliest_checkpoint", earliest_checkpoint);
1464 }
1465
1466 if let Some(x) = round_div(100 * borrowed, required) {
1469 root.record_uint("borrowed_to_required_reservation_percent", x);
1470 }
1471 }
1472 Ok(inspector)
1473 }
1474 .boxed()
1475 });
1476 }
1477
1478 pub fn needs_borrow_for_journal(&self, checkpoint: u64) -> bool {
1487 let inner = self.inner.read();
1488 !inner.journal_checkpoints.is_empty()
1489 && checkpoint.checked_sub(inner.last_end_offset).unwrap()
1490 > journal::BLOCK_SIZE.get() + 64
1491 }
1492
1493 #[cfg(test)]
1494 pub fn store_borrowed(&self, store_id: u64) -> Option<(u64, u64)> {
1495 let inner = self.inner.read();
1496 inner.store_borrowed.contains_key(&store_id).then(|| {
1497 (inner.total_store_borrowed(store_id), inner.required_store_reservation(store_id))
1498 })
1499 }
1500
1501 #[cfg(test)]
1502 pub fn set_store_borrowed(&self, store_id: u64, amount: u64) {
1503 let mut inner = self.inner.write();
1504 let unflushed_borrowed = reserved_space_from_journal_usage(
1505 inner.journal_checkpoints.get(&store_id).map_or(0, |c| c.borrowed_usage()),
1506 );
1507 inner.store_borrowed.insert(store_id, amount.saturating_sub(unflushed_borrowed));
1508 }
1509
1510 #[cfg(test)]
1511 pub fn reserved_metadata_space(&self) -> HashMap<u64, u64> {
1512 let inner = self.inner.read();
1513 let required = inner.required_reservation();
1514 let total = self.metadata_reservation.get().map_or(0, |r| r.amount());
1515 let mut result = HashMap::default();
1516 for &id in inner.store_borrowed.keys() {
1517 result.insert(id, inner.total_store_borrowed(id));
1518 }
1519 result.insert(0, std::cmp::min(total, required));
1520 result
1521 }
1522}
1523
1524pub struct ReservationUpdate(u64);
1527
1528impl ReservationUpdate {
1529 pub fn new(layer_size: u64) -> Self {
1530 Self(layer_size)
1531 }
1532}
1533
1534impl AssociatedObject for ReservationUpdate {
1535 fn will_apply_mutation(&self, _mutation: &Mutation, object_id: u64, manager: &ObjectManager) {
1536 manager.update_layer_size(object_id, self.0);
1537 }
1538}
1539
1540#[cfg(test)]
1541mod tests {
1542 use super::{ForceMajor, ObjectManager};
1543 use crate::filesystem::{FxFilesystem, FxFilesystemBuilder, SyncOptions};
1544 use crate::fsck::{FsckOptions, fsck_volume_with_options, fsck_with_options};
1545 use crate::object_handle::{ObjectHandle, WriteObjectHandle};
1546 use crate::object_store::directory::replace_child;
1547 use crate::object_store::journal::JournalOptions;
1548 use crate::object_store::transaction::{
1549 AssocObj, AssociatedObject, LockKey, Mutation, Options, ReservationOptions, lock_keys,
1550 };
1551 use crate::object_store::volume::root_volume;
1552 use crate::object_store::{
1553 Directory, FlushReason, HandleOptions, INVALID_OBJECT_ID, NewChildStoreOptions, ObjectKey,
1554 ObjectStore, ObjectValue, StoreOptions,
1555 };
1556 use fxfs_crypto::Crypt;
1557 use fxfs_insecure_crypto::new_insecure_crypt;
1558 use rand::rngs::SmallRng;
1559 use rand::{RngExt, SeedableRng};
1560 use std::sync::Arc;
1561 use std::time::Instant;
1562 use storage_device::DeviceHolder;
1563 use storage_device::fake_device::FakeDevice;
1564
1565 #[fuchsia::test(threads = 4)]
1574 async fn test_metadata_reservation_stress() {
1575 struct VolState {
1576 name: String,
1577 store_id: u64,
1578 crypt: Option<Arc<dyn Crypt>>,
1579 permanent: bool,
1580 churn_cursor: usize,
1581 churn_gen: Vec<u64>,
1582 }
1583
1584 async fn check_all_volumes(fs: &Arc<FxFilesystem>, vols: &[VolState]) {
1585 fs.graveyard().flush().await;
1586 let options = FsckOptions {
1587 fail_on_warning: true,
1588 halt_on_error: true,
1589 do_slow_passes: true,
1590 quiet: true,
1591 ..Default::default()
1592 };
1593 fsck_with_options(fs.clone(), &options).await.expect("fsck failed");
1594 for vol in vols {
1595 if fs.object_manager().store(vol.store_id).is_some() {
1596 fsck_volume_with_options(
1597 fs.as_ref(),
1598 &options,
1599 vol.store_id,
1600 vol.crypt.clone(),
1601 )
1602 .await
1603 .expect("fsck_volume failed");
1604 }
1605 }
1606 }
1607
1608 async fn list_dir_files(store: &Arc<ObjectStore>) -> Vec<(String, u64)> {
1609 let root_dir = Directory::open(store, store.root_directory_object_id())
1610 .await
1611 .expect("open root_dir failed");
1612 let layer_set = store.tree().layer_set();
1613 let mut merger = layer_set.merger();
1614 let mut iter = root_dir.iter(&mut merger).await.expect("iter failed");
1615 let mut files = Vec::new();
1616 while let Some((name, object_id, _)) = iter.get() {
1617 if name != "filler" {
1618 files.push((name.to_string(), object_id));
1619 }
1620 iter.advance().await.expect("advance failed");
1621 }
1622 files
1623 }
1624
1625 fn pick_unlocked_volume(
1628 fs: &FxFilesystem,
1629 vols: &[VolState],
1630 rng: &mut SmallRng,
1631 ) -> Arc<ObjectStore> {
1632 let unlocked: Vec<usize> = (0..vols.len())
1633 .filter(|&i| {
1634 fs.object_manager().store(vols[i].store_id).is_some_and(|s| !s.is_locked())
1635 })
1636 .collect();
1637 let idx = unlocked[rng.random_range(0..unlocked.len())];
1638 fs.object_manager().store(vols[idx].store_id).unwrap()
1639 }
1640
1641 async fn unlink_and_tombstone_file(
1642 fs: &FxFilesystem,
1643 store: &Arc<ObjectStore>,
1644 fname: &str,
1645 oid: u64,
1646 immediate: bool,
1647 ) {
1648 let root_dir = Directory::open(store, store.root_directory_object_id())
1649 .await
1650 .expect("open root_dir failed");
1651 let ctx = root_dir
1652 .acquire_context_for_replace(None, fname, true)
1653 .await
1654 .expect("acquire_context_for_replace failed");
1655 let mut txn = ctx.transaction;
1656 let truncate_guard = ctx.truncate_guard;
1657 replace_child(&mut txn, None, (&root_dir, fname)).await.expect("replace_child failed");
1658 txn.commit().await.expect("commit unlink failed");
1659 if immediate {
1660 fs.tombstone_object(store.store_object_id(), oid, truncate_guard.as_ref())
1661 .await
1662 .expect("tombstone_object failed");
1663 } else {
1664 std::mem::drop(truncate_guard);
1665 fs.graveyard().queue_tombstone_object(store.store_object_id(), oid);
1666 }
1667 }
1668
1669 let stress_secs =
1670 std::env::var("FXFS_STRESS_SECS").ok().and_then(|s| s.parse::<u64>().ok()).unwrap_or(0);
1671 let base_seed = std::env::var("FXFS_STRESS_SEED")
1672 .ok()
1673 .map(|s| {
1674 if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
1675 u64::from_str_radix(hex, 16)
1676 } else {
1677 s.parse::<u64>()
1678 }
1679 .expect("invalid FXFS_STRESS_SEED")
1680 })
1681 .unwrap_or_else(rand::random);
1682
1683 let start_time = Instant::now();
1684 let mut episode = 0u64;
1685
1686 loop {
1687 let seed = base_seed.wrapping_add(episode);
1688 let mut rng = SmallRng::seed_from_u64(seed);
1689 let steps_per_episode = if stress_secs > 0 { 400 } else { 160 };
1690 if episode == 0 || (stress_secs > 0 && episode % 25 == 0) {
1691 eprintln!(
1692 "[stress] episode {episode} (seed={seed:#x}, elapsed={:?})",
1693 start_time.elapsed()
1694 );
1695 }
1696
1697 let reclaim_size = if rng.random_bool(0.5) { 32768 } else { 65536 };
1698 let device = DeviceHolder::new(FakeDevice::new(4096, 4096));
1699 let mut fs = FxFilesystemBuilder::new()
1700 .format(true)
1701 .journal_options(JournalOptions { reclaim_size, ..Default::default() })
1702 .open(device)
1703 .await
1704 .expect("open failed");
1705
1706 let mut vols = Vec::new();
1707 {
1708 let root_vol = root_volume(fs.clone()).await.expect("root_volume failed");
1709 for i in 0..4 {
1710 let encrypted = i < 2;
1711 let crypt: Option<Arc<dyn Crypt>> =
1712 encrypted.then(|| Arc::new(new_insecure_crypt()) as _);
1713 let name = format!("vol_{i}");
1714 let store = root_vol
1715 .new_volume(
1716 &name,
1717 NewChildStoreOptions {
1718 options: StoreOptions {
1719 crypt: crypt.clone(),
1720 ..StoreOptions::default()
1721 },
1722 ..NewChildStoreOptions::default()
1723 },
1724 )
1725 .await
1726 .expect("new_volume failed");
1727 let store_id = store.store_object_id();
1728 let root_dir_id = store.root_directory_object_id();
1729
1730 let base_count = if i == 0 || i == 2 { 1400 } else { 0 };
1734 for batch in 0..(base_count / 50) {
1735 let mut txn = store
1736 .new_transaction(
1737 lock_keys![LockKey::object(store_id, root_dir_id)],
1738 Options::default(),
1739 )
1740 .await
1741 .expect("new_transaction failed");
1742 for j in 0..50 {
1743 let idx = batch * 50 + j;
1744 txn.add(
1745 store_id,
1746 Mutation::replace_or_insert_object(
1747 ObjectKey::extended_attribute(
1748 root_dir_id,
1749 format!("churn_{idx:04}_gen_0_{:<190}", idx).into_bytes(),
1750 ),
1751 ObjectValue::inline_extended_attribute(vec![0x55; 200]),
1752 ),
1753 );
1754 }
1755 txn.commit().await.expect("commit failed");
1756 }
1757 if base_count > 0 {
1758 store
1759 .flush_with_reason(FlushReason::Journal(ForceMajor::True))
1760 .await
1761 .expect("initial major flush failed");
1762 }
1763
1764 vols.push(VolState {
1765 name,
1766 store_id,
1767 crypt,
1768 permanent: true,
1769 churn_cursor: 0,
1770 churn_gen: vec![0; base_count],
1771 });
1772 }
1773 fs.journal().force_compact().await.expect("force_compact failed");
1774 fs.journal().force_compact().await.expect("force_compact failed");
1775
1776 let store_3 = fs.object_manager().store(vols[3].store_id).unwrap();
1781 let root_dir_3 = Directory::open(&store_3, store_3.root_directory_object_id())
1782 .await
1783 .expect("open root_dir_3 failed");
1784 let filler = {
1785 let mut txn = store_3
1786 .new_transaction(
1787 lock_keys![LockKey::object(vols[3].store_id, root_dir_3.object_id())],
1788 Options::default(),
1789 )
1790 .await
1791 .expect("new_transaction failed");
1792 let file = root_dir_3
1793 .create_child_file(&mut txn, "filler")
1794 .await
1795 .expect("create filler failed");
1796 txn.commit().await.expect("commit filler failed");
1797 file
1798 };
1799 let chunk = filler.allocate_buffer(65536).await;
1800 let mut offset = 0;
1801 while fs
1802 .allocator()
1803 .get_disk_bytes()
1804 .saturating_sub(fs.allocator().get_used_bytes().0)
1805 > 1280 * 1024
1806 {
1807 filler
1808 .write_or_append(Some(offset), chunk.as_ref())
1809 .await
1810 .expect("write filler failed");
1811 offset += 65536;
1812 }
1813 fs.journal().force_compact().await.expect("force_compact failed");
1814 fs.sync(SyncOptions { flush_device: true, ..Default::default() })
1815 .await
1816 .expect("sync failed");
1817 }
1818
1819 let mut next_id = 0u64;
1820 for _step in 0..steps_per_episode {
1821 if stress_secs > 0 && start_time.elapsed().as_secs() >= stress_secs {
1822 break;
1823 }
1824 next_id += 1;
1825 let op = rng.random_range(0..100);
1826
1827 if op < 18 {
1828 let store = pick_unlocked_volume(&fs, &vols, &mut rng);
1831 let existing = list_dir_files(&store).await;
1832 if existing.len() >= 3 {
1833 let (ref old_name, old_oid) = existing[0];
1834 unlink_and_tombstone_file(&fs, &store, old_name, old_oid, true).await;
1835 }
1836 fs.graveyard().flush().await;
1837 let root_dir = Directory::open(&store, store.root_directory_object_id())
1838 .await
1839 .expect("open failed");
1840 let name = format!("f_{next_id}");
1841 let mut txn = store
1842 .new_transaction(
1843 lock_keys![LockKey::object(
1844 store.store_object_id(),
1845 root_dir.object_id()
1846 )],
1847 Options::default(),
1848 )
1849 .await
1850 .unwrap_or_else(|e| panic!("new_transaction failed (seed={seed:#x}): {e}"));
1851 let handle = root_dir
1852 .create_child_file(&mut txn, &name)
1853 .await
1854 .expect("create_child_file failed");
1855 txn.commit().await.expect("commit create_child_file failed");
1856 let blocks = rng.random_range(1..=2);
1857 let mut buf = handle.allocate_buffer(blocks * 4096).await;
1858 buf.fill((next_id & 0xff) as u8);
1859 handle
1860 .write_or_append(Some(0), buf.as_ref())
1861 .await
1862 .unwrap_or_else(|e| panic!("write_or_append failed (seed={seed:#x}): {e}"));
1863 } else if op < 32 {
1864 let store = pick_unlocked_volume(&fs, &vols, &mut rng);
1866 let files = list_dir_files(&store).await;
1867 if files.is_empty() {
1868 continue;
1869 }
1870 let (_, oid) = files[rng.random_range(0..files.len())];
1871 let handle =
1872 ObjectStore::open_object(&store, oid, HandleOptions::default(), None)
1873 .await
1874 .expect("open_object failed");
1875 if rng.random_bool(0.5) {
1876 if rng.random_bool(0.5) {
1877 handle
1878 .truncate_with_options(
1879 Options {
1880 reservation: ReservationOptions::Borrowed,
1881 ..Default::default()
1882 },
1883 0,
1884 )
1885 .await
1886 .expect("borrowed truncate failed");
1887 } else {
1888 let new_size = rng.random_range(0..=4096);
1889 handle.truncate(new_size).await.expect("truncate failed");
1890 }
1891 } else {
1892 let mut buf = handle.allocate_buffer(4096).await;
1893 buf.fill(0x5a);
1894 let offset = rng.random_range(0..=1) * 4096;
1895 handle
1896 .write_or_append(Some(offset), buf.as_ref())
1897 .await
1898 .expect("write_or_append failed");
1899 }
1900 } else if op < 40 {
1901 let store = pick_unlocked_volume(&fs, &vols, &mut rng);
1903 let files = list_dir_files(&store).await;
1904 if files.is_empty() {
1905 continue;
1906 }
1907 let (ref fname, oid) = files[rng.random_range(0..files.len())];
1908 let immediate = rng.random_bool(0.7);
1909 unlink_and_tombstone_file(&fs, &store, fname, oid, immediate).await;
1910 } else if op < 54 {
1911 let store = pick_unlocked_volume(&fs, &vols, &mut rng);
1913 let store_id = store.store_object_id();
1914 let root_dir_id = store.root_directory_object_id();
1915 let attr_idx = rng.random_range(0..32);
1916 let key_name = format!("xattr_{attr_idx:02}_{:<120}", attr_idx).into_bytes();
1917 if rng.random_bool(0.45) {
1918 let mut txn = store
1919 .new_transaction(
1920 lock_keys![LockKey::object(store_id, root_dir_id)],
1921 Options::default(),
1922 )
1923 .await
1924 .expect("funded xattr txn failed");
1925 let len = rng.random_range(64..=256);
1926 txn.add(
1927 store_id,
1928 Mutation::replace_or_insert_object(
1929 ObjectKey::extended_attribute(root_dir_id, key_name),
1930 ObjectValue::inline_extended_attribute(vec![0x77; len]),
1931 ),
1932 );
1933 txn.commit().await.expect("funded xattr commit failed");
1934 } else {
1935 let mut txn = store
1936 .new_transaction(
1937 lock_keys![LockKey::object(store_id, root_dir_id)],
1938 Options {
1939 skip_journal_checks: true,
1940 reservation: ReservationOptions::Borrowed,
1941 ..Default::default()
1942 },
1943 )
1944 .await
1945 .expect("borrowed txn failed");
1946 txn.add(
1947 store_id,
1948 Mutation::replace_or_insert_object(
1949 ObjectKey::extended_attribute(root_dir_id, key_name),
1950 ObjectValue::None,
1951 ),
1952 );
1953 txn.commit().await.expect("borrowed commit failed");
1954 }
1955 } else if op < 74 {
1956 let candidates: Vec<usize> = (0..vols.len())
1963 .filter(|&i| {
1964 !vols[i].churn_gen.is_empty()
1965 && fs
1966 .object_manager()
1967 .store(vols[i].store_id)
1968 .is_some_and(|s| !s.is_locked())
1969 })
1970 .collect();
1971 let idx = candidates[rng.random_range(0..candidates.len())];
1972 let store = fs.object_manager().store(vols[idx].store_id).unwrap();
1973 let store_id = store.store_object_id();
1974 let root_dir_id = store.root_directory_object_id();
1975
1976 let churn_len = vols[idx].churn_gen.len();
1980 for _ in 0..2 {
1981 let start = vols[idx].churn_cursor;
1982 vols[idx].churn_cursor = (start + 25) % churn_len;
1983
1984 let mut del_txn = store
1985 .new_transaction(
1986 lock_keys![LockKey::object(store_id, root_dir_id)],
1987 Options {
1988 skip_journal_checks: true,
1989 reservation: ReservationOptions::Borrowed,
1990 ..Default::default()
1991 },
1992 )
1993 .await
1994 .expect("borrowed base delete txn failed");
1995 for offset in 0..25 {
1996 let j = (start + offset) % churn_len;
1997 let old_gen = vols[idx].churn_gen[j];
1998 del_txn.add(
1999 store_id,
2000 Mutation::replace_or_insert_object(
2001 ObjectKey::extended_attribute(
2002 root_dir_id,
2003 format!("churn_{j:04}_gen_{old_gen}_{:<190}", j)
2004 .into_bytes(),
2005 ),
2006 ObjectValue::None,
2007 ),
2008 );
2009 }
2010 del_txn.commit().await.expect("borrowed base delete commit failed");
2011
2012 let mut ins_txn = store
2013 .new_transaction(
2014 lock_keys![LockKey::object(store_id, root_dir_id)],
2015 Options::default(),
2016 )
2017 .await
2018 .unwrap_or_else(|e| {
2019 panic!("funded base replacement txn failed (seed={seed:#x}): {e}")
2020 });
2021 for offset in 0..25 {
2022 let j = (start + offset) % churn_len;
2023 let new_gen = vols[idx].churn_gen[j] + 1;
2024 vols[idx].churn_gen[j] = new_gen;
2025 ins_txn.add(
2026 store_id,
2027 Mutation::replace_or_insert_object(
2028 ObjectKey::extended_attribute(
2029 root_dir_id,
2030 format!("churn_{j:04}_gen_{new_gen}_{:<190}", j)
2031 .into_bytes(),
2032 ),
2033 ObjectValue::inline_extended_attribute(vec![0x66; 200]),
2034 ),
2035 );
2036 }
2037 ins_txn.commit().await.expect("funded base replacement commit failed");
2038 }
2039 } else if op < 84 {
2040 let encrypted: Vec<usize> =
2043 (0..vols.len()).filter(|&i| vols[i].crypt.is_some()).collect();
2044 let idx = encrypted[rng.random_range(0..encrypted.len())];
2045 let crypt = vols[idx].crypt.clone().unwrap();
2046 let store = fs.object_manager().store(vols[idx].store_id).unwrap();
2047 if store.is_locked() {
2048 match rng.random_range(0..3) {
2049 0 => {
2050 store.flush().await.expect("locked flush failed");
2051 }
2052 1 => {
2053 store
2054 .unlock_read_only(crypt)
2055 .await
2056 .expect("unlock_read_only failed");
2057 store.lock_read_only();
2058 }
2059 _ => {
2060 store.unlock(crypt).await.expect("unlock failed");
2061 }
2062 }
2063 } else {
2064 fs.graveyard().flush().await;
2065 store.lock().await.expect("lock failed");
2066 if rng.random_bool(0.5) {
2067 store.flush().await.expect("locked flush failed");
2068 }
2069 }
2070 } else if op < 91 {
2071 fs.journal().pause_compactions().await;
2075 let idx = rng.random_range(0..vols.len());
2076 let store = fs.object_manager().store(vols[idx].store_id).unwrap();
2077 match rng.random_range(0..3) {
2078 0 => {
2079 store
2080 .flush_with_reason(FlushReason::Journal(ForceMajor::False))
2081 .await
2082 .expect("minor flush failed");
2083 }
2084 1 => {
2085 fs.object_manager()
2086 .flush(FlushReason::Journal(ForceMajor::False))
2087 .await
2088 .expect("object_manager flush failed");
2089 }
2090 _ => {
2091 fs.journal().force_compact().await.expect("force_compact failed");
2092 }
2093 }
2094 fs.journal().resume_compactions();
2095 } else if op < 95 {
2096 let root_vol = root_volume(fs.clone()).await.expect("root_volume failed");
2099 let dynamic_indices: Vec<usize> =
2100 (0..vols.len()).filter(|&i| !vols[i].permanent).collect();
2101 if dynamic_indices.is_empty() {
2102 let encrypted = rng.random_bool(0.5);
2103 let crypt: Option<Arc<dyn Crypt>> =
2104 encrypted.then(|| Arc::new(new_insecure_crypt()) as _);
2105 let name = format!("dyn_{next_id}");
2106 let store = root_vol
2107 .new_volume(
2108 &name,
2109 NewChildStoreOptions {
2110 options: StoreOptions {
2111 crypt: crypt.clone(),
2112 ..StoreOptions::default()
2113 },
2114 ..NewChildStoreOptions::default()
2115 },
2116 )
2117 .await
2118 .expect("dynamic new_volume failed");
2119 let store_id = store.store_object_id();
2120 let root_dir_id = store.root_directory_object_id();
2121 let mut txn = store
2122 .new_transaction(
2123 lock_keys![LockKey::object(store_id, root_dir_id)],
2124 Options::default(),
2125 )
2126 .await
2127 .expect("dynamic volume txn failed");
2128 txn.add(
2129 store_id,
2130 Mutation::replace_or_insert_object(
2131 ObjectKey::extended_attribute(root_dir_id, b"dyn_attr".to_vec()),
2132 ObjectValue::inline_extended_attribute(vec![0x33; 256]),
2133 ),
2134 );
2135 txn.commit().await.expect("dynamic volume commit failed");
2136 vols.push(VolState {
2137 name,
2138 store_id,
2139 crypt,
2140 permanent: false,
2141 churn_cursor: 0,
2142 churn_gen: Vec::new(),
2143 });
2144 } else {
2145 fs.graveyard().flush().await;
2146 let idx = dynamic_indices[rng.random_range(0..dynamic_indices.len())];
2147 let vol = vols.remove(idx);
2148 let (_, delete_txn) = root_vol
2149 .acquire_transaction_for_remove_volume(
2150 &vol.name,
2151 [],
2152 true,
2153 Options {
2154 reservation: ReservationOptions::Borrowed,
2155 ..Default::default()
2156 },
2157 )
2158 .await
2159 .expect("acquire_transaction_for_remove_volume failed");
2160 root_vol
2161 .delete_volume(&vol.name, delete_txn, || {})
2162 .await
2163 .expect("delete_volume failed");
2164 assert!(fs.object_manager().store_borrowed(vol.store_id).is_none());
2165 }
2166 } else {
2167 fs.graveyard().flush().await;
2169 let remount_kind = rng.random_range(0..3);
2170 if remount_kind == 0 {
2171 fs.journal().pause_compactions().await;
2173 fs.sync(SyncOptions::default()).await.expect("sync failed");
2174 let snapshot = fs.device().snapshot().expect("snapshot failed");
2175 fs.journal().resume_compactions();
2176 snapshot
2177 .discard_random_since_last_flush()
2178 .expect("discard_random_since_last_flush failed");
2179 let pf_fs = FxFilesystem::open(snapshot).await.expect("open pf_fs failed");
2180 check_all_volumes(&pf_fs, &vols).await;
2181 pf_fs.close().await.expect("close pf_fs failed");
2182 } else {
2183 if remount_kind == 1 {
2184 fs.journal().stop_compactions().await;
2188 fs.sync(SyncOptions { flush_device: true, ..Default::default() })
2189 .await
2190 .expect("sync failed");
2191 let snapshot = fs.device().snapshot().expect("snapshot failed");
2192 let expected_reserved = fs.object_manager().reserved_metadata_space();
2193 fs.close().await.expect("close old fs failed");
2194 fs = FxFilesystemBuilder::new()
2195 .journal_options(JournalOptions {
2196 reclaim_size,
2197 ..Default::default()
2198 })
2199 .open(snapshot)
2200 .await
2201 .expect("open snapshot failed");
2202 assert_eq!(
2203 fs.object_manager().reserved_metadata_space(),
2204 expected_reserved,
2205 "replayed reserved_metadata_space mismatch on snapshot remount \
2206 (seed={seed:#x})"
2207 );
2208 } else {
2209 fs.close().await.expect("close failed");
2211 let device = fs.take_device().await;
2212 device.reopen(false);
2213 fs = FxFilesystemBuilder::new()
2214 .journal_options(JournalOptions {
2215 reclaim_size,
2216 ..Default::default()
2217 })
2218 .open(device)
2219 .await
2220 .expect("open failed");
2221 }
2222 check_all_volumes(&fs, &vols).await;
2223 for vol in &vols {
2224 if let Some(crypt) = &vol.crypt
2225 && rng.random_bool(0.5)
2226 {
2227 let store = fs.object_manager().store(vol.store_id).unwrap();
2228 store.unlock(crypt.clone()).await.expect("unlock failed");
2229 }
2230 }
2231 }
2232 }
2233 }
2234
2235 fs.graveyard().flush().await;
2237 fs.journal().pause_compactions().await;
2238 for vol in &vols {
2239 if let Some(crypt) = &vol.crypt {
2240 let store = fs.object_manager().store(vol.store_id).unwrap();
2241 if store.is_locked() {
2242 store.unlock(crypt.clone()).await.expect("unlock failed");
2243 }
2244 }
2245 }
2246 fs.object_manager()
2247 .flush(FlushReason::Journal(ForceMajor::True))
2248 .await
2249 .expect("full flush failed");
2250 fs.journal().force_compact().await.expect("force_compact failed");
2251 fs.journal().force_compact().await.expect("force_compact failed");
2252 let mut layers_before = Vec::with_capacity(vols.len());
2253 for vol in &vols {
2254 let store = fs.object_manager().store(vol.store_id).unwrap();
2255 let info = store.load_store_info().await.unwrap();
2256 assert!(info.layers.len() <= 1);
2257 assert_eq!(info.encrypted_mutations_object_id, INVALID_OBJECT_ID);
2258 let (b, _) = fs.object_manager().store_borrowed(vol.store_id).unwrap();
2259 assert_eq!(b, 0);
2260 layers_before.push((vol.store_id, info.layers));
2261 }
2262 fs.object_manager()
2263 .flush(FlushReason::Journal(ForceMajor::False))
2264 .await
2265 .expect("noop flush failed");
2266 for (store_id, expected_layers) in layers_before {
2267 let store = fs.object_manager().store(store_id).unwrap();
2268 assert_eq!(store.load_store_info().await.unwrap().layers, expected_layers);
2269 }
2270 check_all_volumes(&fs, &vols).await;
2271 fs.close().await.expect("close failed");
2272
2273 episode += 1;
2274 if stress_secs == 0 || start_time.elapsed().as_secs() >= stress_secs {
2275 break;
2276 }
2277 }
2278 }
2279
2280 #[fuchsia::test]
2281 async fn test_required_reservation_earliest_offset_saturating_sub() {
2282 struct CheckRequiredReservation;
2283 impl AssociatedObject for CheckRequiredReservation {
2284 fn will_apply_mutation(
2285 &self,
2286 _mutation: &Mutation,
2287 _object_id: u64,
2288 manager: &ObjectManager,
2289 ) {
2290 let earliest_offset = manager.inner.read().earliest_journal_offset().unwrap();
2291 assert!(earliest_offset > manager.last_end_offset());
2292 let _ = manager.borrowed_metadata_space();
2293 }
2294 }
2295
2296 let device = DeviceHolder::new(FakeDevice::new(8192, 512));
2297 let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
2298
2299 fs.journal().force_compact().await.expect("compact");
2300 let allocator_id = fs.allocator().object_id();
2301 assert!(!fs.object_manager().needs_flush(fs.root_store().store_object_id()));
2302
2303 let txn_opts = Options {
2306 skip_journal_checks: true,
2307 reservation: ReservationOptions::Borrowed,
2308 ..Default::default()
2309 };
2310 for mutation in [Mutation::BeginFlush, Mutation::EndFlush(0)] {
2311 let mut transaction =
2312 fs.root_store().new_transaction(lock_keys![], txn_opts).await.expect("new_txn");
2313 transaction.add(allocator_id, mutation);
2314 transaction.commit().await.expect("commit");
2315 }
2316 assert!(
2317 !fs.object_manager().needs_flush(allocator_id),
2318 "journal_checkpoints should now be completely empty"
2319 );
2320
2321 let checkpoint_before_pad =
2326 fs.journal().sync(SyncOptions::default()).await.expect("sync").unwrap();
2327 let next_block_offset = checkpoint_before_pad.file_offset.next_multiple_of(4096);
2328 assert!(
2329 !fs.object_manager().needs_borrow_for_journal(next_block_offset + 8192),
2330 "needs_borrow_for_journal should return false when journal_checkpoints is empty"
2331 );
2332
2333 let reserved_before = fs.object_manager().metadata_reservation().amount();
2334 let borrowed_before = fs.object_manager().borrowed_metadata_space();
2335 let required_before = reserved_before + borrowed_before;
2336
2337 let check_obj = CheckRequiredReservation;
2341 let mut transaction =
2342 fs.root_store().new_transaction(lock_keys![], Options::default()).await.expect("txn");
2343 let handle1 = ObjectStore::create_object(
2344 &fs.root_store(),
2345 &mut transaction,
2346 HandleOptions::default(),
2347 None,
2348 )
2349 .await
2350 .expect("create_object");
2351 let handle2 = ObjectStore::create_object(
2352 &fs.root_store(),
2353 &mut transaction,
2354 HandleOptions::default(),
2355 None,
2356 )
2357 .await
2358 .expect("create_object");
2359 let second_id = std::cmp::max(handle1.object_id(), handle2.object_id());
2360 transaction.add_with_object(
2361 fs.root_store().store_object_id(),
2362 Mutation::merge_object(ObjectKey::object(second_id), ObjectValue::None),
2363 AssocObj::Borrowed(&check_obj),
2364 );
2365 transaction.commit().await.expect("commit");
2366
2367 let reserved_after = fs.object_manager().metadata_reservation().amount();
2368 let borrowed_after = fs.object_manager().borrowed_metadata_space();
2369 let required_after = reserved_after + borrowed_after;
2370 assert_eq!(
2371 reserved_after - reserved_before,
2372 required_after - required_before,
2373 "Non-borrowed transaction should increase metadata_reservation by the exact increase in required_reservation"
2374 );
2375
2376 fs.close().await.expect("close failed");
2377 }
2378
2379 #[fuchsia::test]
2380 async fn test_sync_padding_paid_by_next_transaction() {
2381 let device = DeviceHolder::new(FakeDevice::new(8192, 512));
2382 let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
2383 fs.journal().force_compact().await.expect("compact");
2384
2385 let mut transaction =
2388 fs.root_store().new_transaction(lock_keys![], Options::default()).await.expect("txn");
2389 let handle = ObjectStore::create_object(
2390 &fs.root_store(),
2391 &mut transaction,
2392 HandleOptions::default(),
2393 None,
2394 )
2395 .await
2396 .expect("create_object");
2397 transaction.commit().await.expect("commit");
2398 let borrowed_before = fs.object_manager().borrowed_metadata_space();
2399
2400 let checkpoint_before_pad = fs
2404 .journal()
2405 .sync(SyncOptions { flush_device: true, ..Default::default() })
2406 .await
2407 .expect("sync")
2408 .unwrap();
2409 let padded_bytes = checkpoint_before_pad.file_offset.next_multiple_of(4096)
2410 - checkpoint_before_pad.file_offset;
2411 assert!(padded_bytes > 256, "expected > 256 bytes of block padding, got {padded_bytes}");
2412
2413 let mut transaction = fs
2414 .root_store()
2415 .new_transaction(
2416 lock_keys![LockKey::object(fs.root_store().store_object_id(), handle.object_id())],
2417 Options::default(),
2418 )
2419 .await
2420 .expect("txn");
2421 transaction.add(
2422 fs.root_store().store_object_id(),
2423 Mutation::merge_object(ObjectKey::object(handle.object_id()), ObjectValue::None),
2424 );
2425 transaction.commit().await.expect("commit");
2426
2427 let borrowed_after = fs.object_manager().borrowed_metadata_space();
2428 assert_eq!(
2429 borrowed_after, borrowed_before,
2430 "Funded transaction after sync() should pay for block padding rather than borrowing from metadata_reservation"
2431 );
2432
2433 fs.close().await.expect("close failed");
2434 }
2435}