Skip to main content

matrix_sdk/event_cache/caches/
subscriber.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
15//! The [`Subscriber`] implementation.
16
17use std::{
18    ops::{Deref, DerefMut},
19    sync::{Arc, Weak},
20};
21
22use ruma::{OwnedEventId, OwnedRoomId};
23use tokio::sync::{broadcast::Receiver, mpsc};
24use tracing::{trace, warn};
25
26/// A structure that can generate handles for subscribers, and count them. See
27/// [`SubscriberHandle`] to learn more.
28#[derive(Default)]
29pub struct SubscribersHandle(Arc<()>);
30
31impl SubscribersHandle {
32    /// Count the number of subscribers.
33    ///
34    /// Similar to [`SubscriberHandle::count`].
35    pub fn count(&self) -> usize {
36        Arc::weak_count(&self.0)
37    }
38
39    /// Generate a handle for a subscriber.
40    pub fn new_subscriber_handle(&self) -> SubscriberHandle {
41        SubscriberHandle(Arc::downgrade(&self.0))
42    }
43}
44
45/// A handle for a subscriber.
46///
47/// A cache might need to track/count the number of subscribers. When the number
48/// of subscribers reach zero, it means no more code is listening to it. That's
49/// an opportunity to free some memory by shrinking the cache for example!
50/// Shrinking means forgetting about “old” events, keeping events from the last
51/// chunk for example.
52///
53/// Every time a subscriber is created,
54/// [`SubscribersHandle::new_subscriber_handle`] is called. The resulting value
55/// must be kept by the subscriber for its entire lifetime.
56pub struct SubscriberHandle(Weak<()>);
57
58impl SubscriberHandle {
59    /// Count the number of subscribers.
60    ///
61    /// Similar to [`SubscribersHandle::count`].
62    pub fn count(&self) -> usize {
63        Weak::weak_count(&self.0)
64    }
65}
66
67/// Thin wrapper for a cache subscriber, so as to trigger side-effects when all
68/// subscribers are gone.
69///
70/// The current side-effect is: auto-shrinking the cache when no more
71/// subscribers are active. This is an optimisation to reduce the number of data
72/// held in memory by the cache: when no more subscribers are active, all data
73/// are reduced to the minimum.
74///
75/// The side-effect takes effect on `Drop`.
76#[allow(missing_debug_implementations)]
77pub struct Subscriber<T> {
78    /// Underlying receiver of the cache's updates.
79    subscriber_receiver: Receiver<T>,
80
81    /// The message that is going to be sent to [`Self::auto_shrink_sender`].
82    ///
83    /// This is an `Option` to take/own the value in the `Drop` implementation
84    /// without cloning it.
85    auto_shrink_message: Option<AutoShrinkMessage>,
86
87    /// The sender side of the auto-shrink channel.
88    auto_shrink_sender: mpsc::Sender<AutoShrinkMessage>,
89
90    /// The subscribers handle shared by all subscribers.
91    ///
92    /// This is used to detect when no more subscribers are active, and trigger
93    /// side-effect.
94    ///
95    /// This is an `Option` to take/own the value in the `Drop` implementation
96    /// without cloning it. Being able to own it helps to control when the value
97    /// is dropped.
98    subscriber_handle: Option<SubscriberHandle>,
99}
100
101impl<T> Subscriber<T> {
102    /// Create a new [`Subscriber`].
103    pub(super) fn new(
104        subscriber_receiver: Receiver<T>,
105        auto_shrink_message: AutoShrinkMessage,
106        auto_shrink_sender: mpsc::Sender<AutoShrinkMessage>,
107        subscribers_handle: &SubscribersHandle,
108    ) -> Self {
109        Self {
110            subscriber_receiver,
111            auto_shrink_message: Some(auto_shrink_message),
112            auto_shrink_sender,
113            subscriber_handle: Some(subscribers_handle.new_subscriber_handle()),
114        }
115    }
116}
117
118impl<T> Drop for Subscriber<T> {
119    fn drop(&mut self) {
120        let number_of_subscribers = self
121            .subscriber_handle
122            // Ensure the handle is dropped before sending the message to the
123            // auto shrink task.
124            .take()
125            .expect("Unreachable: `subscriber_handle` must be `Some`")
126            .count();
127
128        trace!("dropping a room event cache subscriber; count: {number_of_subscribers}");
129
130        if number_of_subscribers == 1 {
131            // We were the last instance of the subscriber; let the auto-shrinker know by
132            // notifying it.
133
134            // Try to send without waiting for channel capacity, and restart in a loop if it
135            // failed (until a maximum number of attempts is reached, or the send was
136            // successful). The channel shouldn't be super busy in general, so this should
137            // resolve quickly enough.
138
139            let mut message = self
140                .auto_shrink_message
141                .take()
142                .expect("Unreachable: `auto_shrink_message` must be `Some`");
143            let mut num_attempts = 0;
144
145            while let Err(err) = self.auto_shrink_sender.try_send(message) {
146                num_attempts += 1;
147
148                if num_attempts > 1024 {
149                    // If we've tried too many times, just give up with a warning; after all, this
150                    // is only an optimization.
151                    warn!(
152                        "couldn't send notification to the auto-shrink channel \
153                         after 1024 attempts; giving up"
154                    );
155                    return;
156                }
157
158                match err {
159                    mpsc::error::TrySendError::Full(stolen_message) => {
160                        message = stolen_message;
161                    }
162                    mpsc::error::TrySendError::Closed(_) => return,
163                }
164            }
165
166            trace!("sent notification to the parent channel that we were the last subscriber");
167        }
168    }
169}
170
171impl<T> Deref for Subscriber<T> {
172    type Target = Receiver<T>;
173
174    fn deref(&self) -> &Self::Target {
175        &self.subscriber_receiver
176    }
177}
178
179impl<T> DerefMut for Subscriber<T> {
180    fn deref_mut(&mut self) -> &mut Self::Target {
181        &mut self.subscriber_receiver
182    }
183}
184
185/// Type of messages exchanged between [`Subscriber`] and the
186/// [`auto_shrink_linked_chunk_task`] task.
187///
188/// [`auto_shrink_linked_chunk_task`]: super::super::tasks::auto_shrink_linked_chunk_task
189#[derive(Debug)]
190pub enum AutoShrinkMessage {
191    /// Ask to automatically shrink a [`RoomEventCache`].
192    ///
193    /// [`RoomEventCache`]: super::room::RoomEventCache
194    Room {
195        /// The ID of the room.
196        room_id: OwnedRoomId,
197    },
198
199    /// Ask to automatically shrink a [`ThreadEventCache`].
200    ///
201    /// [`ThreadEventCache`]: super::thread::ThreadEventCache
202    Thread {
203        /// The room ID of the thread.
204        room_id: OwnedRoomId,
205        /// The thread ID (root).
206        thread_id: OwnedEventId,
207    },
208}
209
210#[cfg(test)]
211mod tests {
212    use assert_matches::assert_matches;
213    use ruma::owned_room_id;
214    use tokio::sync::{broadcast, mpsc};
215
216    use super::{AutoShrinkMessage, Subscriber, SubscribersHandle};
217
218    #[test]
219    fn test_subscribers_handle() {
220        let subscribers_handle = SubscribersHandle::default();
221
222        // No subscribers.
223        assert_eq!(subscribers_handle.count(), 0);
224
225        // Pretend a new subscriber exists!
226        let handle0 = subscribers_handle.new_subscriber_handle();
227        assert_eq!(subscribers_handle.count(), 1);
228        assert_eq!(handle0.count(), 1);
229
230        // Pretend another new subscriber exists!
231        let handle1 = subscribers_handle.new_subscriber_handle();
232        assert_eq!(subscribers_handle.count(), 2);
233        assert_eq!(handle0.count(), 2);
234        assert_eq!(handle1.count(), 2);
235
236        // A subscriber dies. RIP.
237        drop(handle0);
238        assert_eq!(subscribers_handle.count(), 1);
239        assert_eq!(handle1.count(), 1);
240
241        // Another subscriber dies. RIP.
242        drop(handle1);
243        assert_eq!(subscribers_handle.count(), 0);
244
245        // Create a new subscriber, for the last test.
246        let handle2 = subscribers_handle.new_subscriber_handle();
247
248        // We can even drop the `SubscribersHandle`!
249        drop(subscribers_handle);
250        assert_eq!(handle2.count(), 0);
251        // ZERO, yes, not 1.
252        // If the state containing the `SubscribersHandle` drops, there is no
253        // more update, and no auto-shrink, so it's fine to get a zero here.
254    }
255
256    #[test]
257    fn test_subscriber_t_derefs_to_t() {
258        let (auto_shrink_sender, _auto_shrink_receiver) = mpsc::channel(1);
259        let (subscriber_sender, subscriber_receiver) = broadcast::channel(1);
260        let subscribers_handle = SubscribersHandle::default();
261
262        let mut subscriber = Subscriber::new(
263            subscriber_receiver,
264            AutoShrinkMessage::Room { room_id: owned_room_id!("!r0") },
265            auto_shrink_sender,
266            &subscribers_handle,
267        );
268
269        subscriber_sender.send('a').unwrap();
270
271        // `DerefMut` with `try_recv(&mut self)`.
272        assert_eq!(subscriber.try_recv().unwrap(), 'a');
273
274        // `Deref` with `is_empty(&self)`.
275        assert!(subscriber.is_empty());
276    }
277
278    #[test]
279    fn test_subscriber_send_auto_shrink_message_on_last_drop() {
280        let (auto_shrink_sender, mut auto_shrink_receiver) = mpsc::channel(1);
281        let (_subscriber_sender, subscriber_receiver) = broadcast::channel::<()>(1);
282        let subscribers_handle = SubscribersHandle::default();
283        let room_id = owned_room_id!("!r0");
284        let auto_shrink_message = AutoShrinkMessage::Room { room_id: room_id.clone() };
285
286        let subscriber0 = Subscriber::new(
287            subscriber_receiver.resubscribe(),
288            AutoShrinkMessage::Room { room_id: room_id.clone() },
289            auto_shrink_sender.clone(),
290            &subscribers_handle,
291        );
292
293        let subscriber1 = Subscriber::new(
294            subscriber_receiver,
295            auto_shrink_message,
296            auto_shrink_sender,
297            &subscribers_handle,
298        );
299
300        // Drop a subscriber. No side-effect because there is still one alive!
301        drop(subscriber0);
302        assert!(auto_shrink_receiver.is_empty());
303
304        // Drop the last subscriber. Side-effect should… take effect!
305        drop(subscriber1);
306        assert_matches!(
307            auto_shrink_receiver.try_recv().unwrap(),
308            AutoShrinkMessage::Room { room_id: expected_room_id } => {
309                assert_eq!(expected_room_id, room_id);
310            }
311        );
312        assert!(auto_shrink_receiver.is_empty());
313    }
314
315    #[test]
316    fn test_subscriber_send_auto_shrink_message_with_full_channel() {
317        let (auto_shrink_sender, mut auto_shrink_receiver) = mpsc::channel(1);
318        let (_subscriber_sender, subscriber_receiver) = broadcast::channel::<()>(1);
319        let subscribers_handle = SubscribersHandle::default();
320        let noisy_room_id = owned_room_id!("!r1");
321        let auto_shrink_noisy_message = AutoShrinkMessage::Room { room_id: noisy_room_id.clone() };
322        let room_id = owned_room_id!("!r0");
323        let auto_shrink_message = AutoShrinkMessage::Room { room_id };
324
325        // Saturate the `auto_shrink` channel.
326        auto_shrink_sender.try_send(auto_shrink_noisy_message).unwrap();
327
328        let subscriber = Subscriber::new(
329            subscriber_receiver,
330            auto_shrink_message,
331            auto_shrink_sender,
332            &subscribers_handle,
333        );
334
335        // Drop the last subscriber. Side-effect should… take effect, but (!) the
336        // channel is full, so it's going to retry many times and will fail, resulting
337        // in no side-effect.
338        drop(subscriber);
339
340        // We receive the noisy message: **not** the message from the subscriber under
341        // testing.
342        assert_matches!(
343            auto_shrink_receiver.try_recv().unwrap(),
344            AutoShrinkMessage::Room { room_id: expected_room_id } => {
345                assert_eq!(expected_room_id, noisy_room_id);
346            }
347        );
348
349        // Then, we receive nothing, i.e. `subscriber` dropped without any side-effect.
350        assert!(auto_shrink_receiver.is_empty());
351    }
352
353    #[test]
354    fn test_subscriber_send_auto_shrink_message_with_closed_channel() {
355        let (auto_shrink_sender, auto_shrink_receiver) = mpsc::channel(1);
356        let (_subscriber_sender, subscriber_receiver) = broadcast::channel::<()>(1);
357        let subscribers_handle = SubscribersHandle::default();
358        let room_id = owned_room_id!("!r0");
359        let auto_shrink_message = AutoShrinkMessage::Room { room_id };
360
361        let subscriber = Subscriber::new(
362            subscriber_receiver,
363            auto_shrink_message,
364            auto_shrink_sender,
365            &subscribers_handle,
366        );
367
368        // Close the `auto_shrink` channel.
369        drop(auto_shrink_receiver);
370
371        // Drop the last subscriber. Side-effect should… take effect, but (!) the
372        // channel is closed, so it's going to stop immediately, resulting in no
373        // side-effect.
374        drop(subscriber);
375
376        // Sadly, nothing to assert because we are now blind, but at least the
377        // system shouldn't explode!
378    }
379}