1use std::collections::HashMap;
2
3use diesel::{
4 backend::Backend,
5 deserialize::{self, FromSql, FromSqlRow},
6 expression::AsExpression,
7 prelude::*,
8 serialize::{self, IsNull, Output, ToSql},
9 sql_types::Integer,
10};
11use xmtp_proto::types::Cursor;
12
13use super::{ConnectionExt, Sqlite, db_connection::DbConnection, schema::refresh_state};
14use crate::{StorageError, StoreOrIgnore, impl_store_or_ignore};
15
16#[repr(i32)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq, AsExpression, Hash, FromSqlRow)]
18#[diesel(sql_type = Integer)]
19pub enum EntityKind {
20 Welcome = 1,
21 ApplicationMessage = 2, CommitLogUpload = 3, CommitLogDownload = 4, CommitLogForkCheckLocal = 5, CommitLogForkCheckRemote = 6, Identity = 8,
27 DeliveryAllocator = 9,
28 Delivery = 10,
29}
30
31pub trait HasEntityKind {
32 fn entity_kind(&self) -> EntityKind;
33}
34
35impl HasEntityKind for xmtp_proto::types::GroupMessage {
36 fn entity_kind(&self) -> EntityKind {
37 EntityKind::ApplicationMessage
38 }
39}
40
41impl HasEntityKind for xmtp_proto::types::WelcomeMessage {
42 fn entity_kind(&self) -> EntityKind {
43 EntityKind::Welcome
44 }
45}
46
47impl std::fmt::Display for EntityKind {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 use EntityKind::*;
50 match self {
51 Welcome => write!(f, "welcome"),
52 ApplicationMessage => write!(f, "group"),
53 CommitLogUpload => write!(f, "commit_log_upload"),
54 CommitLogDownload => write!(f, "commit_log_download"),
55 CommitLogForkCheckLocal => write!(f, "commit_log_fork_check_local"),
56 CommitLogForkCheckRemote => write!(f, "commit_log_fork_check_remote"),
57 Identity => write!(f, "identity"),
58 DeliveryAllocator => write!(f, "delivery_allocator"),
59 Delivery => write!(f, "delivery"),
60 }
61 }
62}
63
64impl ToSql<Integer, Sqlite> for EntityKind
65where
66 i32: ToSql<Integer, Sqlite>,
67{
68 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
69 out.set_value(*self as i32);
70 Ok(IsNull::No)
71 }
72}
73
74impl FromSql<Integer, Sqlite> for EntityKind
75where
76 i32: FromSql<Integer, Sqlite>,
77{
78 fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
79 match i32::from_sql(bytes)? {
80 1 => Ok(EntityKind::Welcome),
81 2 => Ok(EntityKind::ApplicationMessage),
82 3 => Ok(EntityKind::CommitLogUpload),
83 4 => Ok(EntityKind::CommitLogDownload),
84 5 => Ok(EntityKind::CommitLogForkCheckLocal),
85 6 => Ok(EntityKind::CommitLogForkCheckRemote),
86 8 => Ok(EntityKind::Identity),
87 9 => Ok(EntityKind::DeliveryAllocator),
88 10 => Ok(EntityKind::Delivery),
89 x => Err(format!("Unrecognized variant {}", x).into()),
90 }
91 }
92}
93
94#[derive(Insertable, Identifiable, Queryable, Selectable, Debug, Clone)]
95#[diesel(table_name = refresh_state)]
96#[diesel(primary_key(entity_id, entity_kind))]
97pub struct RefreshState {
98 pub entity_id: Vec<u8>,
99 pub entity_kind: EntityKind,
100 pub sequence_id: i64,
102 pub received_sequence_id: Option<i64>,
104}
105
106impl_store_or_ignore!(RefreshState, refresh_state);
107
108pub trait QueryRefreshState {
109 fn get_refresh_state<Id: AsRef<[u8]>>(
110 &self,
111 entity_id: Id,
112 entity_kind: EntityKind,
113 ) -> Result<Option<RefreshState>, StorageError>;
114
115 fn get_last_cursor<Id: AsRef<[u8]>>(
117 &self,
118 id: Id,
119 entity_kind: EntityKind,
120 ) -> Result<Cursor, StorageError>;
121
122 fn get_last_cursor_for_ids<Id: AsRef<[u8]>>(
125 &self,
126 ids: &[Id],
127 entities: &[EntityKind],
128 ) -> Result<HashMap<Vec<u8>, Cursor>, StorageError>;
129
130 fn update_cursor<Id: AsRef<[u8]>>(
132 &self,
133 entity_id: Id,
134 entity_kind: EntityKind,
135 cursor: Cursor,
136 ) -> Result<bool, StorageError>;
137
138 fn latest_cursor_for_id<Id: AsRef<[u8]>>(
139 &self,
140 entity_id: Id,
141 entities: &[EntityKind],
142 ) -> Result<Cursor, StorageError> {
143 Ok(self
144 .get_last_cursor_for_ids(&[entity_id.as_ref()], entities)?
145 .remove(entity_id.as_ref())
146 .unwrap_or_default())
147 }
148
149 fn get_remote_log_cursors(
150 &self,
151 conversation_ids: &[&[u8]],
152 ) -> Result<HashMap<Vec<u8>, Cursor>, StorageError> {
153 conversation_ids
154 .iter()
155 .map(|id| {
156 self.get_last_cursor(id, EntityKind::CommitLogDownload)
157 .map(|cursor| (id.to_vec(), cursor))
158 })
159 .collect()
160 }
161}
162
163impl<T: QueryRefreshState> QueryRefreshState for &T {
164 fn get_refresh_state<Id: AsRef<[u8]>>(
165 &self,
166 entity_id: Id,
167 entity_kind: EntityKind,
168 ) -> Result<Option<RefreshState>, StorageError> {
169 (**self).get_refresh_state(entity_id, entity_kind)
170 }
171
172 fn get_last_cursor<Id: AsRef<[u8]>>(
173 &self,
174 id: Id,
175 entity_kind: EntityKind,
176 ) -> Result<Cursor, StorageError> {
177 (**self).get_last_cursor(id, entity_kind)
178 }
179
180 fn get_last_cursor_for_ids<Id: AsRef<[u8]>>(
181 &self,
182 ids: &[Id],
183 entities: &[EntityKind],
184 ) -> Result<HashMap<Vec<u8>, Cursor>, StorageError> {
185 (**self).get_last_cursor_for_ids(ids, entities)
186 }
187
188 fn update_cursor<Id: AsRef<[u8]>>(
189 &self,
190 entity_id: Id,
191 entity_kind: EntityKind,
192 cursor: Cursor,
193 ) -> Result<bool, StorageError> {
194 (**self).update_cursor(entity_id, entity_kind, cursor)
195 }
196}
197
198impl<C: ConnectionExt> QueryRefreshState for DbConnection<C> {
199 #[xmtp_common::db_span]
200 fn get_refresh_state<Id: AsRef<[u8]>>(
201 &self,
202 entity_id: Id,
203 entity_kind: EntityKind,
204 ) -> Result<Option<RefreshState>, StorageError> {
205 Ok(self.raw_query(|conn| {
206 refresh_state::table
207 .find((entity_id.as_ref(), entity_kind))
208 .select(RefreshState::as_select())
209 .first(conn)
210 .optional()
211 })?)
212 }
213
214 #[xmtp_common::db_span]
215 fn get_last_cursor<Id: AsRef<[u8]>>(
216 &self,
217 id: Id,
218 entity_kind: EntityKind,
219 ) -> Result<Cursor, StorageError> {
220 RefreshState {
221 entity_id: id.as_ref().to_vec(),
222 entity_kind,
223 sequence_id: 0,
224 received_sequence_id: None,
225 }
226 .store_or_ignore(self)?;
227 Ok(Cursor(
228 self.get_refresh_state(id, entity_kind)?
229 .ok_or(StorageError::DbDeserialize)?
230 .sequence_id as u64,
231 ))
232 }
233
234 #[xmtp_common::db_span]
235 fn get_last_cursor_for_ids<Id: AsRef<[u8]>>(
236 &self,
237 ids: &[Id],
238 entities: &[EntityKind],
239 ) -> Result<HashMap<Vec<u8>, Cursor>, StorageError> {
240 use super::schema::refresh_state::dsl;
241 use diesel::dsl::{count, min};
242 use std::collections::HashSet;
243
244 const IDS_PER_QUERY: usize = 900;
246 let entities: HashSet<_> = entities.iter().copied().collect();
247 if entities.is_empty() {
248 return Ok(HashMap::new());
249 }
250 Ok(self.raw_query(|conn| {
251 let mut result = HashMap::new();
252 for chunk in ids.chunks(IDS_PER_QUERY) {
253 let ids: Vec<_> = chunk.iter().map(AsRef::as_ref).collect();
254 let rows = dsl::refresh_state
255 .filter(dsl::entity_kind.eq_any(&entities))
256 .filter(dsl::entity_id.eq_any(ids))
257 .group_by(dsl::entity_id)
258 .select((
259 dsl::entity_id,
260 min(dsl::sequence_id),
261 count(dsl::entity_kind),
262 ))
263 .load::<(Vec<u8>, Option<i64>, i64)>(conn)?;
264 for (id, sequence, kinds) in rows {
265 let sequence = if kinds as usize == entities.len() {
266 sequence.unwrap_or_default() as u64
267 } else {
268 0
269 };
270 result.insert(id, Cursor(sequence));
271 }
272 }
273 Ok(result)
274 })?)
275 }
276
277 #[xmtp_common::db_span]
278 fn update_cursor<Id: AsRef<[u8]>>(
279 &self,
280 entity_id: Id,
281 entity_kind: EntityKind,
282 cursor: Cursor,
283 ) -> Result<bool, StorageError> {
284 use super::schema::refresh_state::dsl;
285 use diesel::{query_dsl::methods::FilterDsl, upsert::excluded};
286 let state = RefreshState {
287 entity_id: entity_id.as_ref().to_vec(),
288 entity_kind,
289 sequence_id: i64::try_from(cursor.0).map_err(|_| StorageError::DbSerialize)?,
290 received_sequence_id: None,
291 };
292 Ok(self.raw_query(|conn| {
293 diesel::insert_into(dsl::refresh_state)
294 .values(&state)
295 .on_conflict((dsl::entity_id, dsl::entity_kind))
296 .do_update()
297 .set(dsl::sequence_id.eq(excluded(dsl::sequence_id)))
298 .filter(dsl::sequence_id.lt(excluded(dsl::sequence_id)))
299 .execute(conn)
300 })? > 0)
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::test_utils::with_connection;
308
309 #[xmtp_common::test(unwrap_try = true)]
310 fn cursor_defaults_and_advances_only_in_order() {
311 with_connection(|conn| {
312 let id = [1, 2, 3];
313 let kind = EntityKind::ApplicationMessage;
314 assert!(conn.get_refresh_state(id, kind).unwrap().is_none());
315 assert_eq!(conn.get_last_cursor(id, kind).unwrap(), Cursor(0));
316 assert!(conn.get_refresh_state(id, kind).unwrap().is_some());
317 assert!(conn.update_cursor(id, kind, Cursor(123)).unwrap());
318 assert!(!conn.update_cursor(id, kind, Cursor(122)).unwrap());
319 assert!(!conn.update_cursor(id, kind, Cursor(123)).unwrap());
320 assert!(conn.update_cursor(id, kind, Cursor(124)).unwrap());
321 assert_eq!(conn.get_last_cursor(id, kind).unwrap(), Cursor(124));
322 });
323 }
324
325 #[rstest::rstest]
326 #[case(Some(500), Some(250), 250)]
327 #[case(Some(100), Some(200), 100)]
328 #[case(Some(500), None, 0)]
329 #[case(None, Some(250), 0)]
330 #[case(None, None, 0)]
331 #[xmtp_common::test(unwrap_try = true)]
332 async fn cursor_meets_requested_kinds(
333 #[case] application: Option<u64>,
334 #[case] identity: Option<u64>,
335 #[case] expected: u64,
336 ) {
337 with_connection(|conn| {
338 let id = [1, 2, 3];
339 for (kind, value) in [
340 (EntityKind::ApplicationMessage, application),
341 (EntityKind::Identity, identity),
342 ] {
343 if let Some(value) = value {
344 conn.update_cursor(id, kind, Cursor(value)).unwrap();
345 }
346 }
347 conn.update_cursor(id, EntityKind::Welcome, Cursor(999))
348 .unwrap();
349 let kinds = [EntityKind::ApplicationMessage, EntityKind::Identity];
350 assert_eq!(
351 conn.latest_cursor_for_id(id, &kinds).unwrap(),
352 Cursor(expected)
353 );
354 assert_eq!(
355 conn.latest_cursor_for_id(id, &[EntityKind::Welcome])
356 .unwrap(),
357 Cursor(999)
358 );
359 });
360 }
361
362 #[rstest::rstest]
363 #[case(0)]
364 #[case(1)]
365 #[case(900)]
366 #[case(1000)]
367 #[case(2000)]
368 #[xmtp_common::test(unwrap_try = true)]
369 async fn cursor_queries_batch_ids(#[case] count: u64) {
370 with_connection(|conn| {
371 let ids: Vec<_> = (0..count).map(u64::to_be_bytes).collect();
372 for (index, id) in ids.iter().enumerate() {
373 conn.update_cursor(id, EntityKind::ApplicationMessage, Cursor(index as u64))
374 .unwrap();
375 }
376 let found = conn
377 .get_last_cursor_for_ids(&ids, &[EntityKind::ApplicationMessage])
378 .unwrap();
379 assert_eq!(found.len(), ids.len());
380 for (index, id) in ids.iter().enumerate() {
381 assert_eq!(found.get(id.as_slice()), Some(&Cursor(index as u64)));
382 }
383 let missing = conn
384 .get_last_cursor_for_ids(&[[255; 8]], &[EntityKind::ApplicationMessage])
385 .unwrap();
386 assert!(missing.is_empty());
387 });
388 }
389}