Skip to content

Commit 6327f98

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 6327f98

File tree

3 files changed

+257
-34
lines changed

3 files changed

+257
-34
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: 144 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,11 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
410410
///
411411
/// Because the `position` can be invalid, this method returns a
412412
/// `Result`.
413-
pub fn remove_item_at(&mut self, position: Position) -> Result<Item, Error> {
413+
pub fn remove_item_at(
414+
&mut self,
415+
position: Position,
416+
empty_chunk: EmptyChunk,
417+
) -> Result<Item, Error> {
414418
let chunk_identifier = position.chunk_identifier();
415419
let item_index = position.index();
416420

@@ -446,9 +450,9 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
446450
}
447451
};
448452

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() {
453+
// If removing empty chunk is desired, and if the `chunk` can be unlinked, and
454+
// if the `chunk` is not the first one, we can remove it.
455+
if empty_chunk.remove() && can_unlink_chunk && chunk.is_first_chunk().not() {
452456
// Unlink `chunk`.
453457
chunk.unlink(&mut self.updates);
454458

@@ -1336,15 +1340,30 @@ where
13361340
}
13371341
}
13381342

1343+
/// A type representing what to do when the system has to handle an empty chunk.
1344+
pub(crate) enum EmptyChunk {
1345+
/// Keep the empty chunk.
1346+
Keep,
1347+
1348+
/// Remove the empty chunk.
1349+
Remove,
1350+
}
1351+
1352+
impl EmptyChunk {
1353+
fn remove(&self) -> bool {
1354+
matches!(self, Self::Remove)
1355+
}
1356+
}
1357+
13391358
#[cfg(test)]
13401359
mod tests {
13411360
use std::ops::Not;
13421361

13431362
use assert_matches::assert_matches;
13441363

13451364
use super::{
1346-
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Error, LinkedChunk,
1347-
Position,
1365+
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, EmptyChunk, Error,
1366+
LinkedChunk, Position,
13481367
};
13491368

13501369
#[test]
@@ -1950,21 +1969,21 @@ mod tests {
19501969
// that. The chunk is removed.
19511970
{
19521971
let position_of_f = linked_chunk.item_position(|item| *item == 'f').unwrap();
1953-
let removed_item = linked_chunk.remove_item_at(position_of_f)?;
1972+
let removed_item = linked_chunk.remove_item_at(position_of_f, EmptyChunk::Remove)?;
19541973

19551974
assert_eq!(removed_item, 'f');
19561975
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e'] ['g', 'h', 'i'] ['j', 'k']);
19571976
assert_eq!(linked_chunk.len(), 10);
19581977

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

19621981
assert_eq!(removed_item, 'e');
19631982
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d'] ['g', 'h', 'i'] ['j', 'k']);
19641983
assert_eq!(linked_chunk.len(), 9);
19651984

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

19691988
assert_eq!(removed_item, 'd');
19701989
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
@@ -1985,19 +2004,19 @@ mod tests {
19852004
// that. The chunk is NOT removed because it's the first chunk.
19862005
{
19872006
let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap();
1988-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2007+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
19892008

19902009
assert_eq!(removed_item, 'a');
19912010
assert_items_eq!(linked_chunk, ['b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
19922011
assert_eq!(linked_chunk.len(), 7);
19932012

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

19962015
assert_eq!(removed_item, 'b');
19972016
assert_items_eq!(linked_chunk, ['c'] ['g', 'h', 'i'] ['j', 'k']);
19982017
assert_eq!(linked_chunk.len(), 6);
19992018

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

20022021
assert_eq!(removed_item, 'c');
20032022
assert_items_eq!(linked_chunk, [] ['g', 'h', 'i'] ['j', 'k']);
@@ -2017,19 +2036,19 @@ mod tests {
20172036
// that. The chunk is removed.
20182037
{
20192038
let first_position = linked_chunk.item_position(|item| *item == 'g').unwrap();
2020-
let removed_item = linked_chunk.remove_item_at(first_position)?;
2039+
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
20212040

20222041
assert_eq!(removed_item, 'g');
20232042
assert_items_eq!(linked_chunk, [] ['h', 'i'] ['j', 'k']);
20242043
assert_eq!(linked_chunk.len(), 4);
20252044

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

20282047
assert_eq!(removed_item, 'h');
20292048
assert_items_eq!(linked_chunk, [] ['i'] ['j', 'k']);
20302049
assert_eq!(linked_chunk.len(), 3);
20312050

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

20342053
assert_eq!(removed_item, 'i');
20352054
assert_items_eq!(linked_chunk, [] ['j', 'k']);
@@ -2050,15 +2069,15 @@ mod tests {
20502069
// The chunk is removed.
20512070
{
20522071
let position_of_k = linked_chunk.item_position(|item| *item == 'k').unwrap();
2053-
let removed_item = linked_chunk.remove_item_at(position_of_k)?;
2072+
let removed_item = linked_chunk.remove_item_at(position_of_k, EmptyChunk::Remove)?;
20542073

20552074
assert_eq!(removed_item, 'k');
20562075
#[rustfmt::skip]
20572076
assert_items_eq!(linked_chunk, [] ['j']);
20582077
assert_eq!(linked_chunk.len(), 1);
20592078

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

20632082
assert_eq!(removed_item, 'j');
20642083
assert_items_eq!(linked_chunk, []);
@@ -2092,27 +2111,27 @@ mod tests {
20922111
let _ = linked_chunk.updates().unwrap().take();
20932112

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

20972116
assert_eq!(removed_item, 'c');
20982117
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d']);
20992118
assert_eq!(linked_chunk.len(), 3);
21002119

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

21042123
assert_eq!(removed_item, 'd');
21052124
assert_items_eq!(linked_chunk, ['a', 'b'] [-]);
21062125
assert_eq!(linked_chunk.len(), 2);
21072126

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

21112130
assert_eq!(removed_item, 'a');
21122131
assert_items_eq!(linked_chunk, ['b'] [-]);
21132132
assert_eq!(linked_chunk.len(), 1);
21142133

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

21172136
assert_eq!(removed_item, 'b');
21182137
assert_items_eq!(linked_chunk, [] [-]);
@@ -2134,6 +2153,110 @@ mod tests {
21342153
Ok(())
21352154
}
21362155

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

0 commit comments

Comments
 (0)