1use super::{ConnectionExt, db_connection::DbConnection, schema::tasks};
2use crate::StorageError;
3use derive_builder::Builder;
4use diesel::prelude::*;
5use prost::Message;
6use xmtp_common::{NS_IN_DAY, NS_IN_SEC, time::now_ns};
7use xmtp_proto::types::GroupId;
8use xmtp_proto::xmtp::mls::database::{Task as TaskProto, task::Task as TaskKind};
9
10#[derive(Queryable, Identifiable, Debug, Clone)]
11#[diesel(table_name = tasks)]
12#[diesel(primary_key(id))]
13pub struct Task {
14 pub id: i32,
15 pub originating_message_sequence_id: i64,
16
17 pub created_at_ns: i64,
18 pub expires_at_ns: i64,
19 pub attempts: i32,
20 pub max_attempts: i32,
21 pub last_attempted_at_ns: i64,
22 pub backoff_scaling_factor: f32,
23 pub max_backoff_duration_ns: i64,
24 pub initial_backoff_duration_ns: i64,
25 pub next_attempt_at_ns: i64,
26 pub data_hash: Vec<u8>,
27 pub data: Vec<u8>,
28}
29
30#[derive(Insertable, Debug, PartialEq, Clone, Builder)]
31#[diesel(table_name = tasks)]
32#[builder(build_fn(skip))]
33pub struct NewTask {
34 pub originating_message_sequence_id: i64,
35
36 pub created_at_ns: i64,
37 pub expires_at_ns: i64,
38 pub attempts: i32,
39 pub max_attempts: i32,
40 pub last_attempted_at_ns: i64,
41 pub backoff_scaling_factor: f32,
42 pub max_backoff_duration_ns: i64,
43 pub initial_backoff_duration_ns: i64,
44 pub next_attempt_at_ns: i64,
45 #[builder(setter(skip))]
46 pub data_hash: Vec<u8>,
47 #[builder(setter(skip))]
48 pub data: Vec<u8>,
49}
50
51impl NewTask {
52 pub fn builder() -> NewTaskBuilder {
53 NewTaskBuilder::default()
54 }
55}
56
57impl NewTaskBuilder {
58 pub fn build(&mut self, task: TaskProto) -> Result<NewTask, StorageError> {
59 use derive_builder::UninitializedFieldError;
60 let err = |s: &'static str| UninitializedFieldError::new(s);
61 let data = task.encode_to_vec();
62 let data_hash = xmtp_common::sha256_array(&data).to_vec();
63 let new_task = NewTask {
64 originating_message_sequence_id: self
65 .originating_message_sequence_id
66 .ok_or_else(|| err("originating_message_sequence_id"))?,
67 created_at_ns: self.created_at_ns.unwrap_or_else(now_ns),
68 expires_at_ns: self
69 .expires_at_ns
70 .unwrap_or_else(|| now_ns() + NS_IN_DAY * 3),
71 attempts: self.attempts.unwrap_or(0),
72 max_attempts: self.max_attempts.unwrap_or(20),
73 last_attempted_at_ns: self.last_attempted_at_ns.unwrap_or_else(now_ns),
74 backoff_scaling_factor: self.backoff_scaling_factor.unwrap_or(1.5),
75 max_backoff_duration_ns: self.max_backoff_duration_ns.unwrap_or(60 * NS_IN_SEC),
76 initial_backoff_duration_ns: self.initial_backoff_duration_ns.unwrap_or(2 * NS_IN_SEC),
77 next_attempt_at_ns: self.next_attempt_at_ns.unwrap_or_else(now_ns),
78 data_hash,
79 data,
80 };
81 Ok(new_task)
82 }
83}
84
85#[derive(Clone, Copy, PartialEq, Eq)]
91pub struct TaskDataHash([u8; 32]);
92
93impl TaskDataHash {
94 pub fn to_vec(&self) -> Vec<u8> {
95 self.0.to_vec()
96 }
97}
98
99impl AsRef<[u8]> for TaskDataHash {
100 fn as_ref(&self) -> &[u8] {
101 &self.0
102 }
103}
104
105impl std::fmt::Debug for TaskDataHash {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 write!(f, "TaskDataHash({})", hex::encode(self.0))
108 }
109}
110
111impl TryFrom<&[u8]> for TaskDataHash {
112 type Error = std::array::TryFromSliceError;
113 fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
114 Ok(Self(v.try_into()?))
115 }
116}
117
118pub fn data_hash_for(task: &TaskProto) -> TaskDataHash {
120 let bytes = task.encode_to_vec();
121 debug_assert_eq!(
124 bytes,
125 task.encode_to_vec(),
126 "task payload encoding is nondeterministic — hashes cannot identify rows"
127 );
128 TaskDataHash(xmtp_common::sha256_array(&bytes))
129}
130
131pub const NEVER_EXPIRES: i64 = i64::MAX;
134
135pub trait QueryTasks {
136 fn create_task(&self, task: NewTask) -> Result<Task, StorageError>;
137
138 fn create_or_ignore_task(&self, task: NewTask) -> Result<(), StorageError>;
141
142 fn pull_in_task_deadline(
146 &self,
147 target_data_hash: &TaskDataHash,
148 at_ns: i64,
149 ) -> Result<bool, StorageError>;
150
151 fn get_tasks(&self) -> Result<Vec<Task>, StorageError>;
152
153 fn get_next_task(&self) -> Result<Option<Task>, StorageError>;
154
155 fn upsert_pending_self_remove_task(
161 &self,
162 group_id: &GroupId,
163 task: NewTask,
164 ) -> Result<(), StorageError>;
165
166 fn update_task(
167 &self,
168 id: i32,
169 attempts: i32,
170 last_attempted_at_ns: i64,
171 next_attempt_at_ns: i64,
172 ) -> Result<Task, StorageError>;
173
174 fn delete_task(&self, id: i32) -> Result<bool, StorageError>;
175}
176
177impl<T: QueryTasks> QueryTasks for &'_ T {
178 fn create_task(&self, task: NewTask) -> Result<Task, StorageError> {
179 (**self).create_task(task)
180 }
181
182 fn create_or_ignore_task(&self, task: NewTask) -> Result<(), StorageError> {
183 (**self).create_or_ignore_task(task)
184 }
185
186 fn pull_in_task_deadline(
187 &self,
188 target_data_hash: &TaskDataHash,
189 at_ns: i64,
190 ) -> Result<bool, StorageError> {
191 (**self).pull_in_task_deadline(target_data_hash, at_ns)
192 }
193
194 fn get_tasks(&self) -> Result<Vec<Task>, StorageError> {
195 (**self).get_tasks()
196 }
197
198 fn get_next_task(&self) -> Result<Option<Task>, StorageError> {
199 (**self).get_next_task()
200 }
201
202 fn upsert_pending_self_remove_task(
203 &self,
204 group_id: &GroupId,
205 task: NewTask,
206 ) -> Result<(), StorageError> {
207 (**self).upsert_pending_self_remove_task(group_id, task)
208 }
209
210 fn update_task(
211 &self,
212 id: i32,
213 attempts: i32,
214 last_attempted_at_ns: i64,
215 next_attempt_at_ns: i64,
216 ) -> Result<Task, StorageError> {
217 (**self).update_task(id, attempts, last_attempted_at_ns, next_attempt_at_ns)
218 }
219
220 fn delete_task(&self, id: i32) -> Result<bool, StorageError> {
221 (**self).delete_task(id)
222 }
223}
224
225impl<C: ConnectionExt> QueryTasks for DbConnection<C> {
226 fn create_task(&self, task: NewTask) -> Result<Task, StorageError> {
227 self.raw_query(|conn| {
228 diesel::insert_into(tasks::table)
229 .values(task)
230 .get_result::<Task>(conn)
231 })
232 .map_err(Into::into)
233 }
234
235 fn create_or_ignore_task(&self, task: NewTask) -> Result<(), StorageError> {
236 self.raw_query(|conn| {
238 diesel::insert_or_ignore_into(tasks::table)
239 .values(task)
240 .execute(conn)
241 })?;
242 Ok(())
243 }
244
245 fn pull_in_task_deadline(
246 &self,
247 target_data_hash: &TaskDataHash,
248 at_ns: i64,
249 ) -> Result<bool, StorageError> {
250 use diesel::dsl::sql;
251 use diesel::sql_types::BigInt;
252 let matched = self.raw_query(|conn| {
253 diesel::update(tasks::table.filter(tasks::data_hash.eq(target_data_hash.as_ref())))
254 .set(
255 tasks::next_attempt_at_ns.eq(sql::<BigInt>("MIN(next_attempt_at_ns, ")
256 .bind::<BigInt, _>(at_ns)
257 .sql(")")),
258 )
259 .execute(conn)
260 })?;
261 Ok(matched > 0)
262 }
263
264 fn get_tasks(&self) -> Result<Vec<Task>, StorageError> {
265 self.raw_query(|conn| tasks::table.load::<Task>(conn))
266 .map_err(Into::into)
267 }
268
269 fn get_next_task(&self) -> Result<Option<Task>, StorageError> {
270 self.raw_query(|conn| {
271 tasks::table
272 .order(tasks::next_attempt_at_ns)
273 .first::<Task>(conn)
274 .optional()
275 })
276 .map_err(Into::into)
277 }
278
279 fn upsert_pending_self_remove_task(
280 &self,
281 group_id: &GroupId,
282 task: NewTask,
283 ) -> Result<(), StorageError> {
284 let now = now_ns();
285 self.raw_query(|conn| {
286 conn.transaction(|conn| {
287 let rows: Vec<(i32, i32, i32, i64, Vec<u8>)> = tasks::table
296 .select((
297 tasks::id,
298 tasks::attempts,
299 tasks::max_attempts,
300 tasks::expires_at_ns,
301 tasks::data,
302 ))
303 .load(conn)?;
304 for (id, attempts, max_attempts, expires_at_ns, data) in rows {
305 let is_self_remove = matches!(
306 TaskProto::decode(data.as_slice()).ok().and_then(|t| t.task),
307 Some(TaskKind::ProcessPendingSelfRemove(p)) if p.group_id == group_id.as_slice()
308 );
309 let is_dead = expires_at_ns < now || attempts >= max_attempts;
310 if is_self_remove && is_dead {
311 diesel::delete(tasks::table.filter(tasks::id.eq(id))).execute(conn)?;
312 }
313 }
314 diesel::insert_or_ignore_into(tasks::table)
315 .values(task)
316 .execute(conn)?;
317 Ok(())
318 })
319 })
320 .map_err(Into::into)
321 }
322
323 fn update_task(
324 &self,
325 id: i32,
326 attempts: i32,
327 last_attempted_at_ns: i64,
328 next_attempt_at_ns: i64,
329 ) -> Result<Task, StorageError> {
330 self.raw_query(|conn| {
331 diesel::update(tasks::table.filter(tasks::id.eq(id)))
332 .set((
333 tasks::attempts.eq(attempts),
334 tasks::last_attempted_at_ns.eq(last_attempted_at_ns),
335 tasks::next_attempt_at_ns.eq(next_attempt_at_ns),
336 ))
337 .get_result::<Task>(conn)
338 })
339 .map_err(Into::into)
340 }
341
342 fn delete_task(&self, id: i32) -> Result<bool, StorageError> {
343 let num_deleted = self.raw_query(|conn| {
344 diesel::delete(tasks::table.filter(tasks::id.eq(id))).execute(conn)
345 })?;
346 Ok(num_deleted == 1)
347 }
348}
349
350#[cfg(test)]
351pub(crate) mod tests {
352 use super::*;
353 use crate::test_utils::with_connection;
354
355 #[xmtp_common::test]
356 fn get_tasks_returns_empty_list_initially() {
357 with_connection(|conn| {
358 let tasks = conn.get_tasks().unwrap();
359 assert!(tasks.is_empty());
360 })
361 }
362
363 #[xmtp_common::test]
364 fn update_task_returns_error_when_not_found() {
365 with_connection(|conn| {
366 let result = conn.update_task(999, 5, 1000, 2000);
368 assert!(result.is_err());
370 })
371 }
372
373 #[xmtp_common::test]
374 fn delete_task_returns_false_when_not_found() {
375 with_connection(|conn| {
376 let deleted = conn.delete_task(999).unwrap();
377 assert!(!deleted);
378 })
379 }
380
381 fn gen_task_data() -> TaskProto {
383 TaskProto {
384 task: Some(xmtp_proto::xmtp::mls::database::task::Task::PullInDeadline(
385 xmtp_proto::xmtp::mls::database::PullInDeadline {
386 target_data_hash: xmtp_common::rand_vec::<32>(),
387 not_later_than_ns: 1000,
388 },
389 )),
390 }
391 }
392
393 #[xmtp_common::test]
394 fn all_task_operations_work_together() {
395 with_connection(|conn| {
396 let now = xmtp_common::time::now_ns();
397
398 let task1 = NewTaskBuilder::default()
400 .originating_message_sequence_id(1)
401 .created_at_ns(now)
402 .expires_at_ns(now + 3_600_000_000_000)
403 .attempts(0)
404 .max_attempts(5)
405 .last_attempted_at_ns(0)
406 .backoff_scaling_factor(1.5)
407 .max_backoff_duration_ns(600_000_000_000)
408 .initial_backoff_duration_ns(2_000_000_000)
409 .next_attempt_at_ns(now + 1000) .build(gen_task_data())
411 .unwrap();
412
413 let task2 = NewTaskBuilder::default()
415 .originating_message_sequence_id(2)
416 .created_at_ns(now)
417 .expires_at_ns(now + 7_200_000_000_000) .attempts(0)
419 .max_attempts(3)
420 .last_attempted_at_ns(0)
421 .backoff_scaling_factor(2.0)
422 .max_backoff_duration_ns(300_000_000_000)
423 .initial_backoff_duration_ns(1_000_000_000)
424 .next_attempt_at_ns(now + 500) .build(gen_task_data())
426 .unwrap();
427
428 assert!(conn.get_next_task().unwrap().is_none());
430 assert!(conn.get_tasks().unwrap().is_empty());
431
432 let created_task1 = conn.create_task(task1).unwrap();
434 let created_task2 = conn.create_task(task2).unwrap();
435
436 let task1_id = created_task1.id;
437 let task2_id = created_task2.id;
438 assert!(task1_id >= 0, "task1_id: {task1_id}");
439 assert!(task2_id >= 0, "task2_id: {task2_id}");
440 assert_ne!(task1_id, task2_id);
441
442 let all_tasks = conn.get_tasks().unwrap();
444 assert_eq!(all_tasks.len(), 2);
445
446 let next_task = conn.get_next_task().unwrap();
448 assert!(next_task.is_some());
449 let next_task = next_task.unwrap();
450 assert_eq!(next_task.id, task2_id);
451 assert_eq!(next_task.next_attempt_at_ns, now + 500);
452
453 let updated_task1 = conn
455 .update_task(
456 task1_id,
457 1, now + 2000, now + 200, )
461 .unwrap();
462
463 assert_eq!(updated_task1.id, task1_id);
465 assert_eq!(updated_task1.attempts, 1);
466 assert_eq!(updated_task1.next_attempt_at_ns, now + 200);
467
468 let next_task = conn.get_next_task().unwrap();
470 assert!(next_task.is_some());
471 let next_task = next_task.unwrap();
472 assert_eq!(next_task.id, task1_id);
473 assert_eq!(next_task.next_attempt_at_ns, now + 200);
474
475 let all_tasks_after_update = conn.get_tasks().unwrap();
477 assert_eq!(all_tasks_after_update.len(), 2);
478
479 let updated_task1_in_list = all_tasks_after_update
481 .iter()
482 .find(|t| t.id == task1_id)
483 .unwrap();
484 let task2_in_list = all_tasks_after_update
485 .iter()
486 .find(|t| t.id == task2_id)
487 .unwrap();
488
489 assert_eq!(updated_task1_in_list.attempts, 1);
490 assert_eq!(updated_task1_in_list.next_attempt_at_ns, now + 200);
491 assert_eq!(task2_in_list.attempts, 0);
492 assert_eq!(task2_in_list.next_attempt_at_ns, now + 500);
493
494 let deleted = conn.delete_task(task1_id).unwrap();
496 assert!(deleted);
497
498 let next_task = conn.get_next_task().unwrap();
500 assert!(next_task.is_some());
501 let next_task = next_task.unwrap();
502 assert_eq!(next_task.id, task2_id);
503
504 let remaining_tasks = conn.get_tasks().unwrap();
506 assert_eq!(remaining_tasks.len(), 1);
507 assert_eq!(remaining_tasks[0].id, task2_id);
508
509 let deleted = conn.delete_task(task2_id).unwrap();
511 assert!(deleted);
512
513 let all_tasks_after_delete = conn.get_tasks().unwrap();
515 assert!(all_tasks_after_delete.is_empty());
516 assert!(conn.get_next_task().unwrap().is_none());
517
518 let deleted_again = conn.delete_task(task1_id).unwrap();
520 assert!(!deleted_again);
521 })
522 }
523
524 #[xmtp_common::test]
525 fn data_hash_for_matches_builder() {
526 let proto = gen_task_data();
527 let task = NewTask::builder()
528 .originating_message_sequence_id(0)
529 .build(proto.clone())
530 .unwrap();
531 assert_eq!(task.data_hash, data_hash_for(&proto).as_ref());
532 }
533
534 #[xmtp_common::test]
539 fn data_hash_encoding_is_pinned() {
540 use xmtp_proto::xmtp::mls::database::{
541 AddMissingInstallations, KpDeletion, KpRotation, PullInDeadline,
542 };
543 let rotation = TaskProto {
544 task: Some(TaskKind::KpRotation(KpRotation {})),
545 };
546 let deletion = TaskProto {
547 task: Some(TaskKind::KpDeletion(KpDeletion {})),
548 };
549 assert_eq!(rotation.encode_to_vec(), [0x2a, 0x00]);
551 assert_eq!(deletion.encode_to_vec(), [0x32, 0x00]);
552 assert_eq!(
553 hex::encode(data_hash_for(&rotation)),
554 "17d5f5a33ab5f6aed0395d2bc0a4e5df61d92441ea8d77b0952c01bc8aa8bde0"
555 );
556 assert_eq!(
557 hex::encode(data_hash_for(&deletion)),
558 "913da1f8df6f8fd47593840d533ba0458cc9873996bf310460abb495b34c232a"
559 );
560 let pull_in = TaskProto {
562 task: Some(TaskKind::PullInDeadline(PullInDeadline {
563 target_data_hash: vec![0x11; 32],
564 not_later_than_ns: 1_234_567_890,
565 })),
566 };
567 assert_eq!(
568 hex::encode(data_hash_for(&pull_in)),
569 "16b424873a34096e5157ab9f0a31e80dff1d23ebd8b1aab2b948a4732abfc849"
570 );
571 let add = TaskProto {
575 task: Some(TaskKind::AddMissingInstallations(AddMissingInstallations {
576 group_id: vec![0x22; 32],
577 })),
578 };
579 assert_eq!(add.encode_to_vec()[..4], [0x3a, 0x22, 0x0a, 0x20]);
580 assert_eq!(
581 hex::encode(data_hash_for(&add)),
582 "2180ffa08703d0dcc600a070da9d162da6821e29176af24e7a9955af2cf43764"
583 );
584 let bytes = pull_in.encode_to_vec();
586 for _ in 0..100 {
587 assert_eq!(pull_in.encode_to_vec(), bytes);
588 }
589 let decoded = TaskProto::decode(bytes.as_slice()).unwrap();
590 assert_eq!(decoded.encode_to_vec(), bytes);
591 }
592
593 #[xmtp_common::test]
594 fn create_or_ignore_task_is_idempotent() {
595 with_connection(|conn| {
596 let proto = gen_task_data();
597 let mk = || {
598 NewTask::builder()
599 .originating_message_sequence_id(0)
600 .build(proto.clone())
601 .unwrap()
602 };
603 conn.create_or_ignore_task(mk()).unwrap();
604 conn.create_or_ignore_task(mk()).unwrap();
607 assert_eq!(conn.get_tasks().unwrap().len(), 1);
608 })
609 }
610
611 #[xmtp_common::test]
612 fn pull_in_lowers_deadline() {
613 with_connection(|conn| {
614 let proto = gen_task_data();
615 let now = now_ns();
616 let task = NewTask::builder()
617 .originating_message_sequence_id(0)
618 .next_attempt_at_ns(now + NS_IN_DAY)
619 .build(proto.clone())
620 .unwrap();
621 conn.create_or_ignore_task(task).unwrap();
622 let hash = data_hash_for(&proto);
623
624 assert!(conn.pull_in_task_deadline(&hash, now + 5).unwrap());
626 assert_eq!(
627 conn.get_next_task().unwrap().unwrap().next_attempt_at_ns,
628 now + 5
629 );
630
631 assert!(conn.pull_in_task_deadline(&hash, now + NS_IN_DAY).unwrap());
633 assert_eq!(
634 conn.get_next_task().unwrap().unwrap().next_attempt_at_ns,
635 now + 5
636 );
637
638 let absent = TaskDataHash::try_from([0xAAu8; 32].as_slice()).unwrap();
640 assert!(!conn.pull_in_task_deadline(&absent, now).unwrap());
641 assert_eq!(
642 conn.get_next_task().unwrap().unwrap().next_attempt_at_ns,
643 now + 5
644 );
645 })
646 }
647
648 #[xmtp_common::test(unwrap_try = true)]
649 fn upsert_pending_self_remove_dedups_per_group() {
650 use xmtp_proto::xmtp::mls::database::ProcessPendingSelfRemove;
651 let build = |gid: &GroupId| {
652 let proto = TaskProto {
653 task: Some(TaskKind::ProcessPendingSelfRemove(
654 ProcessPendingSelfRemove {
655 group_id: gid.to_vec(),
656 },
657 )),
658 };
659 NewTask::builder()
660 .originating_message_sequence_id(0)
661 .build(proto)
662 .unwrap()
663 };
664 with_connection(|conn| {
665 conn.upsert_pending_self_remove_task(&GroupId::ONE, build(&GroupId::ONE))?;
667 conn.upsert_pending_self_remove_task(&GroupId::ONE, build(&GroupId::ONE))?;
668 assert_eq!(conn.get_tasks()?.len(), 1);
669
670 conn.upsert_pending_self_remove_task(&GroupId::TWO, build(&GroupId::TWO))?;
672 assert_eq!(conn.get_tasks()?.len(), 2);
673 })
674 }
675
676 #[xmtp_common::test(unwrap_try = true)]
677 fn upsert_preserves_live_task_but_replaces_dead_one() {
678 use xmtp_proto::xmtp::mls::database::ProcessPendingSelfRemove;
679 let proto = |gid: &GroupId| TaskProto {
680 task: Some(TaskKind::ProcessPendingSelfRemove(
681 ProcessPendingSelfRemove {
682 group_id: gid.to_vec(),
683 },
684 )),
685 };
686 with_connection(|conn| {
687 let now = now_ns();
689 let live = NewTask::builder()
690 .originating_message_sequence_id(0)
691 .attempts(2)
692 .next_attempt_at_ns(now + NS_IN_DAY)
693 .build(proto(&GroupId::ONE))?;
694 conn.create_task(live)?;
695
696 conn.upsert_pending_self_remove_task(&GroupId::ONE, {
698 NewTask::builder()
699 .originating_message_sequence_id(0)
700 .next_attempt_at_ns(now)
701 .build(proto(&GroupId::ONE))?
702 })?;
703 let tasks = conn.get_tasks()?;
704 assert_eq!(tasks.len(), 1);
705 assert_eq!(tasks[0].attempts, 2);
706 assert_eq!(tasks[0].next_attempt_at_ns, now + NS_IN_DAY);
707
708 let dead = NewTask::builder()
710 .originating_message_sequence_id(0)
711 .attempts(20)
712 .max_attempts(20)
713 .build(proto(&GroupId::TWO))?;
714 conn.create_task(dead)?;
715 conn.upsert_pending_self_remove_task(&GroupId::TWO, {
716 NewTask::builder()
717 .originating_message_sequence_id(0)
718 .attempts(0)
719 .build(proto(&GroupId::TWO))?
720 })?;
721 let two: Vec<_> = conn
722 .get_tasks()?
723 .into_iter()
724 .filter(|t| {
725 matches!(
726 TaskProto::decode(t.data.as_slice()).ok().and_then(|p| p.task),
727 Some(TaskKind::ProcessPendingSelfRemove(p)) if p.group_id == GroupId::TWO.as_slice()
728 )
729 })
730 .collect();
731 assert_eq!(two.len(), 1);
732 assert_eq!(two[0].attempts, 0);
733 })
734 }
735}