diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -17,7 +17,7 @@ use crate::database::{ DBDeviceTypeInt, DatabaseClient, DeviceType, KeyPayload, UserInfoAndPasswordFile, }; -use crate::ddb_utils::Identifier; +use crate::ddb_utils::{Identifier, is_transaction_conflict}; use crate::device_list::SignedDeviceList; use crate::error::{DeviceListError, Error as DBError, consume_error}; use crate::grpc_services::authenticated::{DeletePasswordUserInfo, UpdatePasswordInfo}; @@ -1253,6 +1253,10 @@ | E::AwsSdk(DDBError::RequestLimitExceeded(_)) => { Status::unavailable(msg::RETRY) } + E::AwsSdk(ref err) if is_transaction_conflict(err) => { + warn!("DB operation conflicted. Returning RETRY status."); + Status::unavailable(msg::RETRY) + } E::DeviceList(DeviceListError::InvalidDeviceListUpdate) => { Status::invalid_argument(msg::INVALID_DEVICE_LIST_UPDATE) } diff --git a/services/identity/src/ddb_utils.rs b/services/identity/src/ddb_utils.rs --- a/services/identity/src/ddb_utils.rs +++ b/services/identity/src/ddb_utils.rs @@ -9,12 +9,13 @@ }, database::{AttributeExtractor, AttributeMap}, }; +use once_cell::sync::Lazy; use std::collections::{HashMap, HashSet}; use std::iter::IntoIterator; use crate::{ constants::{ - USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME, + retry, USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME, USERS_TABLE_SOCIAL_PROOF_ATTRIBUTE_NAME, USERS_TABLE_USERNAME_ATTRIBUTE, USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE, }, @@ -244,6 +245,27 @@ } } +/// There are two error codes for operation failure due to already ongoing +/// transaction: +/// - `DynamoDBError::TransactionConflict` +/// - `DynamoDBError::TransactionCanceled` if `reason == "TransactionConflict"` +/// +/// The former is thrown in case of normal write operation +/// (WriteItem, UpdateItem, etc) when a transaction is modifying them +/// at the moment. +/// +/// The latter is thrown in transaction operation (TransactWriteItem) when +/// another transaction is modifying them at the moment. +pub fn is_transaction_conflict(err: &DynamoDBError) -> bool { + static RETRYABLE_CODES: Lazy> = + Lazy::new(|| HashSet::from([retry::TRANSACTION_CONFLICT])); + + match err { + DynamoDBError::TransactionConflictException(_) => true, + _ => is_transaction_retryable(err, &RETRYABLE_CODES), + } +} + #[cfg(test)] mod tests { use crate::constants::one_time_keys_table;