Skip to main content

matrix_sdk/event_cache/
tasks.rs

1// Copyright 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    collections::HashMap,
17    ops::ControlFlow,
18    sync::{Arc, Weak},
19};
20
21use eyeball::Subscriber;
22use matrix_sdk_base::{
23    linked_chunk::OwnedLinkedChunkId, serde_helpers::extract_thread_root_from_content,
24    sync::RoomUpdates,
25};
26use ruma::{OwnedEventId, OwnedTransactionId, RoomId};
27use tokio::{
28    select,
29    sync::{
30        OwnedRwLockReadGuard,
31        broadcast::{Receiver, Sender, error::RecvError},
32        mpsc,
33    },
34};
35use tracing::{Instrument as _, Span, debug, error, info, info_span, instrument, trace, warn};
36
37use super::{
38    AutoShrinkMessage, Caches, CachesByRoom, EventCacheError, EventCacheInner,
39    RoomEventCacheLinkedChunkUpdate,
40};
41use crate::{
42    client::WeakClient,
43    send_queue::{LocalEchoContent, RoomSendQueueUpdate, SendQueueUpdate},
44};
45
46/// Listen to [`RoomUpdates`] to update the Event Cache.
47#[instrument(skip_all)]
48pub(super) async fn room_updates_task(
49    inner: Arc<EventCacheInner>,
50    mut room_updates_feed: Receiver<RoomUpdates>,
51) {
52    trace!("Spawning the listen task");
53    loop {
54        match room_updates_feed.recv().await {
55            Ok(updates) => {
56                trace!("Receiving `RoomUpdates`");
57
58                if let Err(err) = inner.handle_room_updates(updates).await {
59                    match err {
60                        EventCacheError::ClientDropped => {
61                            // The client has dropped, exit the listen task.
62                            info!(
63                                "Closing the event cache global listen task because client dropped"
64                            );
65                            break;
66                        }
67                        err => {
68                            error!("Error when handling room updates: {err}");
69                        }
70                    }
71                }
72            }
73
74            Err(RecvError::Lagged(num_skipped)) => {
75                // Forget everything we know; we could have missed events, and we have
76                // no way to reconcile at the moment!
77                // TODO: implement Smart Matching™,
78                warn!(num_skipped, "Lagged behind room updates, clearing all rooms");
79                if let Err(err) = inner.clear_all_rooms().await {
80                    error!("when clearing storage after lag in listen_task: {err}");
81                }
82            }
83
84            Err(RecvError::Closed) => {
85                // The sender has shut down, exit.
86                info!("Closing the event cache global listen task because receiver closed");
87                break;
88            }
89        }
90    }
91}
92
93/// Listen to _ignore user list update changes_ to clear the rooms when a user
94/// is ignored or unignored.
95#[instrument(skip_all)]
96pub(super) async fn ignore_user_list_update_task(
97    inner: Arc<EventCacheInner>,
98    mut ignore_user_list_stream: Subscriber<Vec<String>>,
99) {
100    let span = info_span!(parent: Span::none(), "ignore_user_list_update_task");
101    span.follows_from(Span::current());
102
103    async move {
104        while ignore_user_list_stream.next().await.is_some() {
105            info!("Received an ignore user list change");
106
107            if let Err(err) = inner.clear_all_rooms().await {
108                error!("when clearing room storage after ignore user list change: {err}");
109            }
110        }
111
112        info!("Ignore user list stream has closed");
113    }
114    .instrument(span)
115    .await;
116}
117
118/// Spawns the task that will listen to auto-shrink notifications.
119///
120/// The auto-shrink mechanism works this way:
121///
122/// - Each time there's a new subscriber to a [`RoomEventCache`], it will
123///   increment the active number of subscribers to that room, see
124///   `RoomEventCacheState::subscribers_handle`.
125/// - When that subscriber is dropped, it will decrement that count; and notify
126///   the task below if it reached 0.
127/// - The task spawned here, owned by the [`EventCacheInner`], will listen to
128///   such notifications that a room may be shrunk. It will attempt an
129///   auto-shrink, by letting the inner state decide whether this is a good time
130///   to do so (new subscribers might have spawned in the meanwhile).
131///
132/// [`RoomEventCache`]: super::RoomEventCache
133/// [`EventCacheInner`]: super::EventCacheInner
134#[instrument(skip_all)]
135pub(super) async fn auto_shrink_linked_chunk_task(
136    inner: Weak<EventCacheInner>,
137    mut auto_shrink_receiver: mpsc::Receiver<AutoShrinkMessage>,
138) {
139    while let Some(message) = auto_shrink_receiver.recv().await {
140        trace!(?message, "received notification to shrink");
141
142        let Some(inner) = inner.upgrade() else {
143            return;
144        };
145
146        let maybe_diffs = match message {
147            AutoShrinkMessage::Room { room_id } => {
148                let ControlFlow::Continue(caches) = all_caches(inner.as_ref(), &room_id).await
149                else {
150                    continue;
151                };
152
153                match caches.room().state().write().await {
154                    Ok(mut state) => state.auto_shrink_if_no_subscribers().await,
155                    Err(err) => {
156                        warn!(%room_id, ?err, "Failed to get the `RoomEventCacheStateLock`");
157                        continue;
158                    }
159                }
160            }
161
162            AutoShrinkMessage::Thread { room_id, thread_id } => {
163                let ControlFlow::Continue(caches) = all_caches(inner.as_ref(), &room_id).await
164                else {
165                    continue;
166                };
167
168                let Ok(cache) = caches.thread(thread_id.clone()).await else {
169                    warn!(%room_id, %thread_id, "Failed to get the `ThreadEventCache`");
170                    continue;
171                };
172
173                match cache.state().write().await {
174                    Ok(mut state) => state.auto_shrink_if_no_subscribers().await,
175                    Err(err) => {
176                        warn!(%room_id, ?err, "Failed to get the `ThreadEventCacheStateLock`");
177                        continue;
178                    }
179                }
180            }
181        };
182
183        match maybe_diffs {
184            Ok(_diffs) => {
185                // Two situations here:
186                //
187                // 1. No race, no subscribers have been registered, so it's safe
188                //    to do nothing,
189                // 2. A race, a subscriber has been created meanwhile, we **must
190                //    not** send the diff to it, otherwise it can create an
191                //    invalid state.
192                //
193                // Note that a race shouldn't be possible as we have acquired an
194                // exclusive access to the state, ensuring no subscriber can be
195                // created.
196            }
197
198            Err(err) => {
199                // There's not much we can do here, unfortunately.
200                warn!(?err, "error when attempting to shrink linked chunk");
201            }
202        }
203    }
204
205    info!("Auto-shrink linked chunk task has been closed, exiting");
206
207    async fn all_caches(
208        inner: &EventCacheInner,
209        room_id: &RoomId,
210    ) -> ControlFlow<(), OwnedRwLockReadGuard<CachesByRoom, Caches>> {
211        match inner.all_caches_for_room(room_id).await {
212            Ok(caches) => ControlFlow::Continue(caches),
213            Err(err) => {
214                warn!(?err, "Failed to get the `Caches`");
215                ControlFlow::Break(())
216            }
217        }
218    }
219}
220
221/// Handle [`SendQueueUpdate`] and [`RoomEventCacheLinkedChunkUpdate`] to update
222/// the threads, for a thread the user was not subscribed to.
223#[instrument(skip_all)]
224pub(super) async fn thread_subscriber_task(
225    client: WeakClient,
226    linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
227    thread_subscriber_sender: Sender<()>,
228) {
229    let mut send_q_rx = if let Some(client) = client.get() {
230        match client.enabled_thread_subscriptions().await {
231            Ok(enabled) => {
232                if !enabled {
233                    trace!(
234                        "Thread subscriptions are not enabled, not spawning thread subscriber task"
235                    );
236                    return;
237                }
238            }
239
240            Err(err) => {
241                warn!(%err, "Failed to get whether thread subscriptions are enabled, not spawning thread subscriber task");
242                return;
243            }
244        }
245
246        client.send_queue().subscribe()
247    } else {
248        trace!("Client is shutting down, not spawning thread subscriber task");
249        return;
250    };
251
252    let mut linked_chunk_rx = linked_chunk_update_sender.subscribe();
253
254    // A mapping of local echoes (events being sent), to their thread root, if
255    // they're in an in-thread reply.
256    //
257    // Entirely managed by `handle_thread_subscriber_send_queue_update`.
258    let mut events_being_sent = HashMap::new();
259
260    loop {
261        select! {
262            res = send_q_rx.recv() => {
263                match res {
264                    Ok(up) => {
265                        if !handle_thread_subscriber_send_queue_update(&client, &thread_subscriber_sender, &mut events_being_sent, up).await {
266                            break;
267                        }
268                    }
269                    Err(RecvError::Closed) => {
270                        debug!("Linked chunk update channel has been closed, exiting thread subscriber task");
271                        break;
272                    }
273                    Err(RecvError::Lagged(num_skipped)) => {
274                        warn!(num_skipped, "Lagged behind linked chunk updates");
275                    }
276                }
277            }
278
279            res = linked_chunk_rx.recv() => {
280                match res {
281                    Ok(up) => {
282                        if !handle_thread_subscriber_linked_chunk_update(&client, &thread_subscriber_sender, up).await {
283                            break;
284                        }
285                    }
286                    Err(RecvError::Closed) => {
287                        debug!("Linked chunk update channel has been closed, exiting thread subscriber task");
288                        break;
289                    }
290                    Err(RecvError::Lagged(num_skipped)) => {
291                        warn!(num_skipped, "Lagged behind linked chunk updates");
292                    }
293                }
294            }
295        }
296    }
297}
298
299/// React to a given send queue update by subscribing the user to a
300/// thread, if needs be (when the user sent an event in a thread they were
301/// not subscribed to).
302///
303/// Returns a boolean indicating whether the task should keep on running or
304/// not.
305#[instrument(skip(client, thread_subscriber_sender))]
306async fn handle_thread_subscriber_send_queue_update(
307    client: &WeakClient,
308    thread_subscriber_sender: &Sender<()>,
309    events_being_sent: &mut HashMap<OwnedTransactionId, OwnedEventId>,
310    up: SendQueueUpdate,
311) -> bool {
312    let Some(client) = client.get() else {
313        // Client shutting down.
314        debug!("Client is shutting down, exiting thread subscriber task");
315        return false;
316    };
317
318    let room_id = up.room_id;
319    let Some(room) = client.get_room(&room_id) else {
320        warn!(%room_id, "unknown room");
321        return true;
322    };
323
324    let (thread_root, subscribe_up_to) = match up.update {
325        RoomSendQueueUpdate::NewLocalEvent(local_echo) => {
326            match local_echo.content {
327                LocalEchoContent::Event { serialized_event, .. } => {
328                    if let Some(thread_root) =
329                        extract_thread_root_from_content(serialized_event.into_raw().0)
330                    {
331                        events_being_sent.insert(local_echo.transaction_id, thread_root);
332                    }
333                }
334                LocalEchoContent::React { .. } => {
335                    // Nothing to do, reactions don't count as a thread
336                    // subscription.
337                }
338
339                LocalEchoContent::Redaction { .. } => {
340                    // Nothing to do, redactions don't count as a thread
341                    // subscription.
342                }
343            }
344            return true;
345        }
346
347        RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {
348            events_being_sent.remove(&transaction_id);
349            return true;
350        }
351
352        RoomSendQueueUpdate::ReplacedLocalEvent { transaction_id, new_content } => {
353            if let Some(thread_root) = extract_thread_root_from_content(new_content.into_raw().0) {
354                events_being_sent.insert(transaction_id, thread_root);
355            } else {
356                // It could be that the event isn't part of a thread anymore; handle that by
357                // removing the pending transaction id.
358                events_being_sent.remove(&transaction_id);
359            }
360            return true;
361        }
362
363        RoomSendQueueUpdate::SentEvent { transaction_id, event_id } => {
364            if let Some(thread_root) = events_being_sent.remove(&transaction_id) {
365                (thread_root, event_id)
366            } else {
367                // We don't know about the event that has been sent, so ignore it.
368                trace!(%transaction_id, "received a sent event that we didn't know about, ignoring");
369                return true;
370            }
371        }
372
373        RoomSendQueueUpdate::SendError { .. }
374        | RoomSendQueueUpdate::RetryEvent { .. }
375        | RoomSendQueueUpdate::MediaUpload { .. } => {
376            // Nothing to do for these bad boys.
377            return true;
378        }
379    };
380
381    // And if we've found such a mention, subscribe to the thread up to this event.
382    trace!(thread = %thread_root, up_to = %subscribe_up_to, "found a new thread to subscribe to");
383
384    if let Err(err) = room.subscribe_thread_if_needed(&thread_root, Some(subscribe_up_to)).await {
385        warn!(%err, "Failed to subscribe to thread");
386    } else {
387        let _ = thread_subscriber_sender.send(());
388    }
389
390    true
391}
392
393/// React to a given linked chunk update by subscribing the user to a
394/// thread, if needs be (when the user got mentioned in a thread reply, for
395/// a thread they were not subscribed to).
396///
397/// Returns a boolean indicating whether the task should keep on running or
398/// not.
399#[instrument(skip(client, thread_subscriber_sender))]
400async fn handle_thread_subscriber_linked_chunk_update(
401    client: &WeakClient,
402    thread_subscriber_sender: &Sender<()>,
403    up: RoomEventCacheLinkedChunkUpdate,
404) -> bool {
405    let Some(client) = client.get() else {
406        // Client shutting down.
407        debug!("Client is shutting down, exiting thread subscriber task");
408        return false;
409    };
410
411    let OwnedLinkedChunkId::Thread(room_id, thread_root) = &up.linked_chunk_id else {
412        trace!("received an update for a non-thread linked chunk, ignoring");
413        return true;
414    };
415
416    let Some(room) = client.get_room(room_id) else {
417        warn!(%room_id, "unknown room");
418        return true;
419    };
420
421    let thread_root = thread_root.clone();
422
423    let mut new_events = up.events().peekable();
424
425    if new_events.peek().is_none() {
426        // No new events, nothing to do.
427        return true;
428    }
429
430    // This `PushContext` is going to be used to compute whether an in-thread event
431    // would trigger a mention.
432    //
433    // Of course, we're not interested in an in-thread event causing a mention,
434    // because it's part of a thread we've subscribed to. So the
435    // `PushContext` must not include the check for thread subscriptions (otherwise
436    // it would be impossible to subscribe to new threads).
437
438    let with_thread_subscriptions = false;
439
440    let Some(push_context) = room
441        .push_context_internal(with_thread_subscriptions)
442        .await
443        .inspect_err(|err| {
444            warn!("Failed to get push context for threads: {err}");
445        })
446        .ok()
447        .flatten()
448    else {
449        warn!("Missing push context for thread subscriptions.");
450        return true;
451    };
452
453    let mut subscribe_up_to = None;
454
455    // Find if there's an event that would trigger a mention for the current
456    // user, iterating from the end of the new events towards the oldest, so we can
457    // find the most recent event to subscribe to.
458    for ev in new_events.rev() {
459        if push_context.for_event(ev.raw()).await.into_iter().any(|action| action.should_notify()) {
460            let Some(event_id) = ev.event_id() else {
461                // Shouldn't happen.
462                continue;
463            };
464            subscribe_up_to = Some(event_id.to_owned());
465            break;
466        }
467    }
468
469    // And if we've found such a mention, subscribe to the thread up to this
470    // event.
471    if let Some(event_id) = subscribe_up_to {
472        trace!(thread = %thread_root, up_to = %event_id, "found a new thread to subscribe to");
473        if let Err(err) = room.subscribe_thread_if_needed(&thread_root, Some(event_id)).await {
474            warn!(%err, "Failed to subscribe to thread");
475        } else {
476            let _ = thread_subscriber_sender.send(());
477        }
478    }
479
480    true
481}
482
483/// Takes an [`Event`] and passes it to the [`RoomIndex`] of the
484/// given room which will add/remove/edit an event in the index based on
485/// the event type.
486///
487/// [`Event`]: matrix_sdk_base::event_cache::Event
488/// [`RoomIndex`]: matrix_sdk_search::index::RoomIndex
489#[cfg(feature = "experimental-search")]
490#[instrument(skip_all)]
491pub(super) async fn search_indexing_task(
492    client: WeakClient,
493    linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
494) {
495    let mut linked_chunk_update_receiver = linked_chunk_update_sender.subscribe();
496
497    loop {
498        match linked_chunk_update_receiver.recv().await {
499            Ok(room_ec_lc_update) => {
500                let OwnedLinkedChunkId::Room(room_id) = room_ec_lc_update.linked_chunk_id.clone()
501                else {
502                    trace!("Received non-room updates, ignoring.");
503                    continue;
504                };
505
506                let mut timeline_events = room_ec_lc_update.events().peekable();
507
508                if timeline_events.peek().is_none() {
509                    continue;
510                }
511
512                let Some(client) = client.get() else {
513                    trace!("Client is shutting down, exiting search task");
514                    return;
515                };
516
517                let maybe_room_cache = client.event_cache().room(&room_id).await;
518                let Ok((room_cache, _drop_handles)) = maybe_room_cache else {
519                    warn!(for_room = %room_id, "Failed to get RoomEventCache: {maybe_room_cache:?}");
520                    continue;
521                };
522
523                let maybe_room = client.get_room(&room_id);
524                let Some(room) = maybe_room else {
525                    warn!(get_room = %room_id, "Failed to get room while indexing: {maybe_room:?}");
526                    continue;
527                };
528                let redaction_rules = room.clone_info().room_version_rules_or_default().redaction;
529
530                let mut search_index_guard = client.search_index().lock().await;
531
532                if let Err(err) = search_index_guard
533                    .bulk_handle_timeline_event(
534                        timeline_events,
535                        &room_cache,
536                        &room_id,
537                        &redaction_rules,
538                    )
539                    .await
540                {
541                    error!("Failed to handle events for indexing: {err}")
542                }
543            }
544            Err(RecvError::Closed) => {
545                debug!(
546                    "Linked chunk update channel has been closed, exiting thread subscriber task"
547                );
548                break;
549            }
550            Err(RecvError::Lagged(num_skipped)) => {
551                warn!(num_skipped, "Lagged behind linked chunk updates");
552            }
553        }
554    }
555}