Skip to content

fix(timeline): maintain aggregations when an event is deduplicated #4576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Feb 10, 2025

Conversation

bnjbvr
Copy link
Member

@bnjbvr bnjbvr commented Jan 22, 2025

Some context

An aggregation is an event that relates to another event: for instance, a
reaction, a poll response, and so on and so forth.

Some requirements

Because of the sync mechanisms and federation, it can happen that a related
event is received before receiving the event it relates to. Those events
must be accounted for, stashed somewhere, and reapplied later, if/when the
related-to event shows up.

In addition to that, a room's event cache can also decide to move events
around, in its own internal representation (likely because it ran into some
duplicate events, or it managed to decrypt a previously UTD event).
When that happens, a timeline opened on the given room
will see a removal then re-insertion of the given event. If that event was
the target of aggregations, then those aggregations must be re-applied when
the given event is reinserted.

Some solution

To satisfy both requirements, the [Aggregations] "manager" object provided
by this PR will take care of memoizing aggregations, for the entire
lifetime of the timeline
(or until it's clear'd by some
caller). Aggregations are saved in memory, and have the same lifetime as
that of a timeline. This makes it possible to apply pending aggregations
to cater for the first use case, and to never lose any aggregations in the
second use case.

Some points for the reviewer

  • I think the most controversial point is that all aggregations are memoized for the entire lifetime of the timeline. Would that become an issue, we can get back to some incremental scheme, in the future: instead of memoizing aggregations for the entire lifetime of the timeline, we'd attach them to a single timeline item. When that item is removed, we'd put the aggregations back into a "pending" stash of aggregations. If the item is reinserted later, we could peek at the pending stash of aggregations, remove any that's in there, and reapply them to the reinserted event. This is what the first version of this patch did, in a much more adhoc way, for reactions only; based on the current PR, we could do the same in a simpler manner
  • while the PR has small commits, they don't quite make sense to review individually, I'm afraid, as I was trying to find a way to make a general system that would work not only for reactions, poll responses and ends. As a matter of fact, the first commits may have introduced code that is changed in subsequent commits, making the review a bit hazardous. Happy to have a live reviewing party over Element Call, if that helps, considering the size of the patch.
  • future work may include using the aggregations manager for edits too, leading to more code removal.

Part of #3280.

@bnjbvr bnjbvr force-pushed the bnjbvr/lost-reactions branch from 6c2393e to 140ab9a Compare January 23, 2025 16:58
Copy link

codecov bot commented Jan 23, 2025

Codecov Report

Attention: Patch coverage is 79.26421% with 62 lines in your changes missing coverage. Please review.

Project coverage is 85.69%. Comparing base (ce44c6e) to head (4b26a48).
Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
...rix-sdk-ui/src/timeline/controller/aggregations.rs 83.73% 20 Missing ⚠️
...rates/matrix-sdk-ui/src/timeline/controller/mod.rs 71.87% 18 Missing ⚠️
crates/matrix-sdk-ui/src/timeline/event_handler.rs 84.84% 10 Missing ⚠️
...trix-sdk-ui/src/timeline/event_item/content/mod.rs 61.53% 10 Missing ⚠️
...ix-sdk-ui/src/timeline/event_item/content/polls.rs 55.55% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4576      +/-   ##
==========================================
- Coverage   85.73%   85.69%   -0.05%     
==========================================
  Files         292      292              
  Lines       33492    33558      +66     
==========================================
+ Hits        28715    28756      +41     
- Misses       4777     4802      +25     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@bnjbvr bnjbvr force-pushed the bnjbvr/lost-reactions branch 4 times, most recently from e660de4 to 78d32b5 Compare February 5, 2025 17:51
@bnjbvr bnjbvr force-pushed the bnjbvr/lost-reactions branch from 095a5ed to 3d7feaa Compare February 6, 2025 17:00
bnjbvr added 21 commits February 6, 2025 18:00
forever = the lifetime of a timeline.
…eItemContent::reactions()`

I give up; returning a reference would be better to avoid plain cloning,
but there's no way to make a Lazy reference work on wasm, because the
map transitively uses a field that's not sync/send :(

Which is sad, because the default value would be an empty map, and not
contain any data that wouldn't be sync/send anyways.
@bnjbvr bnjbvr force-pushed the bnjbvr/lost-reactions branch from 3d7feaa to c54dbfe Compare February 6, 2025 17:00
@bnjbvr bnjbvr marked this pull request as ready for review February 6, 2025 17:01
@bnjbvr bnjbvr requested a review from a team as a code owner February 6, 2025 17:01
@bnjbvr bnjbvr requested review from andybalaam and removed request for a team February 6, 2025 17:01
@bnjbvr bnjbvr force-pushed the bnjbvr/lost-reactions branch from c54dbfe to da828c3 Compare February 6, 2025 17:04
@andybalaam
Copy link
Member

A live review party sounds good to me, and I've asked some other members of the Rust team whether they'd like to join because I'm not sure I should look at this on my own.

Copy link
Member

@andybalaam andybalaam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to see some tests for the Aggregations manager, but looks good!

let content = self.inner.content().with_reactions(reactions);

// Do not use `Self::with_content` which may override the latest_edit_json.
// TODO: it's likely incorrect?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment - can you expand on it? If it's incorrect, do we need to fix it before we can commit this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, something I noticed, and fixed in another PR; I can remove the comment now, thanks for spotting it!

///
/// In case of error, returns an error detailing why the aggregation
/// couldn't be applied.
pub fn apply(&self, content: &mut TimelineItemContent) -> Result<bool, AggregationError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed irl, returning an enum instead of the bool here would make it clearer, I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turned out better than I expected! The code in apply/unapply is a bit uglier IMO, but that makes it much clearer for the callers. Thank you and @Hywan both for suggesting and pushing for this!

/// In case of success, returns a boolean indicating whether unapplying the
/// aggregation caused a change in the content. If the aggregation could be
/// unapplied, but it didn't cause any change in the passed
/// [`TimelineItemContent`], `Ok(false)` will be returned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, an enum return value would help to understand.

/// Mark a target event as being sent (i.e. it transitions from an local
/// transaction id to its remote event id counterpart), by updating the
/// internal mappings.
pub fn mark_target_as_sent(&mut self, txn_id: OwnedTransactionId, event_id: OwnedEventId) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have some unit tests for this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, will write a bunch in a subsequent PR!

}
} else {
warn!("missing related-to item ({target:?}) for aggregation {aggregation_id:?}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not a warning since it is fine to happen.

@bnjbvr bnjbvr enabled auto-merge (squash) February 10, 2025 15:12
@bnjbvr bnjbvr merged commit 9f2c572 into main Feb 10, 2025
41 checks passed
@bnjbvr bnjbvr deleted the bnjbvr/lost-reactions branch February 10, 2025 15:38
yostyle pushed a commit to tchapgouv/matrix-rust-sdk that referenced this pull request Apr 4, 2025
…atrix-org#4576)

## Some context

An aggregation is an event that relates to another event: for instance,
a
reaction, a poll response, and so on and so forth.
                               
## Some requirements
                                              
Because of the sync mechanisms and federation, it can happen that a
related
event is received *before* receiving the event it relates to. Those
events
must be accounted for, stashed somewhere, and reapplied later, if/when
the
related-to event shows up.
In addition to that, a room's event cache can also decide to move events
around, in its own internal representation (likely because it ran into
some
duplicate events, or it managed to decrypt a previously UTD event).
When that happens, a timeline opened on the given room
will see a removal then re-insertion of the given event. If that event
was
the target of aggregations, then those aggregations must be re-applied
when
the given event is reinserted.
                                                                       
## Some solution
      
To satisfy both requirements, the [`Aggregations`] "manager" object
provided
by this PR will take care of memoizing aggregations, **for the entire
lifetime of the timeline** (or until it's clear'd by some
caller). Aggregations are saved in memory, and have the same lifetime as
that of a timeline. This makes it possible to apply pending aggregations
to cater for the first use case, and to never lose any aggregations in
the
second use case.

## Some points for the reviewer

- I think the most controversial point is that all aggregations are
memoized for the entire lifetime of the timeline. Would that become an
issue, we can get back to some incremental scheme, in the future:
instead of memoizing aggregations for the entire lifetime of the
timeline, we'd attach them to a single timeline item. When that item is
removed, we'd put the aggregations back into a "pending" stash of
aggregations. If the item is reinserted later, we could peek at the
pending stash of aggregations, remove any that's in there, and reapply
them to the reinserted event. This is what the [first version of this
patch](matrix-org@ec64b9e)
did, in a much more adhoc way, for reactions only; based on the current
PR, we could do the same in a simpler manner
- while the PR has small commits, they don't quite make sense to review
individually, I'm afraid, as I was trying to find a way to make a
general system that would work not only for reactions, poll responses
and ends. As a matter of fact, the first commits may have introduced
code that is changed in subsequent commits, making the review a bit
hazardous. Happy to have a live reviewing party over Element Call, if
that helps, considering the size of the patch.
- future work may include using the aggregations manager for edits too,
leading to more code removal.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants