Skip to content

Commit 31e9600

Browse files
authored
feat(send_queue): Persist failed to send errors (#4137)
Modify the SendQueue in order to persist the error that cause the event to fail to send as a `QueueWedgeError`. The `QueueWedgeError` is not a 1:1 mapping for all kinds of errors, but holds variant and information that the client can react to in order to propose "quick fixes"/solution before retrying to send. Fixes #3973 Also fixes element-hq/element-x-ios#3287 because when a timeline reset occurs the fail to send reason is also lost. This PR starts with a refactoring commit e769600 to introduce the new `QueueWedgedError` and move the logic that was in the ffi layer to convert api errors to SendState error variant. This `QueueWedgedError` can be directly use in the `SendingFailed` variant and expose to ffi. Second commit 109c133 adds the persistence, `QueuedEvent` now have an optional error field instead of a `is_weged` boolean. Same for LocalEchoContent::Event. Adds also Migration for sqlite and indexeddb Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc> Changelog: We now persist the error that caused an event to fail to send. The error `QueueWedgeError` contains info that client can use to try to resolve the problem when the error is not automatically retry-able. Some breaking changes occurred in the FFI layer for `timeline::EventSendState`, `SendingFailed` now directly contains the wedge reason enum; use it in place of the removed variant of `EventSendState`.
1 parent 3291a42 commit 31e9600

File tree

18 files changed

+460
-151
lines changed

18 files changed

+460
-151
lines changed

bindings/matrix-sdk-ffi/src/error.rs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
use std::fmt::Display;
1+
use std::{collections::HashMap, fmt, fmt::Display};
22

33
use matrix_sdk::{
44
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
55
room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
6-
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
6+
NotificationSettingsError as SdkNotificationSettingsError,
7+
QueueWedgeError as SdkQueueWedgeError, StoreError,
78
};
89
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
910
use uniffi::UnexpectedUniFFICallbackError;
10-
1111
#[derive(Debug, thiserror::Error)]
1212
pub enum ClientError {
1313
#[error("client error: {msg}")]
@@ -146,6 +146,79 @@ impl From<RoomSendQueueError> for ClientError {
146146
}
147147
}
148148

149+
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
150+
/// String.
151+
///
152+
/// Represent a failed to send unrecoverable error of an event sent via the
153+
/// send_queue. It is a serializable representation of a client error, see
154+
/// `From` implementation for more details. These errors can not be
155+
/// automatically retried, but yet some manual action can be taken before retry
156+
/// sending. If not the only solution is to delete the local event.
157+
#[derive(Debug, Clone, uniffi::Enum)]
158+
pub enum QueueWedgeError {
159+
/// This error occurs when there are some insecure devices in the room, and
160+
/// the current encryption setting prohibit sharing with them.
161+
InsecureDevices {
162+
/// The insecure devices as a Map of userID to deviceID.
163+
user_device_map: HashMap<String, Vec<String>>,
164+
},
165+
166+
/// This error occurs when a previously verified user is not anymore, and
167+
/// the current encryption setting prohibit sharing when it happens.
168+
IdentityViolations {
169+
/// The users that are expected to be verified but are not.
170+
users: Vec<String>,
171+
},
172+
173+
/// It is required to set up cross-signing and properly erify the current
174+
/// session before sending.
175+
CrossVerificationRequired,
176+
177+
/// Other errors.
178+
GenericApiError { msg: String },
179+
}
180+
181+
/// Simple display implementation that strips out userIds/DeviceIds to avoid
182+
/// accidental logging.
183+
impl Display for QueueWedgeError {
184+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185+
match self {
186+
QueueWedgeError::InsecureDevices { .. } => {
187+
f.write_str("There are insecure devices in the room")
188+
}
189+
QueueWedgeError::IdentityViolations { .. } => {
190+
f.write_str("Some users that were previously verified are not anymore")
191+
}
192+
QueueWedgeError::CrossVerificationRequired => {
193+
f.write_str("Own verification is required")
194+
}
195+
QueueWedgeError::GenericApiError { msg } => f.write_str(msg),
196+
}
197+
}
198+
}
199+
impl From<SdkQueueWedgeError> for QueueWedgeError {
200+
fn from(value: SdkQueueWedgeError) -> Self {
201+
match value {
202+
SdkQueueWedgeError::InsecureDevices { user_device_map } => Self::InsecureDevices {
203+
user_device_map: user_device_map
204+
.iter()
205+
.map(|(user_id, devices)| {
206+
(
207+
user_id.to_string(),
208+
devices.iter().map(|device_id| device_id.to_string()).collect(),
209+
)
210+
})
211+
.collect(),
212+
},
213+
SdkQueueWedgeError::IdentityViolations { users } => Self::IdentityViolations {
214+
users: users.iter().map(ruma::OwnedUserId::to_string).collect(),
215+
},
216+
SdkQueueWedgeError::CrossVerificationRequired => Self::CrossVerificationRequired,
217+
SdkQueueWedgeError::GenericApiError { msg } => Self::GenericApiError { msg },
218+
}
219+
}
220+
}
221+
149222
#[derive(Debug, thiserror::Error, uniffi::Error)]
150223
#[uniffi(flat_error)]
151224
pub enum RoomError {

bindings/matrix-sdk-ffi/src/timeline/mod.rs

Lines changed: 11 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ mod content;
8282

8383
pub use content::MessageContent;
8484

85+
use crate::error::QueueWedgeError;
86+
8587
#[derive(uniffi::Object)]
8688
#[repr(transparent)]
8789
pub struct Timeline {
@@ -951,49 +953,20 @@ pub enum EventSendState {
951953
/// The local event has not been sent yet.
952954
NotSentYet,
953955

954-
/// One or more verified users in the room has an unsigned device.
955-
///
956-
/// Happens only when the room key recipient strategy (as set by
957-
/// [`ClientBuilder::room_key_recipient_strategy`]) has
958-
/// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem) set.
959-
VerifiedUserHasUnsignedDevice {
960-
/// The unsigned devices belonging to verified users. A map from user ID
961-
/// to a list of device IDs.
962-
devices: HashMap<String, Vec<String>>,
963-
},
964-
965-
/// One or more verified users in the room has changed identity since they
966-
/// were verified.
967-
///
968-
/// Happens only when the room key recipient strategy (as set by
969-
/// [`ClientBuilder::room_key_recipient_strategy`]) has
970-
/// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem)
971-
/// set, or when using [`CollectStrategy::IdentityBasedStrategy`].
972-
VerifiedUserChangedIdentity {
973-
/// The users that were previously verified, but are no longer
974-
users: Vec<String>,
975-
},
976-
977-
/// The user does not have cross-signing set up, but
978-
/// [`CollectStrategy::IdentityBasedStrategy`] was used.
979-
CrossSigningNotSetup,
980-
981-
/// The current device is not verified, but
982-
/// [`CollectStrategy::IdentityBasedStrategy`] was used.
983-
SendingFromUnverifiedDevice,
984-
985956
/// The local event has been sent to the server, but unsuccessfully: The
986957
/// sending has failed.
987958
SendingFailed {
988-
/// Stringified error message.
989-
error: String,
959+
/// The error reason, with information for the user.
960+
error: QueueWedgeError,
961+
990962
/// Whether the error is considered recoverable or not.
991963
///
992964
/// An error that's recoverable will disable the room's send queue,
993965
/// while an unrecoverable error will be parked, until the user
994966
/// decides to cancel sending it.
995967
is_recoverable: bool,
996968
},
969+
997970
/// The local event has been sent successfully to the server.
998971
Sent { event_id: String },
999972
}
@@ -1005,46 +978,17 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
1005978
match value {
1006979
NotSentYet => Self::NotSentYet,
1007980
SendingFailed { error, is_recoverable } => {
1008-
event_send_state_from_sending_failed(error, *is_recoverable)
981+
let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&**error).into();
982+
Self::SendingFailed {
983+
is_recoverable: *is_recoverable,
984+
error: as_queue_wedge_error.into(),
985+
}
1009986
}
1010987
Sent { event_id } => Self::Sent { event_id: event_id.to_string() },
1011988
}
1012989
}
1013990
}
1014991

1015-
fn event_send_state_from_sending_failed(error: &Error, is_recoverable: bool) -> EventSendState {
1016-
use matrix_sdk::crypto::{OlmError, SessionRecipientCollectionError::*};
1017-
1018-
match error {
1019-
// Special-case the SessionRecipientCollectionErrors, to pass the information they contain
1020-
// back to the application.
1021-
Error::OlmError(OlmError::SessionRecipientCollectionError(error)) => match error {
1022-
VerifiedUserHasUnsignedDevice(devices) => {
1023-
let devices = devices
1024-
.iter()
1025-
.map(|(user_id, devices)| {
1026-
(
1027-
user_id.to_string(),
1028-
devices.iter().map(|device_id| device_id.to_string()).collect(),
1029-
)
1030-
})
1031-
.collect();
1032-
EventSendState::VerifiedUserHasUnsignedDevice { devices }
1033-
}
1034-
1035-
VerifiedUserChangedIdentity(bad_users) => EventSendState::VerifiedUserChangedIdentity {
1036-
users: bad_users.iter().map(|user_id| user_id.to_string()).collect(),
1037-
},
1038-
1039-
CrossSigningNotSetup => EventSendState::CrossSigningNotSetup,
1040-
1041-
SendingFromUnverifiedDevice => EventSendState::SendingFromUnverifiedDevice,
1042-
},
1043-
1044-
_ => EventSendState::SendingFailed { error: error.to_string(), is_recoverable },
1045-
}
1046-
}
1047-
1048992
/// Recommended decorations for decrypted messages, representing the message's
1049993
/// authenticity properties.
1050994
#[derive(uniffi::Enum, Clone)]

crates/matrix-sdk-base/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ pub use rooms::{
6161
RoomStateFilter,
6262
};
6363
pub use store::{
64-
ComposerDraft, ComposerDraftType, StateChanges, StateStore, StateStoreDataKey,
64+
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
6565
StateStoreDataValue, StoreError,
6666
};
6767
pub use utils::{

crates/matrix-sdk-base/src/store/integration_tests.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ use serde_json::{json, value::Value as JsonValue};
3636
use super::{DependentQueuedEventKind, DynStateStore, ServerCapabilities};
3737
use crate::{
3838
deserialized_responses::MemberEvent,
39-
store::{traits::ChildTransactionId, Result, SerializableEventContent, StateStoreExt},
39+
store::{
40+
traits::ChildTransactionId, QueueWedgeError, Result, SerializableEventContent,
41+
StateStoreExt,
42+
},
4043
RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue,
4144
};
4245

@@ -1223,7 +1226,7 @@ impl StateStoreIntegrationTests for DynStateStore {
12231226
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
12241227
assert_eq!(content.body(), "msg0");
12251228

1226-
assert!(!pending[0].is_wedged);
1229+
assert!(!pending[0].is_wedged());
12271230
}
12281231

12291232
// Saving another three things should work.
@@ -1249,12 +1252,18 @@ impl StateStoreIntegrationTests for DynStateStore {
12491252
let deserialized = pending[i].event.deserialize().unwrap();
12501253
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
12511254
assert_eq!(content.body(), format!("msg{i}"));
1252-
assert!(!pending[i].is_wedged);
1255+
assert!(!pending[i].is_wedged());
12531256
}
12541257

12551258
// Marking an event as wedged works.
12561259
let txn2 = &pending[2].transaction_id;
1257-
self.update_send_queue_event_status(room_id, txn2, true).await.unwrap();
1260+
self.update_send_queue_event_status(
1261+
room_id,
1262+
txn2,
1263+
Some(QueueWedgeError::GenericApiError { msg: "Oops".to_owned() }),
1264+
)
1265+
.await
1266+
.unwrap();
12581267

12591268
// And it is reflected.
12601269
let pending = self.load_send_queue_events(room_id).await.unwrap();
@@ -1263,10 +1272,13 @@ impl StateStoreIntegrationTests for DynStateStore {
12631272
assert_eq!(pending.len(), 4);
12641273
assert_eq!(pending[0].transaction_id, txn0);
12651274
assert_eq!(pending[2].transaction_id, *txn2);
1266-
assert!(pending[2].is_wedged);
1275+
assert!(pending[2].is_wedged());
1276+
let error = pending[2].clone().error.unwrap();
1277+
let generic_error = assert_matches!(error, QueueWedgeError::GenericApiError { msg } => msg);
1278+
assert_eq!(generic_error, "Oops");
12671279
for i in 0..4 {
12681280
if i != 2 {
1269-
assert!(!pending[i].is_wedged);
1281+
assert!(!pending[i].is_wedged());
12701282
}
12711283
}
12721284

@@ -1288,15 +1300,15 @@ impl StateStoreIntegrationTests for DynStateStore {
12881300
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
12891301
assert_eq!(content.body(), "wow that's a cool test");
12901302

1291-
assert!(!pending[2].is_wedged);
1303+
assert!(!pending[2].is_wedged());
12921304

12931305
for i in 0..4 {
12941306
if i != 2 {
12951307
let deserialized = pending[i].event.deserialize().unwrap();
12961308
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
12971309
assert_eq!(content.body(), format!("msg{i}"));
12981310

1299-
assert!(!pending[i].is_wedged);
1311+
assert!(!pending[i].is_wedged());
13001312
}
13011313
}
13021314
}

crates/matrix-sdk-base/src/store/memory_store.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ use super::{
4444
StoreError,
4545
};
4646
use crate::{
47-
deserialized_responses::RawAnySyncOrStrippedState, MinimalRoomMemberEvent, RoomMemberships,
48-
StateStoreDataKey, StateStoreDataValue,
47+
deserialized_responses::RawAnySyncOrStrippedState, store::QueueWedgeError,
48+
MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
4949
};
5050

5151
/// In-memory, non-persistent implementation of the `StateStore`.
@@ -815,7 +815,7 @@ impl StateStore for MemoryStore {
815815
.unwrap()
816816
.entry(room_id.to_owned())
817817
.or_default()
818-
.push(QueuedEvent { event, transaction_id, is_wedged: false });
818+
.push(QueuedEvent { event, transaction_id, error: None });
819819
Ok(())
820820
}
821821

@@ -835,7 +835,7 @@ impl StateStore for MemoryStore {
835835
.find(|item| item.transaction_id == transaction_id)
836836
{
837837
entry.event = content;
838-
entry.is_wedged = false;
838+
entry.error = None;
839839
Ok(true)
840840
} else {
841841
Ok(false)
@@ -876,7 +876,7 @@ impl StateStore for MemoryStore {
876876
&self,
877877
room_id: &RoomId,
878878
transaction_id: &TransactionId,
879-
wedged: bool,
879+
error: Option<QueueWedgeError>,
880880
) -> Result<(), Self::Error> {
881881
if let Some(entry) = self
882882
.send_queue_events
@@ -887,7 +887,7 @@ impl StateStore for MemoryStore {
887887
.iter_mut()
888888
.find(|item| item.transaction_id == transaction_id)
889889
{
890-
entry.is_wedged = wedged;
890+
entry.error = error;
891891
}
892892
Ok(())
893893
}

crates/matrix-sdk-base/src/store/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ pub use self::{
7575
memory_store::MemoryStore,
7676
traits::{
7777
ChildTransactionId, ComposerDraft, ComposerDraftType, DependentQueuedEvent,
78-
DependentQueuedEventKind, DynStateStore, IntoStateStore, QueuedEvent,
78+
DependentQueuedEventKind, DynStateStore, IntoStateStore, QueueWedgeError, QueuedEvent,
7979
SerializableEventContent, ServerCapabilities, StateStore, StateStoreDataKey,
8080
StateStoreDataValue, StateStoreExt,
8181
},

0 commit comments

Comments
 (0)