1use crate::ids::{RequestId, RoomId};
126use crate::persist::{FlushBatch, FlushEpoch, RequiredSeq};
127use crate::store::{StateStore, StoreError};
128use crate::wire::{HttpResponseDescriptor, OutgoingRequest, OutgoingRequestKind};
129use serde::{Deserialize, Serialize};
130use std::collections::{BTreeMap, BTreeSet};
131
132const OTHER_LANE_CONCURRENCY: usize = 4;
134
135const BACKOFF_BASE_MS: u64 = 1_000;
138
139const BACKOFF_CAP_MS: u64 = 60_000;
142
143#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
147pub enum Lane {
148 Sync,
150 ToDevice,
153 Room(RoomId),
156 AccountData,
161 Other,
164}
165
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
172pub struct PendingRequest {
173 pub request: OutgoingRequest,
175 pub lane: Lane,
177 pub required_seq: RequiredSeq,
180}
181
182#[derive(Clone, Debug, PartialEq, Eq)]
185pub enum ResponseOutcome {
186 Done(HttpResponseDescriptor),
188 Retry {
194 after_ms: u64,
196 },
197 Failed {
201 errcode: String,
204 error: String,
206 },
207}
208
209pub trait Jitter {
215 fn next_unit(&mut self) -> f64;
218}
219
220fn capped_exponential_backoff_ms(attempt: u32, jitter_unit: f64) -> u64 {
229 let shift = attempt.min(6);
230 let base = BACKOFF_BASE_MS.saturating_mul(1u64 << shift).min(BACKOFF_CAP_MS);
231 let unit = jitter_unit.clamp(0.0, 1.0);
232 let jittered = (base as f64 * (0.5 + 0.5 * unit)).round() as u64;
233 jittered.min(BACKOFF_CAP_MS)
234}
235
236fn retry_after_ms_from_body(response: &HttpResponseDescriptor) -> Option<u64> {
239 let value: serde_json::Value = serde_json::from_slice(&response.body).ok()?;
240 value.get("retry_after_ms")?.as_u64()
241}
242
243#[derive(Debug, Default)]
245pub struct OutgoingQueue {
246 order: Vec<RequestId>,
251 pending: BTreeMap<RequestId, PendingRequest>,
253 in_flight: BTreeSet<RequestId>,
257 sync_in_flight: bool,
258 to_device_in_flight: bool,
259 room_in_flight: BTreeSet<RoomId>,
260 other_in_flight: BTreeSet<RequestId>,
261 attempts: BTreeMap<RequestId, u32>,
265 retry_not_before_ms: BTreeMap<RequestId, i64>,
269}
270
271impl OutgoingQueue {
272 pub fn new() -> Self {
275 Self::default()
276 }
277
278 pub fn load<S: StateStore>(store: &S) -> Result<Self, StoreError> {
283 let mut records = store.pending_requests()?;
284 records.sort_by(|a, b| a.request.id.cmp(&b.request.id));
285 let mut queue = Self::new();
286 for mut record in records {
287 if record.lane == Lane::Other
291 && matches!(record.request.kind, OutgoingRequestKind::AccountData | OutgoingRequestKind::RoomAccountData)
292 {
293 record.lane = Lane::AccountData;
294 }
295 let id = record.request.id.clone();
296 queue.order.push(id.clone());
297 queue.pending.insert(id, record);
298 }
299 Ok(queue)
300 }
301
302 pub fn discard_kind<S: StateStore>(&mut self, store: &mut S, kind: OutgoingRequestKind) -> Result<(), StoreError> {
307 let ids: Vec<RequestId> = self.pending.iter().filter(|(_, record)| record.request.kind == kind).map(|(id, _)| id.clone()).collect();
308 for id in ids {
309 self.complete(store, &id)?;
310 }
311 Ok(())
312 }
313
314 pub fn enqueue<S: StateStore + FlushEpoch>(
327 &mut self,
328 store: &mut S,
329 request: OutgoingRequest,
330 lane: Lane,
331 ) -> Result<Option<FlushBatch>, StoreError> {
332 if self.pending.contains_key(&request.id) {
333 return Ok(None);
334 }
335 let (required_seq, batch) = store.seal_for_request();
336 let id = request.id.clone();
337 let record = PendingRequest { request, lane, required_seq };
338 store.save_pending_request(record.clone())?;
339 self.pending.insert(id.clone(), record);
340 self.order.push(id);
341 Ok(batch)
342 }
343
344 pub fn releasable<S: FlushEpoch>(&mut self, store: &S, now_ms: i64) -> Vec<OutgoingRequest> {
353 let mut out = Vec::new();
354 let ids = self.order.clone();
355 let mut account_data_blocked = false;
356 for id in ids {
357 let Some(pending) = self.pending.get(&id) else { continue };
358 if pending.lane == Lane::AccountData {
359 if account_data_blocked {
363 continue;
364 }
365 account_data_blocked = true;
366 }
367 if self.in_flight.contains(&id) {
368 continue;
369 }
370 if let Some(¬_before) = self.retry_not_before_ms.get(&id) {
371 if now_ms < not_before {
372 continue;
373 }
374 }
375 if !store.is_released(pending.required_seq) {
376 continue;
377 }
378 if !self.lane_available(&pending.lane) {
379 continue;
380 }
381 let request = pending.request.clone();
382 let lane = pending.lane.clone();
383 self.mark_in_flight(&id, &lane);
384 out.push(request);
385 }
386 out
387 }
388
389 pub fn on_response<S: StateStore>(
394 &mut self,
395 store: &mut S,
396 request_id: &RequestId,
397 response: HttpResponseDescriptor,
398 now_ms: i64,
399 jitter: &mut dyn Jitter,
400 ) -> Result<Option<ResponseOutcome>, StoreError> {
401 let Some(pending) = self.pending.get(request_id).cloned() else { return Ok(None) };
402
403 if (200..300).contains(&response.status) {
404 self.complete(store, request_id)?;
405 return Ok(Some(ResponseOutcome::Done(response)));
406 }
407
408 if response.status == 429 {
409 self.clear_in_flight(request_id, &pending.lane);
410 let after_ms =
411 retry_after_ms_from_body(&response).unwrap_or_else(|| self.next_backoff_ms(request_id, jitter));
412 self.retry_not_before_ms.insert(request_id.clone(), now_ms.saturating_add(after_ms as i64));
413 return Ok(Some(ResponseOutcome::Retry { after_ms }));
414 }
415
416 if (500..600).contains(&response.status) {
417 self.clear_in_flight(request_id, &pending.lane);
418 let after_ms = self.next_backoff_ms(request_id, jitter);
419 self.retry_not_before_ms.insert(request_id.clone(), now_ms.saturating_add(after_ms as i64));
420 return Ok(Some(ResponseOutcome::Retry { after_ms }));
421 }
422
423 let (errcode, error) =
424 response.matrix_error().unwrap_or_else(|| ("M_UNKNOWN".to_string(), String::new()));
425 self.complete(store, request_id)?;
426 Ok(Some(ResponseOutcome::Failed { errcode, error }))
427 }
428
429 pub fn on_transport_error(
435 &mut self,
436 request_id: &RequestId,
437 now_ms: i64,
438 jitter: &mut dyn Jitter,
439 ) -> Option<ResponseOutcome> {
440 let pending = self.pending.get(request_id)?.clone();
441 self.clear_in_flight(request_id, &pending.lane);
442 let after_ms = self.next_backoff_ms(request_id, jitter);
443 self.retry_not_before_ms.insert(request_id.clone(), now_ms.saturating_add(after_ms as i64));
444 Some(ResponseOutcome::Retry { after_ms })
445 }
446
447 fn next_backoff_ms(&mut self, request_id: &RequestId, jitter: &mut dyn Jitter) -> u64 {
448 let attempt = self.attempts.entry(request_id.clone()).or_insert(0);
449 let ms = capped_exponential_backoff_ms(*attempt, jitter.next_unit());
450 *attempt = attempt.saturating_add(1);
451 ms
452 }
453
454 fn lane_available(&self, lane: &Lane) -> bool {
455 match lane {
456 Lane::Sync => !self.sync_in_flight,
457 Lane::ToDevice => !self.to_device_in_flight,
458 Lane::Room(room_id) => !self.room_in_flight.contains(room_id),
459 Lane::AccountData => true,
461 Lane::Other => self.other_in_flight.len() < OTHER_LANE_CONCURRENCY,
462 }
463 }
464
465 fn mark_in_flight(&mut self, id: &RequestId, lane: &Lane) {
466 match lane {
467 Lane::Sync => self.sync_in_flight = true,
468 Lane::ToDevice => self.to_device_in_flight = true,
469 Lane::Room(room_id) => {
470 self.room_in_flight.insert(room_id.clone());
471 }
472 Lane::AccountData => {}
473 Lane::Other => {
474 self.other_in_flight.insert(id.clone());
475 }
476 }
477 self.in_flight.insert(id.clone());
478 }
479
480 fn clear_in_flight(&mut self, id: &RequestId, lane: &Lane) {
481 match lane {
482 Lane::Sync => self.sync_in_flight = false,
483 Lane::ToDevice => self.to_device_in_flight = false,
484 Lane::Room(room_id) => {
485 self.room_in_flight.remove(room_id);
486 }
487 Lane::AccountData => {}
488 Lane::Other => {
489 self.other_in_flight.remove(id);
490 }
491 }
492 self.in_flight.remove(id);
493 }
494
495 fn complete<S: StateStore>(&mut self, store: &mut S, id: &RequestId) -> Result<(), StoreError> {
496 if let Some(pending) = self.pending.remove(id) {
497 self.clear_in_flight(id, &pending.lane);
498 self.attempts.remove(id);
499 self.retry_not_before_ms.remove(id);
500 self.order.retain(|existing| existing != id);
501 store.delete_pending_request(id)?;
502 }
503 Ok(())
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use crate::ids::{DeviceId, RequestId, RoomId, TxnId};
511 use crate::store::{CryptoStore, InsecurePlainCodecForTests, Store};
512 use crate::wire::OutgoingRequest;
513
514 struct FixedJitter(f64);
515
516 impl Jitter for FixedJitter {
517 fn next_unit(&mut self) -> f64 {
518 self.0
519 }
520 }
521
522 fn device_id() -> DeviceId {
523 DeviceId::parse("DEV1").expect("valid device id")
524 }
525
526 fn room_id() -> RoomId {
527 RoomId::parse("!room:example.org").expect("valid room id")
528 }
529
530 #[test]
531 fn pending_request_survives_a_fresh_core_construction_before_ack() {
532 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
533 let mut queue = OutgoingQueue::new();
534
535 let request = OutgoingRequest::sync(RequestId::next(0), None, None);
536 queue.enqueue(&mut store, request.clone(), Lane::Sync).expect("enqueue succeeds");
537
538 let batch = store.take_flush_batch().expect("save_pending_request dirtied a record");
541 let reloaded =
542 Store::load(batch.records, InsecurePlainCodecForTests, device_id()).expect("load succeeds");
543
544 let mut restored = OutgoingQueue::load(&reloaded).expect("queue reloads");
545 assert_eq!(restored.releasable(&reloaded, 0), vec![request]);
546 }
547
548 #[test]
549 fn discard_kind_drops_the_restored_sync_but_keeps_other_requests() {
550 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
551 let mut queue = OutgoingQueue::new();
552 let sync = OutgoingRequest::sync(RequestId::next(0), Some("s1"), Some(30_000));
553 let join = OutgoingRequest::join_room(RequestId::next(1), room_id().as_str());
554 queue.enqueue(&mut store, sync, Lane::Sync).expect("enqueue sync");
555 queue.enqueue(&mut store, join.clone(), Lane::Other).expect("enqueue join");
556 let batch = store.take_flush_batch().expect("pending requests dirtied records");
557 let mut reloaded = Store::load(batch.records, InsecurePlainCodecForTests, device_id()).expect("load succeeds");
558
559 let mut restored = OutgoingQueue::load(&reloaded).expect("queue reloads");
560 restored.discard_kind(&mut reloaded, OutgoingRequestKind::Sync).expect("discard succeeds");
561
562 assert_eq!(restored.releasable(&reloaded, 0), vec![join], "only the non-sync request is left");
563 let stored: Vec<OutgoingRequestKind> = reloaded.pending_requests().expect("no error").iter().map(|record| record.request.kind).collect();
564 assert_eq!(stored, vec![OutgoingRequestKind::JoinRoom], "the sync record is gone from the store too");
565 }
566
567 #[test]
568 fn duplicate_dispatch_does_not_double_enqueue() {
569 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
570 let mut queue = OutgoingQueue::new();
571 let request = OutgoingRequest::sync(RequestId::next(0), None, None);
572
573 queue.enqueue(&mut store, request.clone(), Lane::Sync).expect("first enqueue");
574 queue.enqueue(&mut store, request.clone(), Lane::Sync).expect("duplicate enqueue is a no-op");
575
576 assert_eq!(store.pending_requests().expect("no error").len(), 1, "only one record persisted");
577 assert_eq!(queue.releasable(&store, 0).len(), 1, "only one request queued for release");
578 }
579
580 #[test]
581 fn sync_is_not_blocked_by_mutations_made_after_it_was_enqueued() {
582 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
583 let mut queue = OutgoingQueue::new();
584
585 let sync_req = OutgoingRequest::sync(RequestId::next(0), None, None);
586 queue.enqueue(&mut store, sync_req.clone(), Lane::Sync).expect("enqueue sync");
587 if let Some(batch) = store.take_flush_batch() {
588 store.ack_flush(batch.id);
589 }
590
591 store.save_account(b"later-mutation".to_vec()).expect("save succeeds");
594
595 assert_eq!(
596 queue.releasable(&store, 0),
597 vec![sync_req],
598 "the sync never depended on a mutation made after it was enqueued"
599 );
600 }
601
602 #[test]
603 fn send_is_blocked_until_the_flush_containing_its_ratchet_advance_is_acked() {
604 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
605 let mut queue = OutgoingQueue::new();
606 let room = room_id();
607
608 store.save_outbound_group_session(&room, b"ratchet-v1".to_vec()).expect("save succeeds");
610
611 let send_req = OutgoingRequest::room_send(
612 RequestId::next(1),
613 &room,
614 "m.room.encrypted",
615 &TxnId::new(0),
616 serde_json::json!({ "ciphertext": "..." }),
617 );
618 let batch = queue
619 .enqueue(&mut store, send_req.clone(), Lane::Room(room.clone()))
620 .expect("enqueue succeeds")
621 .expect("the pending ratchet advance was sealed into a batch");
622
623 assert!(queue.releasable(&store, 0).is_empty(), "blocked: the sealed batch is not yet acked");
624
625 store.ack_flush(batch.id);
626 assert_eq!(queue.releasable(&store, 0), vec![send_req]);
627 }
628
629 #[test]
630 fn to_device_lane_is_strictly_ordered_across_retries() {
631 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
632 let mut queue = OutgoingQueue::new();
633
634 let first = OutgoingRequest::send_to_device(
635 RequestId::next(0),
636 "m.room.encrypted",
637 &TxnId::new(0),
638 serde_json::json!({}),
639 );
640 let second = OutgoingRequest::send_to_device(
641 RequestId::next(1),
642 "m.room.encrypted",
643 &TxnId::new(1),
644 serde_json::json!({}),
645 );
646 queue.enqueue(&mut store, first.clone(), Lane::ToDevice).expect("enqueue first");
647 queue.enqueue(&mut store, second.clone(), Lane::ToDevice).expect("enqueue second");
648
649 assert_eq!(queue.releasable(&store, 0), vec![first.clone()], "only the oldest is released");
650 assert!(queue.releasable(&store, 0).is_empty(), "the lane is occupied while the first is in flight");
651
652 let mut jitter = FixedJitter(0.0);
653 let outcome = queue.on_transport_error(&first.id, 0, &mut jitter).expect("pending");
654 let ResponseOutcome::Retry { after_ms } = outcome else { panic!("expected a retry") };
655
656 assert!(queue.releasable(&store, 0).is_empty(), "back off before retrying");
657 assert_eq!(
658 queue.releasable(&store, after_ms as i64),
659 vec![first],
660 "the SAME request is retried before the second ever runs"
661 );
662 }
663
664 #[test]
665 fn room_lane_preserves_send_order() {
666 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
667 let mut queue = OutgoingQueue::new();
668 let room = room_id();
669
670 let first = OutgoingRequest::room_send(
671 RequestId::next(0),
672 &room,
673 "m.room.message",
674 &TxnId::new(0),
675 serde_json::json!({ "body": "a" }),
676 );
677 let second = OutgoingRequest::room_send(
678 RequestId::next(1),
679 &room,
680 "m.room.message",
681 &TxnId::new(1),
682 serde_json::json!({ "body": "b" }),
683 );
684 queue.enqueue(&mut store, first.clone(), Lane::Room(room.clone())).expect("enqueue first");
685 let batch = queue.enqueue(&mut store, second.clone(), Lane::Room(room.clone())).expect("enqueue second");
691 if let Some(batch) = batch {
692 store.ack_flush(batch.id);
693 }
694
695 assert_eq!(queue.releasable(&store, 0), vec![first.clone()], "only the oldest send is released");
696
697 let resp = HttpResponseDescriptor { status: 200, body: b"{}".to_vec() };
698 let outcome = queue
699 .on_response(&mut store, &first.id, resp, 0, &mut FixedJitter(0.0))
700 .expect("no store error")
701 .expect("pending");
702 assert!(matches!(outcome, ResponseOutcome::Done(_)));
703
704 assert_eq!(queue.releasable(&store, 0), vec![second], "second is released only after the first completes");
705 }
706
707 #[test]
708 fn retry_resends_identical_bytes() {
709 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
710 let mut queue = OutgoingQueue::new();
711
712 let request = OutgoingRequest::room_send(
713 RequestId::next(0),
714 &room_id(),
715 "m.room.encrypted",
716 &TxnId::new(0),
717 serde_json::json!({ "ciphertext": "same-bytes" }),
718 );
719 queue.enqueue(&mut store, request.clone(), Lane::Room(room_id())).expect("enqueue");
720
721 assert_eq!(queue.releasable(&store, 0), vec![request.clone()]);
722
723 let resp = HttpResponseDescriptor { status: 503, body: b"{}".to_vec() };
724 let outcome = queue
725 .on_response(&mut store, &request.id, resp, 0, &mut FixedJitter(0.0))
726 .expect("no error")
727 .expect("pending");
728 let ResponseOutcome::Retry { after_ms } = outcome else { panic!("expected a retry") };
729
730 assert_eq!(
731 queue.releasable(&store, after_ms as i64),
732 vec![request],
733 "the retry carries the exact same body/txn id -- never re-minted"
734 );
735 }
736
737 #[test]
738 fn rate_limited_response_uses_retry_after_ms() {
739 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
740 let mut queue = OutgoingQueue::new();
741 let request = OutgoingRequest::sync(RequestId::next(0), None, None);
742 queue.enqueue(&mut store, request.clone(), Lane::Sync).expect("enqueue");
743 queue.releasable(&store, 0);
744
745 let resp = HttpResponseDescriptor {
746 status: 429,
747 body: br#"{"errcode":"M_LIMIT_EXCEEDED","retry_after_ms":7500}"#.to_vec(),
748 };
749 let outcome = queue
750 .on_response(&mut store, &request.id, resp, 0, &mut FixedJitter(0.5))
751 .expect("no error")
752 .expect("pending");
753 assert_eq!(outcome, ResponseOutcome::Retry { after_ms: 7500 });
754 }
755
756 #[test]
757 fn four_hundred_class_error_fails_and_deletes_pending() {
758 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
759 let mut queue = OutgoingQueue::new();
760 let request = OutgoingRequest::sync(RequestId::next(0), None, None);
761 queue.enqueue(&mut store, request.clone(), Lane::Sync).expect("enqueue");
762 queue.releasable(&store, 0);
763
764 let resp = HttpResponseDescriptor {
765 status: 403,
766 body: br#"{"errcode":"M_FORBIDDEN","error":"nope"}"#.to_vec(),
767 };
768 let outcome = queue
769 .on_response(&mut store, &request.id, resp, 0, &mut FixedJitter(0.0))
770 .expect("no error")
771 .expect("pending");
772 assert_eq!(outcome, ResponseOutcome::Failed { errcode: "M_FORBIDDEN".to_string(), error: "nope".to_string() });
773 assert!(store.pending_requests().expect("no error").is_empty(), "the pending record was deleted");
774 assert!(queue.releasable(&store, 0).is_empty(), "nothing left to release");
775 }
776
777 #[test]
778 fn backoff_is_capped() {
779 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
780 let mut queue = OutgoingQueue::new();
781 let request = OutgoingRequest::sync(RequestId::next(0), None, None);
782 queue.enqueue(&mut store, request.clone(), Lane::Sync).expect("enqueue");
783
784 let mut jitter = FixedJitter(1.0); let mut now = 0i64;
786 let mut last_after_ms = 0u64;
787 for _ in 0..10 {
788 queue.releasable(&store, now);
789 let resp = HttpResponseDescriptor { status: 503, body: b"{}".to_vec() };
790 let outcome = queue
791 .on_response(&mut store, &request.id, resp, now, &mut jitter)
792 .expect("no error")
793 .expect("pending");
794 let ResponseOutcome::Retry { after_ms } = outcome else { panic!("expected a retry") };
795 assert!(after_ms <= BACKOFF_CAP_MS, "backoff never exceeds the cap: got {after_ms}");
796 last_after_ms = after_ms;
797 now += after_ms as i64;
798 }
799 assert_eq!(last_after_ms, BACKOFF_CAP_MS, "after enough retries, backoff saturates at the cap");
800 }
801
802 fn account_data_request(seed: u64) -> OutgoingRequest {
803 let user = crate::ids::UserId::parse("@alice:example.org").expect("valid user id");
804 OutgoingRequest::account_data(RequestId::next(seed), &user, "org.example.t", serde_json::json!({ "n": seed }))
805 }
806
807 #[test]
808 fn account_data_lane_holds_followers_behind_a_head_that_is_backing_off() {
809 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
810 let mut queue = OutgoingQueue::new();
811 let requests: Vec<OutgoingRequest> = (0..3).map(account_data_request).collect();
812 for request in &requests {
813 if let Some(batch) = queue.enqueue(&mut store, request.clone(), Lane::AccountData).expect("enqueue") {
814 store.ack_flush(batch.id);
815 }
816 }
817
818 assert_eq!(queue.releasable(&store, 0), vec![requests[0].clone()], "only the head is released");
819 assert!(queue.releasable(&store, 0).is_empty(), "nothing behind an in-flight head");
820
821 let mut jitter = FixedJitter(0.0);
822 let unavailable = HttpResponseDescriptor { status: 503, body: b"{}".to_vec() };
823 let outcome = queue.on_response(&mut store, &requests[0].id, unavailable, 0, &mut jitter).expect("no error");
824 assert!(matches!(outcome, Some(ResponseOutcome::Retry { .. })));
825 assert!(
826 queue.releasable(&store, 0).is_empty(),
827 "followers never overtake a head that is only backing off before its retry"
828 );
829
830 assert_eq!(queue.releasable(&store, 10_000), vec![requests[0].clone()], "the head retries once its backoff elapsed");
831 let ok = HttpResponseDescriptor { status: 200, body: b"{}".to_vec() };
832 queue.on_response(&mut store, &requests[0].id, ok, 10_000, &mut jitter).expect("no error");
833 assert_eq!(queue.releasable(&store, 10_000), vec![requests[1].clone()], "FIFO order continues");
834 }
835
836 #[test]
837 fn account_data_requests_persisted_on_the_other_lane_load_into_the_account_data_lane() {
838 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
839 let mut queue = OutgoingQueue::new();
840 let mut records = Vec::new();
841 for seed in 0..2 {
842 if let Some(batch) = queue.enqueue(&mut store, account_data_request(seed), Lane::Other).expect("enqueue") {
843 records.extend(batch.records);
844 }
845 }
846 records.extend(store.take_flush_batch().expect("the last record is still dirty").records);
847
848 let reloaded = Store::load(records, InsecurePlainCodecForTests, device_id()).expect("load succeeds");
849 let mut restored = OutgoingQueue::load(&reloaded).expect("queue reloads");
850 assert_eq!(
851 restored.releasable(&reloaded, 0),
852 vec![account_data_request(0)],
853 "a pre-AccountData-lane record keeps its relative order after an upgrade"
854 );
855 }
856}