Skip to content

Commit

Permalink
TransactionMut::force_gc
Browse files Browse the repository at this point in the history
  • Loading branch information
Horusiath committed Oct 9, 2024
1 parent e0a2207 commit c179f9b
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 6 deletions.
2 changes: 1 addition & 1 deletion yrs/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ impl BlockCell {

pub fn len(&self) -> u32 {
match self {
BlockCell::GC(gc) => gc.end - gc.start,
BlockCell::GC(gc) => gc.end - gc.start + 1,
BlockCell::Block(block) => block.len(),
}
}
Expand Down
20 changes: 20 additions & 0 deletions yrs/src/block_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ impl ClientBlockList {
ClientBlockListIter(self.list.iter())
}

pub fn iter_mut(&mut self) -> ClientBlockListIterMut<'_> {
ClientBlockListIterMut(self.list.iter_mut())
}

/// Attempts to squash block at a given `index` with a corresponding block on its left side.
/// If this succeeds, block under a given `index` will be removed, and its contents will be
/// squashed into its left neighbor. In such case a squash result will be returned in order to
Expand Down Expand Up @@ -183,6 +187,16 @@ impl<'a> Iterator for ClientBlockListIter<'a> {
}
}

pub(crate) struct ClientBlockListIterMut<'a>(std::slice::IterMut<'a, BlockCell>);

impl<'a> Iterator for ClientBlockListIterMut<'a> {
type Item = &'a mut BlockCell;

fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}

/// Block store is a collection of all blocks known to a document owning instance of this type.
/// Blocks are organized per client ID and contain a resizable list of all blocks inserted by that
/// client.
Expand All @@ -192,6 +206,7 @@ pub(crate) struct BlockStore {
}

pub(crate) type Iter<'a> = std::collections::hash_map::Iter<'a, ClientID, ClientBlockList>;
pub(crate) type IterMut<'a> = std::collections::hash_map::IterMut<'a, ClientID, ClientBlockList>;

impl BlockStore {
/// Checks if block store is empty. Empty block store doesn't contain any blocks, neither active
Expand Down Expand Up @@ -242,6 +257,11 @@ impl BlockStore {
self.clients.iter()
}

/// Returns an iterator over the client and mutable block lists pairs known to a current block store.
pub fn iter_mut(&mut self) -> IterMut<'_> {
self.clients.iter_mut()
}

/// Returns a state vector, which is a compact representation of the state of blocks integrated
/// into a current block store. This state vector can later be encoded and send to a remote
/// peers in order to calculate differences between two stored and produce a compact update,
Expand Down
50 changes: 46 additions & 4 deletions yrs/src/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,17 +1034,19 @@ impl DocAddr {

#[cfg(test)]
mod test {
use crate::block::ItemContent;
use crate::block::{BlockCell, ItemContent, GC};
use crate::branch::{Branch, BranchPtr};
use crate::test_utils::exchange_updates;
use crate::transaction::{ReadTxn, TransactionMut};
use crate::types::ToJson;
use crate::update::Update;
use crate::updates::decoder::Decode;
use crate::updates::encoder::{Encode, Encoder, EncoderV1};
use crate::{
any, Any, Array, ArrayPrelim, ArrayRef, DeleteSet, Doc, GetString, Map, MapPrelim, MapRef,
OffsetKind, Options, StateVector, Subscription, Text, TextRef, Transact, Uuid, WriteTxn,
XmlElementPrelim, XmlFragment, XmlFragmentRef, XmlTextPrelim, XmlTextRef, ID,
any, Any, Array, ArrayPrelim, ArrayRef, BranchID, DeleteSet, Doc, GetString, Map,
MapPrelim, MapRef, OffsetKind, Options, SharedRef, StateVector, Subscription, Text,
TextPrelim, TextRef, Transact, Uuid, WriteTxn, XmlElementPrelim, XmlFragment,
XmlFragmentRef, XmlTextPrelim, XmlTextRef, ID,
};
use arc_swap::ArcSwapOption;
use assert_matches2::assert_matches;
Expand Down Expand Up @@ -2441,4 +2443,44 @@ mod test {
let actual = e.swap(None);
assert!(actual.is_none());
}

#[test]
fn force_gc() {
let doc = Doc::with_options(Options {
client_id: 1,
skip_gc: true,
..Default::default()
});
let map = doc.get_or_insert_map("map");

{
// create some initial data
let mut txn = doc.transact_mut();
let txt = map.insert(&mut txn, "text", TextPrelim::default()); // <1#0>
txt.insert(&mut txn, 0, "abc"); // <1#1..=3>

// drop nested type
map.remove(&mut txn, "text");
}

// verify that skip_gc works and we have access to an original text content
let mut txn = doc.transact_mut();
let block = txn
.store()
.blocks
.get_block(&ID::new(1, 1))
.unwrap()
.as_item()
.unwrap();
assert!(block.is_deleted(), "`abc` should be marked as deleted");
assert_eq!(&block.content, &ItemContent::String("abc".into()));

// force GC and check if original content is hard deleted
txn.force_gc();

let block = txn.store().blocks.get_block(&ID::new(1, 1)).unwrap();
assert!(block.is_deleted(), "`abc` should be deleted");
assert_matches!(&block, &BlockCell::GC(_));
assert_eq!(block.len(), 3);
}
}
23 changes: 22 additions & 1 deletion yrs/src/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,22 @@ pub(crate) struct GCCollector {
}

impl GCCollector {
/// Garbage collect all blocks deleted within current transaction scope.
pub fn collect(txn: &mut TransactionMut) {
let mut gc = Self::default();
gc.mark_in_scope(txn);
gc.collect_all_marked(txn);
}

/// Garbage collect all deleted blocks from current transaction's document store.
pub fn collect_all(txn: &mut TransactionMut) {
let mut gc = Self::default();
gc.mark_all(txn);
gc.collect_all_marked(txn);
}

fn mark_all(&mut self, txn: &mut TransactionMut) {
/// Mark deleted items based on a current transaction delete set.
fn mark_in_scope(&mut self, txn: &mut TransactionMut) {
for (client, range) in txn.delete_set.iter() {
if let Some(blocks) = txn.store.blocks.get_client_mut(client) {
for delete_item in range.iter().rev() {
Expand All @@ -39,6 +48,18 @@ impl GCCollector {
}
}

fn mark_all(&mut self, txn: &mut TransactionMut) {
for (_, client_blocks) in txn.store.blocks.iter_mut() {
for block in client_blocks.iter_mut() {
if let BlockCell::Block(item) = block {
if item.is_deleted() {
item.gc(self, false);
}
}
}
}
}

/// Marks item with a given [ID] as a candidate for being GCed.
pub(crate) fn mark(&mut self, id: &ID) {
let client = self.items.entry(id.client).or_default();
Expand Down
7 changes: 7 additions & 0 deletions yrs/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,13 @@ impl<'doc> TransactionMut<'doc> {
}
}

/// Perform garbage collection of deleted blocks, even if a document was created with `skip_gc`
/// option. This operation will scan over ALL deleted elements, NOT ONLY the ones that have been
/// changed as part of this transaction scope.
pub fn force_gc(&mut self) {
GCCollector::collect_all(self);
}

pub(crate) fn add_changed_type(&mut self, parent: BranchPtr, parent_sub: Option<Arc<str>>) {
let trigger = if let Some(ptr) = parent.item {
(ptr.id().clock < self.before_state.get(&ptr.id().client)) && !ptr.is_deleted()
Expand Down

0 comments on commit c179f9b

Please sign in to comment.