iris_chat_protocol/protocol_engine/
engine_persistence.rs1impl ProtocolEngine {
2 fn has_authoritative_local_roster(&self) -> bool {
3 if self.local_app_keys_observed {
4 return true;
5 }
6 self.session_manager
7 .roster(self.local_owner)
8 .is_some_and(|roster| {
9 let devices = roster.devices();
10 !devices.is_empty()
11 && (devices.len() > 1 || devices[0].device_pubkey != self.local_device)
12 })
13 }
14
15 #[cfg_attr(all(test, target_os = "linux"), track_caller)]
16 fn persist(&mut self) -> anyhow::Result<()> {
17 #[cfg(all(test, target_os = "linux"))]
18 tests::audit_retry_persist_requested(std::panic::Location::caller());
19 if self.batch_depth.get() > 0 {
20 self.batch_persist_dirty.set(true);
21 return Ok(());
22 }
23 self.persist_now()
24 }
25
26 #[cfg_attr(all(test, target_os = "linux"), track_caller)]
27 fn persist_now(&mut self) -> anyhow::Result<()> {
28 let state = ProtocolEnginePersistedState {
29 version: PROTOCOL_ENGINE_STATE_VERSION,
30 group_invite_uses: self.group_invite_uses.clone(),
31 session_manager: self.session_manager.snapshot(),
32 group_manager: self.group_manager.snapshot(),
33 verified_app_keys_owners: self.verified_app_keys_owners.clone(),
34 app_keys_provenance_version: PROTOCOL_APP_KEYS_PROVENANCE_VERSION,
35 group_ingress_provenance_version: PROTOCOL_GROUP_INGRESS_PROVENANCE_VERSION,
36 invite_owner_app_keys_evidence: self.invite_owner_app_keys_evidence.clone(),
37 processed_private_invite_response_ids: self
38 .processed_private_invite_response_ids
39 .clone(),
40 pending_inbound: Vec::new(),
41 pending_group_fanouts: Vec::new(),
42 ready_group_publications: self.ready_group_publications.clone(),
43 pending_local_sibling_sends: self.pending_local_sibling_sends.clone(),
44 pending_remote_sends: self.pending_remote_sends.clone(),
45 pending_group_pairwise_payloads: self.pending_group_pairwise_payloads.clone(),
46 pending_group_sender_key_messages: PendingGroupMessages::default(),
47 pending_group_sender_key_repairs: self.pending_group_sender_key_repairs.clone(),
48 processed_group_sender_key_messages: self.processed_group_sender_key_messages.clone(),
49 answered_group_sender_key_repairs: self.answered_group_sender_key_repairs.clone(),
50 pending_decrypted_deliveries: self
51 .pending_decrypted_deliveries
52 .iter()
53 .filter(|delivery| !delivery.discarded)
54 .cloned()
55 .collect(),
56 group_roster_fact_histories: self.group_roster_fact_histories.clone(),
57 subscription_generation: self.subscription_generation,
58 };
59 let (checkpoint, layout) = record_protocol_checkpoint(
60 state,
61 &self.checkpoint_layout.borrow(),
62 &self.pending_group_sender_key_messages,
63 &self.pending_group_fanouts,
64 &self.pending_inbound,
65 )?;
66 self.storage
67 .put_checkpoint(PROTOCOL_ENGINE_STATE_KEY, &checkpoint)?;
68 #[cfg(all(test, target_os = "linux"))]
69 tests::audit_retry_persisted(std::panic::Location::caller(), self.batch_depth.get());
70 self.batch_persist_dirty.set(false);
71 self.pending_decrypted_deliveries
72 .retain(|delivery| !delivery.discarded);
73 *self.checkpoint_layout.borrow_mut() = layout;
74 Ok(())
75 }
76
77 pub fn enter_batch(&self) {
78 if self.batch_depth.get() == 0 {
79 self.group_sender_key_retry.borrow_mut().reset_budget();
80 }
81 self.batch_depth
82 .set(self.batch_depth.get().saturating_add(1));
83 }
84
85 pub fn exit_batch(&mut self) -> anyhow::Result<()> {
86 let depth = self.batch_depth.get();
87 if depth == 0 {
88 return Ok(());
89 }
90 self.batch_depth.set(depth - 1);
91 if self.batch_depth.get() == 0 && self.batch_persist_dirty.get() {
92 self.persist_now()?;
93 }
94 Ok(())
95 }
96}
97
98fn record_protocol_checkpoint(
99 core: ProtocolEnginePersistedState,
100 previous: &ProtocolCheckpointLayout,
101 pending: &PendingGroupMessages,
102 fanouts: &[ProtocolPendingGroupFanout],
103 inbound: &[ProtocolPendingInbound],
104) -> anyhow::Result<(crate::StorageCheckpoint, ProtocolCheckpointLayout)> {
105 let core_json = serde_json::to_string(&core)?;
106 let empty_prefix = format!(
107 "{{\"version\":{},\"pending_group_sender_key_messages\":[],\"pending_group_fanouts\":[],\"pending_inbound\":[]",
108 PROTOCOL_ENGINE_STATE_VERSION
109 );
110 anyhow::ensure!(
113 core_json.starts_with(&empty_prefix),
114 "unexpected checkpoint core prefix"
115 );
116 let core_previous = previous.core.as_deref();
117 let empty_layout = ProtocolCheckpointLayout::default();
118 let (core_json, core_layout) = layout_protocol_checkpoint_with_cached_fields(
119 core_json,
120 core_previous.unwrap_or(&empty_layout),
121 None,
122 &[],
123 );
124 let tail = core_json
125 .strip_prefix(&empty_prefix)
126 .ok_or_else(|| anyhow::anyhow!("unexpected checkpoint layout prefix"))?;
127 let mut value = crate::StorageCheckpoint::new();
128 value.push_inline(
129 "prefix",
130 format!(
131 "{{\"version\":{},\"pending_group_sender_key_messages\":[",
132 PROTOCOL_ENGINE_STATE_VERSION
133 ),
134 0,
135 );
136 for (index, body) in pending.record_bodies()?.into_iter().enumerate() {
137 value.push_record(
138 0,
139 body,
140 if index == 0 { "" } else { "," }.into(),
141 String::new(),
142 0,
143 );
144 }
145 value.push_inline("between", "],\"pending_group_fanouts\":[".into(), 0);
146 #[derive(Serialize)]
147 struct FanoutTail<'a> {
148 remaining_devices: &'a Option<BTreeSet<NdrDevicePubkey>>,
149 inner_event_id: &'a Option<String>,
150 created_at_secs: u64,
151 next_retry_at_secs: u64,
152 }
153 for (index, pending) in fanouts.iter().enumerate() {
154 let suffix = serde_json::to_string(&FanoutTail {
155 remaining_devices: &pending.remaining_devices,
156 inner_event_id: &pending.inner_event_id,
157 created_at_secs: pending.created_at_secs,
158 next_retry_at_secs: pending.next_retry_at_secs,
159 })?;
160 let prefix = format!(
161 "{}{{\"group_id\":{},\"fanout\":",
162 if index == 0 { "" } else { "," },
163 serde_json::to_string(&pending.group_id)?
164 );
165 value.push_record(
166 1,
167 pending.fanout.checkpoint_body()?,
168 prefix,
169 format!(",{}", &suffix[1..]),
170 0,
171 );
172 }
173 value.push_inline("before_inbound", "],\"pending_inbound\":[".into(), 0);
174 append_checkpoint_inbound(&mut value, inbound)?;
175 value.push_inline("core", format!("]{tail}"), 0);
176 let compact_bytes = value.len();
177 if compact_bytes < CHECKPOINT_RESERVE {
178 return Ok((value, ProtocolCheckpointLayout::default()));
179 }
180 value.pad_records(CHECKPOINT_PAGE, CHECKPOINT_PAGE / 2);
183 let reuse = previous.bytes <= compact_bytes.saturating_mul(3) + CHECKPOINT_RESERVE;
184 let target = if reuse && value.len() <= previous.bytes {
185 previous.bytes
186 } else {
187 value.len().div_ceil(CHECKPOINT_RESERVE) * CHECKPOINT_RESERVE
188 };
189 value.push_inline("reserve", String::new(), target - value.len());
190 let layout = ProtocolCheckpointLayout {
191 bytes: value.len(),
192 record_ends: Vec::new(),
193 core: Some(Box::new(core_layout)),
194 };
195 Ok((value, layout))
196}