xmtp_mls/worker/
disappearing_messages.rs1use crate::context::XmtpSharedContext;
2use crate::worker::{BoxedWorker, NeedsDbReconnect, Worker, WorkerFactory};
3use crate::worker::{WorkerKind, WorkerResult};
4use futures::TryFutureExt;
5use std::sync::Arc;
6use std::time::Duration;
7use thiserror::Error;
8use xmtp_common::time::now_ns;
9use xmtp_db::{StorageError, prelude::*};
10
11const FALLBACK_INTERVAL: Duration = Duration::from_secs(24 * 3600);
17
18#[derive(Clone)]
27pub struct DisappearingChannels {
28 sender: tokio::sync::mpsc::Sender<()>,
29 pub receiver: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<()>>>,
30}
31
32impl Default for DisappearingChannels {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl DisappearingChannels {
39 pub fn new() -> Self {
40 let (sender, receiver) = tokio::sync::mpsc::channel(1);
41 Self {
42 sender,
43 receiver: Arc::new(tokio::sync::Mutex::new(receiver)),
44 }
45 }
46
47 pub fn rearm(&self) {
52 let _ = self.sender.try_send(());
53 }
54}
55
56#[derive(Debug, Error)]
57pub enum DisappearingMessagesCleanerError {
58 #[error("storage error: {0}")]
59 Storage(#[from] StorageError),
60 #[error("failed to delete expired messages: {0}")]
61 DeleteExpired(StorageError),
62}
63
64impl NeedsDbReconnect for DisappearingMessagesCleanerError {
65 fn needs_db_reconnect(&self) -> bool {
66 match self {
67 Self::Storage(s) | Self::DeleteExpired(s) => s.db_needs_connection(),
68 }
69 }
70}
71
72pub struct DisappearingMessagesWorker<Context> {
73 context: Context,
74}
75
76struct Factory<Context> {
77 context: Context,
78}
79
80impl<Context> WorkerFactory for Factory<Context>
81where
82 Context: XmtpSharedContext + 'static,
83{
84 fn create(
85 &self,
86 metrics: Option<crate::worker::DynMetrics>,
87 ) -> (BoxedWorker, Option<crate::worker::DynMetrics>) {
88 let worker = Box::new(DisappearingMessagesWorker::new(self.context.clone())) as Box<_>;
89 (worker, metrics)
90 }
91
92 fn kind(&self) -> WorkerKind {
93 WorkerKind::DisappearingMessages
94 }
95}
96
97#[xmtp_common::async_trait]
98impl<Context> Worker for DisappearingMessagesWorker<Context>
99where
100 Context: XmtpSharedContext + 'static,
101{
102 fn kind(&self) -> WorkerKind {
103 WorkerKind::DisappearingMessages
104 }
105
106 async fn run_tasks(&mut self) -> WorkerResult<()> {
107 self.run().map_err(|e| Box::new(e) as Box<_>).await
108 }
109
110 fn factory<C>(context: C) -> impl WorkerFactory + 'static
111 where
112 Self: Sized,
113 C: XmtpSharedContext + 'static,
114 {
115 Factory { context }
116 }
117}
118
119impl<Context> DisappearingMessagesWorker<Context>
120where
121 Context: XmtpSharedContext + 'static,
122{
123 pub fn new(context: Context) -> Self {
124 Self { context }
125 }
126}
127
128impl<Context> DisappearingMessagesWorker<Context>
129where
130 Context: XmtpSharedContext + 'static,
131{
132 async fn run(&mut self) -> Result<(), DisappearingMessagesCleanerError> {
137 let (fallback, jitter) = self
140 .context
141 .worker_interval(WorkerKind::DisappearingMessages, FALLBACK_INTERVAL);
142 let receiver = self.context.disappearing_channels().receiver.clone();
143 let mut receiver = receiver.lock().await;
144 loop {
145 while receiver.try_recv().is_ok() {}
147
148 let next = self
149 .context
150 .db()
151 .min_expire_at_ns()
152 .map_err(|e| DisappearingMessagesCleanerError::Storage(e.into()))?;
153 let dur = match next {
157 Some(expire_at) => {
158 Duration::from_nanos((expire_at - now_ns()).max(0) as u64).min(fallback)
159 }
160 None => fallback.saturating_add(xmtp_common::time::rand_offset(jitter)),
161 };
162
163 tokio::select! {
164 _ = receiver.recv() => {}
166 () = xmtp_common::time::sleep(dur) => {
168 self.delete_expired_messages().await?;
169 }
170 }
171 }
172 }
173
174 #[tracing::instrument(skip_all, fields(worker = ?self.kind(), operation = "worker_turn"))]
176 async fn delete_expired_messages(&mut self) -> Result<(), DisappearingMessagesCleanerError> {
177 let db = self.context.db();
178 let deleted_messages = db
180 .delete_expired_messages()
181 .map_err(|e| DisappearingMessagesCleanerError::DeleteExpired(e.into()))?;
182
183 if !deleted_messages.is_empty() {
184 tracing::info!(
185 "Successfully deleted {} expired messages",
186 deleted_messages.len()
187 );
188
189 let _ =
192 self.context
193 .local_events()
194 .send(crate::subscriptions::LocalEvents::MsgsDeleted(
195 deleted_messages,
196 ));
197 }
198
199 Ok(())
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[xmtp_common::test(unwrap_try = true)]
208 async fn rearm_delivers_a_signal() {
209 let ch = DisappearingChannels::new();
210 ch.rearm();
211 let mut rx = ch.receiver.lock().await;
212 assert!(rx.recv().await.is_some());
213 }
214}