Skip to content

Commit 3895f75

Browse files
committed
fix(sdk): Do not always remove empty chunks from LinkedChunk.
This patch introduces `EmptyChunk`, a new enum used to represent whether empty chunks must be removed/unlink or kept from the `LinkedChunk`. It is used by `LinkedChunk::remove_item_at`. Why is it important? For example, imagine the following situation: - one inserts a single event in a new chunk (possible if a (sliding) sync is done with `timeline_limit=1`), - one inserts many events at the position of the previous event, with one of the new events being a duplicate of the first event (possible if a (sliding) sync is done with `timeline_limit=10` this time), - prior to this patch, the older event was removed, resulting in an empty chunk, which was removed from the `LinkedChunk`, invalidating the insertion position! So, with this patch: - `RoomEvents::remove_events` does remove empty chunks, but - `RoomEvents::remove_events_and_update_insert_position` does NOT remove empty chunks, they are kept in case the position wants to insert in this same chunk.
1 parent 04275d7 commit 3895f75

File tree

3 files changed

+264
-36
lines changed

3 files changed

+264
-36
lines changed

crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,10 @@ mod tests {
452452

453453
use imbl::{vector, Vector};
454454

455-
use super::{super::LinkedChunk, VectorDiff};
455+
use super::{
456+
super::{EmptyChunk, LinkedChunk},
457+
VectorDiff,
458+
};
456459

457460
fn apply_and_assert_eq<Item>(
458461
accumulator: &mut Vector<Item>,
@@ -614,7 +617,10 @@ mod tests {
614617
);
615618

616619
let removed_item = linked_chunk
617-
.remove_item_at(linked_chunk.item_position(|item| *item == 'c').unwrap())
620+
.remove_item_at(
621+
linked_chunk.item_position(|item| *item == 'c').unwrap(),
622+
EmptyChunk::Remove,
623+
)
618624
.unwrap();
619625
assert_eq!(removed_item, 'c');
620626
assert_items_eq!(
@@ -634,7 +640,10 @@ mod tests {
634640
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 7 }]);
635641

636642
let removed_item = linked_chunk
637-
.remove_item_at(linked_chunk.item_position(|item| *item == 'z').unwrap())
643+
.remove_item_at(
644+
linked_chunk.item_position(|item| *item == 'z').unwrap(),
645+
EmptyChunk::Remove,
646+
)
638647
.unwrap();
639648
assert_eq!(removed_item, 'z');
640649
assert_items_eq!(
@@ -773,7 +782,7 @@ mod tests {
773782
continue;
774783
};
775784

776-
linked_chunk.remove_item_at(position).expect("Failed to remove an item");
785+
linked_chunk.remove_item_at(position, EmptyChunk::Remove).expect("Failed to remove an item");
777786
}
778787
}
779788
}

crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs

Lines changed: 151 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,18 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
408408

409409
/// Remove item at a specified position in the [`LinkedChunk`].
410410
///
411-
/// Because the `position` can be invalid, this method returns a
412-
/// `Result`.
413-
pub fn remove_item_at(&mut self, position: Position) -> Result<Item, Error> {
411+
/// `position` must point to a valid item, otherwise the method returns
412+
/// `Err`.
413+
///
414+
/// The chunk containing the item represented by `position` may be empty
415+
/// once the item has been removed. In this case, the chunk can be removed
416+
/// if `empty_chunk` contains [`EmptyChunk::Remove`], otherwise the chunk is
417+
/// kept if `empty_chunk` contains [`EmptyChunk::Keep`].
418+
pub fn remove_item_at(
419+
&mut self,
420+
position: Position,
421+
empty_chunk: EmptyChunk,
422+
) -> Result<Item, Error> {
414423
let chunk_identifier = position.chunk_identifier();
415424
let item_index = position.index();
416425

@@ -446,9 +455,9 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
446455
}
447456
};
448457

449-
// If the `chunk` can be unlinked, and if the `chunk` is not the first one, we
450-
// can remove it.
451-
if can_unlink_chunk && chunk.is_first_chunk().not() {
458+
// If removing empty chunk is desired, and if the `chunk` can be unlinked, and
459+
// if the `chunk` is not the first one, we can remove it.
460+
if empty_chunk.remove() && can_unlink_chunk && chunk.is_first_chunk().not() {
452461
// Unlink `chunk`.
453462
chunk.unlink(&mut self.updates);
454463

@@ -1336,15 +1345,30 @@ where
13361345
}
13371346
}
13381347

1348+
/// A type representing what to do when the system has to handle an empty chunk.
1349+
pub(crate) enum EmptyChunk {
1350+
/// Keep the empty chunk.
1351+
Keep,
1352+
1353+
/// Remove the empty chunk.
1354+
Remove,
1355+
}
1356+
1357+
impl EmptyChunk {
1358+
fn remove(&self) -> bool {
1359+
matches!(self, Self::Remove)
1360+
}
1361+
}
1362+
13391363
#[cfg(test)]
13401364
mod tests {
13411365
use std::ops::Not;
13421366

13431367
use assert_matches::assert_matches;
13441368

13451369
use super::{
1346-
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Error, LinkedChunk,
1347-
Position,
1370+
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, EmptyChunk, Error,
1371+
LinkedChunk, Position,
13481372
};
13491373

13501374
#[test]
@@ -1950,21 +1974,21 @@ mod tests {
19501974
// that. The chunk is removed.
19511975
{
19521976
let position_of_f = linked_chunk.item_position(|item| *item == 'f').unwrap();
1953-
let removed_item = linked_chunk.remove_item_at(position_of_f)?;
1977+
let removed_item = linked_chunk.remove_item_at(position_of_f, EmptyChunk::Remove)?;
19541978

19551979
assert_eq!(removed_item, 'f');
19561980
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e'] ['g', 'h', 'i'] ['j', 'k']);
19571981
assert_eq!(linked_chunk.len(), 10);
19581982

19591983
let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
1960-
let removed_item = linked_chunk.remove_item_at(position_of_e)?;
1984+
let removed_item = linked_chunk.remove_item_at(position_of_e, EmptyChunk::Remove)?;
19611985

19621986
assert_eq!(removed_item, 'e');
19631987
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d'] ['g', 'h', 'i'] ['j', 'k']);
19641988
assert_eq!(linked_chunk.len(), 9);
19651989

19661990
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
1967-
let removed_item = linked_chunk.remove_item_at(position_of_d)?;
1991+
let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?;
19681992

19691993
assert_eq!(removed_item, 'd');
19701994
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
@@ -1985,19 +2009,19 @@ mod tests {
19852009
// that. The chunk is NOT removed because it's the first chunk.
19862010
{
19872011
let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap();
1988-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2012+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
19892013

19902014
assert_eq!(removed_item, 'a');
19912015
assert_items_eq!(linked_chunk, ['b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
19922016
assert_eq!(linked_chunk.len(), 7);
19932017

1994-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2018+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
19952019

19962020
assert_eq!(removed_item, 'b');
19972021
assert_items_eq!(linked_chunk, ['c'] ['g', 'h', 'i'] ['j', 'k']);
19982022
assert_eq!(linked_chunk.len(), 6);
19992023

2000-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2024+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
20012025

20022026
assert_eq!(removed_item, 'c');
20032027
assert_items_eq!(linked_chunk, [] ['g', 'h', 'i'] ['j', 'k']);
@@ -2017,19 +2041,19 @@ mod tests {
20172041
// that. The chunk is removed.
20182042
{
20192043
let first_position = linked_chunk.item_position(|item| *item == 'g').unwrap();
2020-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2044+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
20212045

20222046
assert_eq!(removed_item, 'g');
20232047
assert_items_eq!(linked_chunk, [] ['h', 'i'] ['j', 'k']);
20242048
assert_eq!(linked_chunk.len(), 4);
20252049

2026-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2050+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
20272051

20282052
assert_eq!(removed_item, 'h');
20292053
assert_items_eq!(linked_chunk, [] ['i'] ['j', 'k']);
20302054
assert_eq!(linked_chunk.len(), 3);
20312055

2032-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2056+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
20332057

20342058
assert_eq!(removed_item, 'i');
20352059
assert_items_eq!(linked_chunk, [] ['j', 'k']);
@@ -2050,15 +2074,15 @@ mod tests {
20502074
// The chunk is removed.
20512075
{
20522076
let position_of_k = linked_chunk.item_position(|item| *item == 'k').unwrap();
2053-
let removed_item = linked_chunk.remove_item_at(position_of_k)?;
2077+
let removed_item = linked_chunk.remove_item_at(position_of_k, EmptyChunk::Remove)?;
20542078

20552079
assert_eq!(removed_item, 'k');
20562080
#[rustfmt::skip]
20572081
assert_items_eq!(linked_chunk, [] ['j']);
20582082
assert_eq!(linked_chunk.len(), 1);
20592083

20602084
let position_of_j = linked_chunk.item_position(|item| *item == 'j').unwrap();
2061-
let removed_item = linked_chunk.remove_item_at(position_of_j)?;
2085+
let removed_item = linked_chunk.remove_item_at(position_of_j, EmptyChunk::Remove)?;
20622086

20632087
assert_eq!(removed_item, 'j');
20642088
assert_items_eq!(linked_chunk, []);
@@ -2092,27 +2116,27 @@ mod tests {
20922116
let _ = linked_chunk.updates().unwrap().take();
20932117

20942118
let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
2095-
let removed_item = linked_chunk.remove_item_at(position_of_c)?;
2119+
let removed_item = linked_chunk.remove_item_at(position_of_c, EmptyChunk::Remove)?;
20962120

20972121
assert_eq!(removed_item, 'c');
20982122
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d']);
20992123
assert_eq!(linked_chunk.len(), 3);
21002124

21012125
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
2102-
let removed_item = linked_chunk.remove_item_at(position_of_d)?;
2126+
let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?;
21032127

21042128
assert_eq!(removed_item, 'd');
21052129
assert_items_eq!(linked_chunk, ['a', 'b'] [-]);
21062130
assert_eq!(linked_chunk.len(), 2);
21072131

21082132
let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap();
2109-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2133+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
21102134

21112135
assert_eq!(removed_item, 'a');
21122136
assert_items_eq!(linked_chunk, ['b'] [-]);
21132137
assert_eq!(linked_chunk.len(), 1);
21142138

2115-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2139+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
21162140

21172141
assert_eq!(removed_item, 'b');
21182142
assert_items_eq!(linked_chunk, [] [-]);
@@ -2134,6 +2158,110 @@ mod tests {
21342158
Ok(())
21352159
}
21362160

2161+
#[test]
2162+
fn test_remove_item_at_and_keep_empty_chunks() -> Result<(), Error> {
2163+
use super::Update::*;
2164+
2165+
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
2166+
linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
2167+
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h']);
2168+
assert_eq!(linked_chunk.len(), 8);
2169+
2170+
// Ignore previous updates.
2171+
let _ = linked_chunk.updates().unwrap().take();
2172+
2173+
// Remove all items from the same chunk. The chunk is empty after that. The
2174+
// chunk is NOT removed because we asked to keep it.
2175+
{
2176+
let position = linked_chunk.item_position(|item| *item == 'd').unwrap();
2177+
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
2178+
2179+
assert_eq!(removed_item, 'd');
2180+
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['e', 'f'] ['g', 'h']);
2181+
assert_eq!(linked_chunk.len(), 7);
2182+
2183+
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
2184+
2185+
assert_eq!(removed_item, 'e');
2186+
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['f'] ['g', 'h']);
2187+
assert_eq!(linked_chunk.len(), 6);
2188+
2189+
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
2190+
2191+
assert_eq!(removed_item, 'f');
2192+
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['g', 'h']);
2193+
assert_eq!(linked_chunk.len(), 5);
2194+
2195+
assert_eq!(
2196+
linked_chunk.updates().unwrap().take(),
2197+
&[
2198+
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
2199+
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
2200+
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
2201+
]
2202+
);
2203+
}
2204+
2205+
// Remove all items from the same chunk. The chunk is empty after that. The
2206+
// chunk is NOT removed because we asked to keep it.
2207+
{
2208+
let position = linked_chunk.item_position(|item| *item == 'g').unwrap();
2209+
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
2210+
2211+
assert_eq!(removed_item, 'g');
2212+
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['h']);
2213+
assert_eq!(linked_chunk.len(), 4);
2214+
2215+
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
2216+
2217+
assert_eq!(removed_item, 'h');
2218+
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] []);
2219+
assert_eq!(linked_chunk.len(), 3);
2220+
2221+
assert_eq!(
2222+
linked_chunk.updates().unwrap().take(),
2223+
&[
2224+
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
2225+
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
2226+
]
2227+
);
2228+
}
2229+
2230+
// Remove all items from the same chunk. The chunk is empty after that. The
2231+
// chunk is NOT removed because we asked to keep it.
2232+
{
2233+
let position = linked_chunk.item_position(|item| *item == 'a').unwrap();
2234+
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
2235+
2236+
assert_eq!(removed_item, 'a');
2237+
assert_items_eq!(linked_chunk, ['b', 'c'] [] []);
2238+
assert_eq!(linked_chunk.len(), 2);
2239+
2240+
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
2241+
2242+
assert_eq!(removed_item, 'b');
2243+
assert_items_eq!(linked_chunk, ['c'] [] []);
2244+
assert_eq!(linked_chunk.len(), 1);
2245+
2246+
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
2247+
2248+
assert_eq!(removed_item, 'c');
2249+
assert_items_eq!(linked_chunk, [] [] []);
2250+
assert_eq!(linked_chunk.len(), 0);
2251+
2252+
assert_eq!(
2253+
linked_chunk.updates().unwrap().take(),
2254+
&[
2255+
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
2256+
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
2257+
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
2258+
]
2259+
);
2260+
}
2261+
2262+
Ok(())
2263+
}
2264+
21372265
#[test]
21382266
fn test_insert_gap_at() -> Result<(), Error> {
21392267
use super::Update::*;

0 commit comments

Comments
 (0)