diff --git a/src/realm.h b/src/realm.h index f9d102c59db..cef93b96a70 100644 --- a/src/realm.h +++ b/src/realm.h @@ -3374,6 +3374,10 @@ typedef enum realm_sync_error_action { RLM_SYNC_ERROR_ACTION_CLIENT_RESET_NO_RECOVERY, RLM_SYNC_ERROR_ACTION_MIGRATE_TO_FLX, RLM_SYNC_ERROR_ACTION_REVERT_TO_PBS, + RLM_SYNC_ERROR_ACTION_REFRESH_USER, + RLM_SYNC_ERROR_ACTION_REFRESH_LOCATION, + RLM_SYNC_ERROR_ACTION_LOG_OUT_USER, + RLM_SYNC_ERROR_ACTION_BACKUP_THEN_DELETE_REALM, } realm_sync_error_action_e; typedef struct realm_sync_session realm_sync_session_t; @@ -3873,7 +3877,7 @@ RLM_API void realm_sync_session_wait_for_upload_completion(realm_sync_session_t* */ RLM_API void realm_sync_session_handle_error_for_testing(const realm_sync_session_t* session, realm_errno_e error_code, const char* error_str, - bool is_fatal); + bool is_fatal, realm_sync_error_action_e action); /** * In case of exception thrown in user code callbacks, this api will allow the sdk to store the user code exception diff --git a/src/realm/object-store/c_api/sync.cpp b/src/realm/object-store/c_api/sync.cpp index 384bb6ce18c..8870474f633 100644 --- a/src/realm/object-store/c_api/sync.cpp +++ b/src/realm/object-store/c_api/sync.cpp @@ -105,6 +105,13 @@ static_assert(realm_sync_error_action_e(ProtocolErrorInfo::Action::MigrateToFLX) RLM_SYNC_ERROR_ACTION_MIGRATE_TO_FLX); static_assert(realm_sync_error_action_e(ProtocolErrorInfo::Action::RevertToPBS) == RLM_SYNC_ERROR_ACTION_REVERT_TO_PBS); +static_assert(realm_sync_error_action_e(ProtocolErrorInfo::Action::RefreshUser) == + RLM_SYNC_ERROR_ACTION_REFRESH_USER); +static_assert(realm_sync_error_action_e(ProtocolErrorInfo::Action::RefreshLocation) == + RLM_SYNC_ERROR_ACTION_REFRESH_LOCATION); +static_assert(realm_sync_error_action_e(ProtocolErrorInfo::Action::LogOutUser) == RLM_SYNC_ERROR_ACTION_LOG_OUT_USER); +static_assert(realm_sync_error_action_e(ProtocolErrorInfo::Action::BackupThenDeleteRealm) == + RLM_SYNC_ERROR_ACTION_BACKUP_THEN_DELETE_REALM); static_assert(realm_flx_sync_subscription_set_state_e(SubscriptionSet::State::Pending) == RLM_SYNC_SUBSCRIPTION_PENDING); @@ -834,12 +841,12 @@ RLM_API void realm_sync_session_wait_for_upload_completion(realm_sync_session_t* RLM_API void realm_sync_session_handle_error_for_testing(const realm_sync_session_t* session, realm_errno_e error_code, const char* error_str, - bool is_fatal) + bool is_fatal, realm_sync_error_action_e action) { REALM_ASSERT(session); SyncSession::OnlyForTesting::handle_error( - *session->get(), - sync::SessionErrorInfo{Status{static_cast(error_code), error_str}, !is_fatal}); + *session->get(), sync::SessionErrorInfo{Status{static_cast(error_code), error_str}, + !is_fatal, static_cast(action)}); } } // namespace realm::c_api diff --git a/src/realm/object-store/sync/impl/sync_client.hpp b/src/realm/object-store/sync/impl/sync_client.hpp index c0c896a133f..cccde655040 100644 --- a/src/realm/object-store/sync/impl/sync_client.hpp +++ b/src/realm/object-store/sync/impl/sync_client.hpp @@ -97,11 +97,6 @@ struct SyncClient { } #endif - void cancel_reconnect_delay() - { - m_client.cancel_reconnect_delay(); - } - void stop() { m_client.shutdown(); diff --git a/src/realm/object-store/sync/sync_session.cpp b/src/realm/object-store/sync/sync_session.cpp index 98336d6a5e0..cfe2aa55846 100644 --- a/src/realm/object-store/sync/sync_session.cpp +++ b/src/realm/object-store/sync/sync_session.cpp @@ -589,7 +589,7 @@ void SyncSession::handle_fresh_realm_downloaded(DBRef db, Status status, sync::SessionErrorInfo synthetic( Status{ErrorCodes::AutoClientResetFailed, util::format("A fatal error occurred during client reset: '%1'", status.reason())}, - sync::IsFatal{true}); + sync::IsFatal{true}, sync::ProtocolErrorInfo::Action::BackupThenDeleteRealm); handle_error(synthetic); return; } @@ -644,93 +644,85 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) auto next_state = error.is_fatal ? NextStateAfterError::error : NextStateAfterError::none; util::Optional delete_file; bool log_out_user = false; - bool unrecognized_by_client = false; - - if (error.status == ErrorCodes::AutoClientResetFailed) { - // At this point, automatic recovery has been attempted but it failed. - // Fallback to a manual reset and let the user try to handle it. - next_state = NextStateAfterError::inactive; - delete_file = ShouldBackup::yes; - } - else if (error.server_requests_action != sync::ProtocolErrorInfo::Action::NoAction) { - switch (error.server_requests_action) { - case sync::ProtocolErrorInfo::Action::NoAction: - REALM_UNREACHABLE(); // This is not sent by the MongoDB server - case sync::ProtocolErrorInfo::Action::ApplicationBug: - [[fallthrough]]; - case sync::ProtocolErrorInfo::Action::ProtocolViolation: - break; - case sync::ProtocolErrorInfo::Action::Warning: - break; // not fatal, but should be bubbled up to the user below. - case sync::ProtocolErrorInfo::Action::Transient: - // Not real errors, don't need to be reported to the binding. - return; - case sync::ProtocolErrorInfo::Action::DeleteRealm: - next_state = NextStateAfterError::inactive; - delete_file = ShouldBackup::no; - break; - case sync::ProtocolErrorInfo::Action::ClientReset: - [[fallthrough]]; - case sync::ProtocolErrorInfo::Action::ClientResetNoRecovery: - switch (config(&SyncConfig::client_resync_mode)) { - case ClientResyncMode::Manual: - next_state = NextStateAfterError::inactive; - delete_file = ShouldBackup::yes; - break; - case ClientResyncMode::DiscardLocal: - [[fallthrough]]; - case ClientResyncMode::RecoverOrDiscard: - [[fallthrough]]; - case ClientResyncMode::Recover: - download_fresh_realm(error.server_requests_action); - return; // do not propgate the error to the user at this point - } - break; - case sync::ProtocolErrorInfo::Action::MigrateToFLX: - // Should not receive this error if original sync config is FLX - REALM_ASSERT(!m_original_sync_config->flx_sync_requested); - REALM_ASSERT(error.migration_query_string && !error.migration_query_string->empty()); - // Original config was PBS, migrating to FLX - m_migration_store->migrate_to_flx(*error.migration_query_string, - m_original_sync_config->partition_value); - save_sync_config_after_migration_or_rollback(); - download_fresh_realm(error.server_requests_action); + bool unrecognized_by_client = error.status == ErrorCodes::UnknownError; + + switch (error.server_requests_action) { + case sync::ProtocolErrorInfo::Action::NoAction: + REALM_UNREACHABLE(); + case sync::ProtocolErrorInfo::Action::ApplicationBug: + [[fallthrough]]; + case sync::ProtocolErrorInfo::Action::ProtocolViolation: + next_state = NextStateAfterError::inactive; + break; + case sync::ProtocolErrorInfo::Action::Warning: + break; // not fatal, but should be bubbled up to the user below. + case sync::ProtocolErrorInfo::Action::Transient: + // Not real errors, don't need to be reported to the binding. + return; + case sync::ProtocolErrorInfo::Action::BackupThenDeleteRealm: + next_state = NextStateAfterError::inactive; + delete_file = ShouldBackup::yes; + break; + case sync::ProtocolErrorInfo::Action::DeleteRealm: + next_state = NextStateAfterError::inactive; + delete_file = ShouldBackup::no; + break; + case sync::ProtocolErrorInfo::Action::ClientReset: + [[fallthrough]]; + case sync::ProtocolErrorInfo::Action::ClientResetNoRecovery: + switch (config(&SyncConfig::client_resync_mode)) { + case ClientResyncMode::Manual: + next_state = NextStateAfterError::inactive; + delete_file = ShouldBackup::yes; + break; + case ClientResyncMode::DiscardLocal: + [[fallthrough]]; + case ClientResyncMode::RecoverOrDiscard: + [[fallthrough]]; + case ClientResyncMode::Recover: + download_fresh_realm(error.server_requests_action); + return; // do not propgate the error to the user at this point + } + break; + case sync::ProtocolErrorInfo::Action::MigrateToFLX: + // Should not receive this error if original sync config is FLX + REALM_ASSERT(!m_original_sync_config->flx_sync_requested); + REALM_ASSERT(error.migration_query_string && !error.migration_query_string->empty()); + // Original config was PBS, migrating to FLX + m_migration_store->migrate_to_flx(*error.migration_query_string, m_original_sync_config->partition_value); + save_sync_config_after_migration_or_rollback(); + download_fresh_realm(error.server_requests_action); + return; + case sync::ProtocolErrorInfo::Action::RevertToPBS: + // If the client was updated to use FLX natively, but the server was rolled back to PBS, + // the server should be sending switch_to_flx_sync; throw exception if this error is not + // received. + if (m_original_sync_config->flx_sync_requested) { + throw LogicError(ErrorCodes::InvalidServerResponse, + "Received 'RevertToPBS' from server after rollback while client is natively " + "using FLX - expected 'SwitchToPBS'"); + } + // Original config was PBS, rollback the migration + m_migration_store->rollback_to_pbs(); + save_sync_config_after_migration_or_rollback(); + download_fresh_realm(error.server_requests_action); + return; + case sync::ProtocolErrorInfo::Action::RefreshUser: + if (auto u = user()) { + u->refresh_custom_data(false, handle_refresh(shared_from_this(), false)); return; - case sync::ProtocolErrorInfo::Action::RevertToPBS: - // If the client was updated to use FLX natively, but the server was rolled back to PBS, - // the server should be sending switch_to_flx_sync; throw exception if this error is not - // received. - if (m_original_sync_config->flx_sync_requested) { - throw LogicError(ErrorCodes::InvalidServerResponse, - "Received 'RevertToPBS' from server after rollback while client is natively " - "using FLX - expected 'SwitchToPBS'"); - } - // Original config was PBS, rollback the migration - m_migration_store->rollback_to_pbs(); - save_sync_config_after_migration_or_rollback(); - download_fresh_realm(error.server_requests_action); + } + break; + case sync::ProtocolErrorInfo::Action::RefreshLocation: + if (auto u = user()) { + u->refresh_custom_data(true, handle_refresh(shared_from_this(), true)); return; - case sync::ProtocolErrorInfo::Action::RefreshUser: - if (auto u = user()) { - u->refresh_custom_data(false, handle_refresh(shared_from_this(), false)); - return; - } - break; - case sync::ProtocolErrorInfo::Action::RefreshLocation: - if (auto u = user()) { - u->refresh_custom_data(true, handle_refresh(shared_from_this(), true)); - return; - } - break; - case sync::ProtocolErrorInfo::Action::LogOutUser: - next_state = NextStateAfterError::inactive; - log_out_user = true; - break; - } - } - else { - // Unrecognized error code. - unrecognized_by_client = true; + } + break; + case sync::ProtocolErrorInfo::Action::LogOutUser: + next_state = NextStateAfterError::inactive; + log_out_user = true; + break; } util::CheckedUniqueLock lock(m_state_mutex); @@ -743,7 +735,7 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) update_error_and_mark_file_for_deletion(sync_error, *delete_file); if (m_state == State::Dying && error.is_fatal) { - become_inactive(std::move(lock), error.status); + become_inactive(std::move(lock), sync_error.status); return; } @@ -760,6 +752,7 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) } break; case NextStateAfterError::inactive: { + m_session->mark_unresumable(); become_inactive(std::move(lock), sync_error.status); break; } diff --git a/src/realm/sync/client.cpp b/src/realm/sync/client.cpp index 38df01a559c..0f1babefc16 100644 --- a/src/realm/sync/client.cpp +++ b/src/realm/sync/client.cpp @@ -127,6 +127,8 @@ class SessionWrapper final : public util::AtomicRefCountBase, DB::CommitListener std::string get_appservices_connection_id(); + void mark_unresumable(); + private: ClientImpl& m_client; DBRef m_db; @@ -197,6 +199,7 @@ class SessionWrapper final : public util::AtomicRefCountBase, DB::CommitListener bool m_force_closed = false; bool m_suspended = false; + bool m_resumable = true; // Has the SessionWrapper been finalized? bool m_finalized = false; @@ -993,8 +996,8 @@ SyncClientHookAction SessionImpl::call_debug_hook(const SyncClientHookData& data auto action = m_wrapper.m_debug_hook(data); switch (action) { case realm::SyncClientHookAction::SuspendWithRetryableError: { - SessionErrorInfo err_info(Status{ErrorCodes::RuntimeError, "hook requested error"}, IsFatal{false}); - err_info.server_requests_action = ProtocolErrorInfo::Action::Transient; + SessionErrorInfo err_info(Status{ErrorCodes::RuntimeError, "hook requested error"}, IsFatal{false}, + ProtocolErrorInfo::Action::Transient); auto err_processing_err = receive_error_message(err_info); REALM_ASSERT_EX(err_processing_err.is_ok(), err_processing_err); @@ -1322,6 +1325,11 @@ void SessionWrapper::cancel_reconnect_delay() if (REALM_UNLIKELY(!self->m_sess)) return; // Already finalized SessionImpl& sess = *self->m_sess; + if (!self->m_resumable) { + sess.logger.debug("Cannot resume a session that has received a fatal error"); + return; + } + sess.cancel_resumption_delay(); // Throws ClientImpl::Connection& conn = sess.get_connection(); conn.cancel_reconnect_delay(); // Throws @@ -1860,6 +1868,11 @@ std::string SessionWrapper::get_appservices_connection_id() return pf.future.get(); } +void SessionWrapper::mark_unresumable() +{ + m_resumable = false; +} + // ################ ClientImpl::Connection ################ ClientImpl::Connection::Connection(ClientImpl& client, connection_ident_type ident, ServerEndpoint endpoint, @@ -2106,6 +2119,11 @@ std::string Session::get_appservices_connection_id() return m_impl->get_appservices_connection_id(); } +void Session::mark_unresumable() +{ + m_impl->mark_unresumable(); +} + std::ostream& operator<<(std::ostream& os, ProxyConfig::Type proxyType) { switch (proxyType) { diff --git a/src/realm/sync/client.hpp b/src/realm/sync/client.hpp index b5b6b3530b5..3ffc397017c 100644 --- a/src/realm/sync/client.hpp +++ b/src/realm/sync/client.hpp @@ -707,6 +707,13 @@ class Session { /// with the error. std::string get_appservices_connection_id(); + /// Marks the session as un-resumable after a fatal error. + /// + /// This function is not thread-safe and should be called from the connection-state + /// listener callback if the Session should not initiate a re-connect/resume after + /// a fatal error. + void mark_unresumable(); + private: SessionWrapper* m_impl = nullptr; diff --git a/src/realm/sync/client_base.hpp b/src/realm/sync/client_base.hpp index 71b9722ea18..a3a4a5cb1f8 100644 --- a/src/realm/sync/client_base.hpp +++ b/src/realm/sync/client_base.hpp @@ -258,8 +258,8 @@ struct SessionErrorInfo : public ProtocolErrorInfo { { } - SessionErrorInfo(Status status, IsFatal is_fatal) - : ProtocolErrorInfo(0, {}, is_fatal) + SessionErrorInfo(Status status, IsFatal is_fatal, Action error_action) + : ProtocolErrorInfo(0, {}, is_fatal, error_action) , status(std::move(status)) { } diff --git a/src/realm/sync/config.hpp b/src/realm/sync/config.hpp index 525e951bc77..6c758e08c79 100644 --- a/src/realm/sync/config.hpp +++ b/src/realm/sync/config.hpp @@ -129,6 +129,7 @@ enum class SyncClientHookEvent { BootstrapMessageProcessed, BootstrapProcessed, ErrorMessageReceived, + SessionSuspended, }; enum class SyncClientHookAction { diff --git a/src/realm/sync/noinst/client_impl_base.cpp b/src/realm/sync/noinst/client_impl_base.cpp index d07d874c269..af3eb7928fa 100644 --- a/src/realm/sync/noinst/client_impl_base.cpp +++ b/src/realm/sync/noinst/client_impl_base.cpp @@ -475,12 +475,13 @@ void Connection::websocket_connected_handler(const std::string& protocol) } close_due_to_client_side_error({ErrorCodes::SyncProtocolNegotiationFailed, util::format("Bad protocol info from server: '%1'", protocol)}, - IsFatal{true}, ConnectionTerminationReason::bad_headers_in_http_response); + IsFatal{true}, ProtocolErrorInfo::Action::ProtocolViolation, + ConnectionTerminationReason::bad_headers_in_http_response); } else { close_due_to_client_side_error( {ErrorCodes::SyncProtocolNegotiationFailed, "Missing protocol info from server"}, IsFatal{true}, - ConnectionTerminationReason::bad_headers_in_http_response); + ProtocolErrorInfo::Action::ProtocolViolation, ConnectionTerminationReason::bad_headers_in_http_response); } } @@ -496,7 +497,7 @@ bool Connection::websocket_binary_message_received(util::Span data) if (sf::check_trigger(sf::sync_client__read_head)) { close_due_to_client_side_error( {ErrorCodes::RuntimeError, "Simulated failure during sync client websocket read"}, IsFatal{false}, - ConnectionTerminationReason::read_or_write_error); + ProtocolErrorInfo::Action::Transient, ConnectionTerminationReason::read_or_write_error); return bool(m_websocket); } @@ -524,9 +525,9 @@ bool Connection::websocket_closed_handler(bool was_clean, WebSocketError error_c case WebSocketError::websocket_resolve_failed: [[fallthrough]]; case WebSocketError::websocket_connection_failed: { - SessionErrorInfo error_info( - {ErrorCodes::SyncConnectFailed, util::format("Failed to connect to sync: %1", msg)}, IsFatal{false}); - involuntary_disconnect(std::move(error_info), ConnectionTerminationReason::connect_operation_failed); + close_due_to_client_side_error( + {ErrorCodes::SyncConnectFailed, util::format("Failed to connect to sync: %1", msg)}, IsFatal{false}, + ProtocolErrorInfo::Action::Warning, ConnectionTerminationReason::connect_operation_failed); break; } case WebSocketError::websocket_read_error: @@ -552,14 +553,15 @@ bool Connection::websocket_closed_handler(bool was_clean, WebSocketError error_c [[fallthrough]]; case WebSocketError::websocket_invalid_extension: { close_due_to_client_side_error({ErrorCodes::SyncProtocolInvariantFailed, msg}, IsFatal{false}, + ProtocolErrorInfo::Action::ProtocolViolation, ConnectionTerminationReason::websocket_protocol_violation); // Throws break; } case WebSocketError::websocket_message_too_big: { auto message = util::format( "Sync websocket closed because the server received a message that was too large: %1", msg); - SessionErrorInfo error_info(Status(ErrorCodes::LimitExceeded, std::move(message)), IsFatal{false}); - error_info.server_requests_action = ProtocolErrorInfo::Action::ClientReset; + SessionErrorInfo error_info(Status(ErrorCodes::LimitExceeded, std::move(message)), IsFatal{false}, + ProtocolErrorInfo::Action::ClientReset); involuntary_disconnect(std::move(error_info), ConnectionTerminationReason::websocket_protocol_violation); // Throws break; @@ -567,6 +569,7 @@ bool Connection::websocket_closed_handler(bool was_clean, WebSocketError error_c case WebSocketError::websocket_tls_handshake_failed: { close_due_to_client_side_error( Status(ErrorCodes::TlsHandshakeFailed, util::format("TLS handshake failed: %1", msg)), IsFatal{false}, + ProtocolErrorInfo::Action::Warning, ConnectionTerminationReason::ssl_certificate_rejected); // Throws break; } @@ -576,17 +579,19 @@ bool Connection::websocket_closed_handler(bool was_clean, WebSocketError error_c [[fallthrough]]; case WebSocketError::websocket_protocol_mismatch: { close_due_to_client_side_error({ErrorCodes::SyncProtocolNegotiationFailed, msg}, IsFatal{true}, + ProtocolErrorInfo::Action::ProtocolViolation, ConnectionTerminationReason::http_response_says_fatal_error); // Throws break; } case WebSocketError::websocket_fatal_error: { - involuntary_disconnect(SessionErrorInfo({ErrorCodes::ConnectionClosed, msg}, IsFatal{true}), + involuntary_disconnect(SessionErrorInfo({ErrorCodes::ConnectionClosed, msg}, IsFatal{true}, + ProtocolErrorInfo::Action::ApplicationBug), ConnectionTerminationReason::http_response_says_fatal_error); break; } case WebSocketError::websocket_forbidden: { - SessionErrorInfo error_info({ErrorCodes::AuthError, msg}, IsFatal{true}); - error_info.server_requests_action = ProtocolErrorInfo::Action::LogOutUser; + SessionErrorInfo error_info({ErrorCodes::AuthError, msg}, IsFatal{true}, + ProtocolErrorInfo::Action::LogOutUser); involuntary_disconnect(std::move(error_info), ConnectionTerminationReason::http_response_says_fatal_error); break; @@ -595,22 +600,21 @@ bool Connection::websocket_closed_handler(bool was_clean, WebSocketError error_c SessionErrorInfo error_info( {ErrorCodes::AuthError, util::format("Websocket was closed because of an authentication issue: %1", msg)}, - IsFatal{false}); - error_info.server_requests_action = ProtocolErrorInfo::Action::RefreshUser; + IsFatal{false}, ProtocolErrorInfo::Action::RefreshUser); involuntary_disconnect(std::move(error_info), ConnectionTerminationReason::http_response_says_nonfatal_error); break; } case WebSocketError::websocket_moved_permanently: { - SessionErrorInfo error_info({ErrorCodes::ConnectionClosed, msg}, IsFatal{false}); - error_info.server_requests_action = ProtocolErrorInfo::Action::RefreshLocation; + SessionErrorInfo error_info({ErrorCodes::ConnectionClosed, msg}, IsFatal{false}, + ProtocolErrorInfo::Action::RefreshLocation); involuntary_disconnect(std::move(error_info), ConnectionTerminationReason::http_response_says_nonfatal_error); break; } case WebSocketError::websocket_abnormal_closure: { - SessionErrorInfo error_info({ErrorCodes::ConnectionClosed, msg}, IsFatal{false}); - error_info.server_requests_action = ProtocolErrorInfo::Action::RefreshUser; + SessionErrorInfo error_info({ErrorCodes::ConnectionClosed, msg}, IsFatal{false}, + ProtocolErrorInfo::Action::RefreshUser); involuntary_disconnect(std::move(error_info), ConnectionTerminationReason::http_response_says_nonfatal_error); break; @@ -618,7 +622,8 @@ bool Connection::websocket_closed_handler(bool was_clean, WebSocketError error_c case WebSocketError::websocket_internal_server_error: [[fallthrough]]; case WebSocketError::websocket_retry_error: { - involuntary_disconnect(SessionErrorInfo({ErrorCodes::ConnectionClosed, msg}, IsFatal{false}), + involuntary_disconnect(SessionErrorInfo({ErrorCodes::ConnectionClosed, msg}, IsFatal{false}, + ProtocolErrorInfo::Action::Transient), ConnectionTerminationReason::http_response_says_nonfatal_error); break; } @@ -809,7 +814,7 @@ void Connection::handle_connect_wait(Status status) logger.info("Connect timeout"); // Throws involuntary_disconnect( SessionErrorInfo{Status{ErrorCodes::SyncConnectTimeout, "Sync connection was not fully established in time"}, - IsFatal{false}}, + IsFatal{false}, ProtocolErrorInfo::Action::Transient}, ConnectionTerminationReason::sync_connect_timeout); // Throws } @@ -1115,26 +1120,26 @@ void Connection::handle_disconnect_wait(Status status) void Connection::close_due_to_protocol_error(Status status) { - SessionErrorInfo error_info(std::move(status), IsFatal{true}); - error_info.server_requests_action = ProtocolErrorInfo::Action::ProtocolViolation; + SessionErrorInfo error_info(std::move(status), IsFatal{true}, ProtocolErrorInfo::Action::ProtocolViolation); involuntary_disconnect(std::move(error_info), - ConnectionTerminationReason::sync_protocol_violation); // Throws + ConnectionTerminationReason::sync_protocol_violation); // TProtocolErrorInfo:ohrows } -void Connection::close_due_to_client_side_error(Status status, IsFatal is_fatal, ConnectionTerminationReason reason) +void Connection::close_due_to_client_side_error(Status status, IsFatal is_fatal, + ProtocolErrorInfo::Action error_action, + ConnectionTerminationReason reason) { logger.info("Connection closed due to error: %1", status); // Throws - - involuntary_disconnect(SessionErrorInfo{std::move(status), is_fatal}, reason); // Throw + SessionErrorInfo info{std::move(status), is_fatal, error_action}; + involuntary_disconnect(std::move(info), reason); // Throw } void Connection::close_due_to_transient_error(Status status, ConnectionTerminationReason reason) { logger.info("Connection closed due to transient error: %1", status); // Throws - SessionErrorInfo error_info{std::move(status), IsFatal{false}}; - error_info.server_requests_action = ProtocolErrorInfo::Action::Transient; + SessionErrorInfo error_info{std::move(status), IsFatal{false}, ProtocolErrorInfo::Action::Transient}; involuntary_disconnect(std::move(error_info), reason); // Throw } @@ -1583,7 +1588,8 @@ void Session::on_integration_failure(const IntegrationException& error) m_error_to_send = true; // Surface the error to the user otherwise is lost. - on_connection_state_changed(m_conn.get_state(), SessionErrorInfo{error.to_status(), IsFatal{false}}); + on_connection_state_changed( + m_conn.get_state(), SessionErrorInfo{error.to_status(), IsFatal{false}, ProtocolErrorInfo::Action::Warning}); // Since the deactivation process has not been initiated, the UNBIND // message cannot have been sent unless an ERROR message was received. @@ -1667,7 +1673,7 @@ void Session::activate() m_download_progress = m_progress.download; REALM_ASSERT_3(m_last_version_available, >=, m_progress.upload.client_version); - logger.debug("last_version_available = %1", m_last_version_available); // Throws + logger.debug("last_version_available = %1", m_last_version_available); // Throws logger.debug("progress_download_server_version = %1", m_progress.download.server_version); // Throws logger.debug("progress_download_client_version = %1", m_progress.download.last_integrated_client_version); // Throws @@ -1687,7 +1693,8 @@ void Session::activate() logger.error("Error integrating bootstrap changesets: %1", error.what()); m_suspended = true; m_conn.one_less_active_unsuspended_session(); // Throws - on_suspended(SessionErrorInfo{Status{error.code(), error.what()}, IsFatal{true}}); + on_suspended(SessionErrorInfo{Status{error.code(), error.what()}, IsFatal{true}, + ProtocolErrorInfo::Action::ApplicationBug}); } if (has_pending_client_reset) { @@ -2299,7 +2306,8 @@ Status Session::receive_ident_message(SaltedFileIdent client_file_ident) catch (const std::exception& e) { auto err_msg = util::format("A fatal error occurred during client reset: '%1'", e.what()); logger.error(err_msg.c_str()); - SessionErrorInfo err_info(Status{ErrorCodes::AutoClientResetFailed, err_msg}, IsFatal{true}); + SessionErrorInfo err_info(Status{ErrorCodes::AutoClientResetFailed, err_msg}, IsFatal{true}, + ProtocolErrorInfo::Action::BackupThenDeleteRealm); suspend(err_info); return Status::OK(); } @@ -2567,6 +2575,7 @@ void Session::suspend(const SessionErrorInfo& info) // Notify the application of the suspension of the session if the session is // still in the Active state if (m_state == Active) { + call_debug_hook(SyncClientHookEvent::SessionSuspended, info); m_conn.one_less_active_unsuspended_session(); // Throws on_suspended(info); // Throws } diff --git a/src/realm/sync/noinst/client_impl_base.hpp b/src/realm/sync/noinst/client_impl_base.hpp index a335dea4196..1fc0a3c070a 100644 --- a/src/realm/sync/noinst/client_impl_base.hpp +++ b/src/realm/sync/noinst/client_impl_base.hpp @@ -557,7 +557,8 @@ class ClientImpl::Connection { void initiate_disconnect_wait(); void handle_disconnect_wait(Status status); void close_due_to_protocol_error(Status status); - void close_due_to_client_side_error(Status, IsFatal is_fatal, ConnectionTerminationReason reason); + void close_due_to_client_side_error(Status, IsFatal is_fatal, ProtocolErrorInfo::Action error_action, + ConnectionTerminationReason reason); void close_due_to_transient_error(Status status, ConnectionTerminationReason reason); void close_due_to_server_side_error(ProtocolError, const ProtocolErrorInfo& info); void involuntary_disconnect(const SessionErrorInfo& info, ConnectionTerminationReason reason); @@ -1277,8 +1278,8 @@ void ClientImpl::Connection::for_each_active_session(H handler) inline void ClientImpl::Connection::voluntary_disconnect() { m_reconnect_info.update(ConnectionTerminationReason::closed_voluntarily, std::nullopt); - SessionErrorInfo error_info{Status{ErrorCodes::ConnectionClosed, "Connection closed"}, IsFatal{false}}; - error_info.server_requests_action = ProtocolErrorInfo::Action::Transient; + SessionErrorInfo error_info{Status{ErrorCodes::ConnectionClosed, "Connection closed"}, IsFatal{false}, + ProtocolErrorInfo::Action::Transient}; disconnect(std::move(error_info)); // Throws } diff --git a/src/realm/sync/noinst/protocol_codec.cpp b/src/realm/sync/noinst/protocol_codec.cpp index 109fd5732b6..b24c14ec7c1 100644 --- a/src/realm/sync/noinst/protocol_codec.cpp +++ b/src/realm/sync/noinst/protocol_codec.cpp @@ -269,15 +269,20 @@ void ServerProtocol::make_mark_message(OutputBuffer& out, session_ident_type ses } -void ServerProtocol::make_error_message(int protocol_version, OutputBuffer& out, sync::ProtocolError error_code, - const char* message, std::size_t message_size, bool try_again, - session_ident_type session_ident) +void ServerProtocol::make_json_error_message(int protocol_version, OutputBuffer& out, sync::ProtocolError error_code, + const char* message, std::size_t message_size, bool try_again, + session_ident_type session_ident) { static_cast(protocol_version); - sync::ProtocolError error_code_2 = error_code; - out << "error " << int(error_code_2) << " " << message_size << " " << int(try_again) << " " << session_ident - << "\n"; // Throws - out.write(message, message_size); // Throws + nlohmann::json error_body = {{"message", std::string_view(message, message_size)}, + {"tryAgain", try_again}, + {"action", try_again ? "Transient" : "ApplicationBug"}, + {"isRecoveryModeDisabled", false}, + {"logURL", ""}, + {"shouldClientReset", false}}; + std::string error_body_str = error_body.dump(); + out << "json_error " << int(error_code) << " " << error_body_str.size() << " " << session_ident << "\n"; // Throws + out.write(error_body_str.c_str(), error_body_str.size()); // Throws } diff --git a/src/realm/sync/noinst/protocol_codec.hpp b/src/realm/sync/noinst/protocol_codec.hpp index eeaf71c569c..aef614afa16 100644 --- a/src/realm/sync/noinst/protocol_codec.hpp +++ b/src/realm/sync/noinst/protocol_codec.hpp @@ -234,16 +234,6 @@ class ClientProtocol { auto session_ident = msg.read_next('\n'); connection.receive_unbound_message(session_ident); // Throws } - else if (message_type == "error") { - auto error_code = msg.read_next(); - auto message_size = msg.read_next(); - auto is_fatal = sync::IsFatal{!msg.read_next()}; - auto session_ident = msg.read_next('\n'); - auto message = msg.read_sized_data(message_size); - - connection.receive_error_message(sync::ProtocolErrorInfo{error_code, message, is_fatal}, - session_ident); // Throws - } else if (message_type == "log_message") { // introduced in protocol version 10 parse_log_message(connection, msg); } @@ -482,6 +472,7 @@ class ClientProtocol { {"ApplicationBug", action::ApplicationBug}, {"Warning", action::Warning}, {"Transient", action::Transient}, + {"BackupThenDeleteRealm", action::BackupThenDeleteRealm}, {"DeleteRealm", action::DeleteRealm}, {"ClientReset", action::ClientReset}, {"ClientResetNoRecovery", action::ClientResetNoRecovery}, @@ -614,8 +605,9 @@ class ServerProtocol { void make_mark_message(OutputBuffer&, session_ident_type session_ident, request_ident_type request_ident); - void make_error_message(int protocol_version, OutputBuffer&, sync::ProtocolError error_code, const char* message, - std::size_t message_size, bool try_again, session_ident_type session_ident); + void make_json_error_message(int protocol_version, OutputBuffer&, sync::ProtocolError error_code, + const char* message, std::size_t message_size, bool try_again, + session_ident_type session_ident); void make_pong(OutputBuffer&, milliseconds_type timestamp); diff --git a/src/realm/sync/noinst/server/server.cpp b/src/realm/sync/noinst/server/server.cpp index e55dfbcd126..7b1cdd34172 100644 --- a/src/realm/sync/noinst/server/server.cpp +++ b/src/realm/sync/noinst/server/server.cpp @@ -3119,8 +3119,8 @@ class Session final : private FileIdentReceiver { ServerProtocol& protocol = get_server_protocol(); OutputBuffer& out = m_connection.get_output_buffer(); int protocol_version = m_connection.get_client_protocol_version(); - protocol.make_error_message(protocol_version, out, error_code, message, message_size, try_again, - m_session_ident); // Throws + protocol.make_json_error_message(protocol_version, out, error_code, message, message_size, try_again, + m_session_ident); // Throws m_connection.initiate_write_output_buffer(); // Throws m_error_message_sent = true; @@ -4636,8 +4636,8 @@ void SyncConnection::initiate_write_error(ProtocolError error_code, session_iden OutputBuffer& out = get_output_buffer(); int protocol_version = get_client_protocol_version(); - get_server_protocol().make_error_message(protocol_version, out, error_code, message, message_size, try_again, - session_ident); // Throws + get_server_protocol().make_json_error_message(protocol_version, out, error_code, message, message_size, try_again, + session_ident); // Throws auto handler = [this](std::error_code ec, size_t) { handle_write_error(ec); // Throws diff --git a/src/realm/sync/protocol.hpp b/src/realm/sync/protocol.hpp index 3687ffc17e3..504e7cb7c97 100644 --- a/src/realm/sync/protocol.hpp +++ b/src/realm/sync/protocol.hpp @@ -271,16 +271,17 @@ struct ProtocolErrorInfo { RefreshUser, RefreshLocation, LogOutUser, + BackupThenDeleteRealm, }; ProtocolErrorInfo() = default; - ProtocolErrorInfo(int error_code, const std::string& msg, IsFatal is_fatal) + ProtocolErrorInfo(int error_code, const std::string& msg, IsFatal is_fatal, Action error_action) : raw_error_code(error_code) , message(msg) , is_fatal(is_fatal) , client_reset_recovery_is_disabled(false) , should_client_reset(util::none) - , server_requests_action(Action::NoAction) + , server_requests_action(error_action) { } int raw_error_code = 0; @@ -425,6 +426,8 @@ inline std::ostream& operator<<(std::ostream& o, ProtocolErrorInfo::Action actio return o << "Warning"; case ProtocolErrorInfo::Action::Transient: return o << "Transient"; + case ProtocolErrorInfo::Action::BackupThenDeleteRealm: + return o << "BackupThenDeleteRealm"; case ProtocolErrorInfo::Action::DeleteRealm: return o << "DeleteRealm"; case ProtocolErrorInfo::Action::ClientReset: diff --git a/test/object-store/c_api/c_api.cpp b/test/object-store/c_api/c_api.cpp index 2c8c826758f..c161fb442c6 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -5495,9 +5495,10 @@ TEST_CASE("C API - client reset", "[sync][pbs][c_api][client reset][baas]") { REQUIRE(sync_error.c_original_file_path_key); REQUIRE(sync_error.c_recovery_file_path_key); REQUIRE(sync_error.is_client_reset_requested); + REQUIRE(sync_error.status.error == RLM_ERR_AUTO_CLIENT_RESET_FAILED); // Callback in `realm_sync_config_set_before_client_reset_handler` fails, so - // a synthetic error is created with no action. - REQUIRE(sync_error.server_requests_action == RLM_SYNC_ERROR_ACTION_NO_ACTION); + // a synthetic error is created that results in the sync session being made inactive. + REQUIRE(sync_error.server_requests_action == RLM_SYNC_ERROR_ACTION_BACKUP_THEN_DELETE_REALM); ResetRealmFiles::instance().reset_realm(sync_error.c_original_file_path_key); error_handler_counter.fetch_add(1); baas_client_stop.store(true); diff --git a/test/object-store/sync/client_reset.cpp b/test/object-store/sync/client_reset.cpp index e6d61a4710f..1868ed6e78e 100644 --- a/test/object-store/sync/client_reset.cpp +++ b/test/object-store/sync/client_reset.cpp @@ -957,8 +957,7 @@ TEST_CASE("sync: client reset", "[sync][pbs][client reset][baas]") { REQUIRE(session); } sync::SessionErrorInfo synthetic(Status{ErrorCodes::SyncClientResetRequired, "A fake client reset error"}, - sync::IsFatal{true}); - synthetic.server_requests_action = sync::ProtocolErrorInfo::Action::ClientReset; + sync::IsFatal{true}, sync::ProtocolErrorInfo::Action::ClientReset); SyncSession::OnlyForTesting::handle_error(*session, std::move(synthetic)); session->revive_if_needed(); diff --git a/test/object-store/sync/flx_sync.cpp b/test/object-store/sync/flx_sync.cpp index 7c8088a2086..b083b1a15d6 100644 --- a/test/object-store/sync/flx_sync.cpp +++ b/test/object-store/sync/flx_sync.cpp @@ -268,6 +268,13 @@ static auto make_error_handler() return std::make_pair(std::move(error_future), std::move(fn)); } +static auto install_error_handler(Realm::Config& config) +{ + auto&& [error_future, error_handler] = make_error_handler(); + config.sync_config->error_handler = std::move(error_handler); + return std::move(error_future); +} + static auto make_client_reset_handler() { auto [reset_promise, reset_future] = util::make_promise_future(); @@ -283,11 +290,10 @@ TEST_CASE("app: error handling integration test", "[sync][flx][baas]") { static std::optional harness{"error_handling"}; create_user_and_log_in(harness->app()); SyncTestFile config(harness->app()->current_user(), harness->schema(), SyncConfig::FLXSyncEnabled{}); - auto&& [error_future, error_handler] = make_error_handler(); - config.sync_config->error_handler = std::move(error_handler); config.sync_config->client_resync_mode = ClientResyncMode::Manual; SECTION("handles unknown errors gracefully") { + auto error_future = install_error_handler(config); auto r = Realm::get_shared_realm(config); wait_for_download(*r); nlohmann::json error_body = { @@ -308,9 +314,99 @@ TEST_CASE("app: error handling integration test", "[sync][flx][baas]") { REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("Unknown sync protocol error code 299")); REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error")); + + // since the client resync mode is manual here we should have ended up in an inactive state. + REQUIRE(r->sync_session()->state() == SyncSession::State::Inactive); + } + + SECTION("unknown errors that are warnings end up in an active state") { + auto error_future = install_error_handler(config); + auto r = Realm::get_shared_realm(config); + wait_for_download(*r); + nlohmann::json error_body = { + {"tryAgain", false}, {"message", "fake error"}, + {"shouldClientReset", false}, {"isRecoveryModeDisabled", false}, + {"action", "Warning"}, + }; + nlohmann::json test_command = {{"command", "ECHO_ERROR"}, + {"args", nlohmann::json{{"errorCode", 299}, {"errorBody", error_body}}}}; + auto test_cmd_res = + wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump())) + .get(); + REQUIRE(test_cmd_res == "{}"); + auto error = wait_for_future(std::move(error_future)).get(); + REQUIRE(error.status == ErrorCodes::UnknownError); + REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::Warning); + REQUIRE(error.is_fatal); + REQUIRE_THAT(error.status.reason(), + Catch::Matchers::ContainsSubstring("Unknown sync protocol error code 299")); + REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error")); + + REQUIRE(r->sync_session()->state() == SyncSession::State::Active); + } + + SECTION("transient errors do not surface to error handler") { + std::mutex error_mutex; + std::condition_variable error_cv; + std::vector errors; + bool error_handler_called = false; + config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr, + const SyncClientHookData& data) { + if (data.event != SyncClientHookEvent::SessionSuspended) { + return SyncClientHookAction::NoAction; + } + + std::lock_guard lock{error_mutex}; + errors.push_back(*data.error_info); + error_cv.notify_one(); + return SyncClientHookAction::NoAction; + }; + + config.sync_config->error_handler = [&](std::shared_ptr, SyncError) { + std::lock_guard lock{error_mutex}; + error_handler_called = true; + error_cv.notify_one(); + }; + + auto r = Realm::get_shared_realm(config); + wait_for_upload(*r); + nlohmann::json error_body = { + {"tryAgain", true}, {"message", "transient fake error"}, + {"shouldClientReset", false}, {"isRecoveryModeDisabled", false}, + {"action", "Transient"}, + }; + nlohmann::json test_command = { + {"command", "ECHO_ERROR"}, + {"args", nlohmann::json{{"errorCode", static_cast(sync::ProtocolError::initial_sync_not_completed)}, + {"errorBody", error_body}}}}; + auto test_cmd_res = + wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump())) + .get(); + REQUIRE(test_cmd_res == "{}"); + + std::unique_lock lock{error_mutex}; + error_cv.wait(lock, [&] { + return error_handler_called || !errors.empty(); + }); + + REQUIRE(!errors.empty()); + lock.unlock(); + wait_for_download(*r); + lock.lock(); + REQUIRE(!error_handler_called); + const auto error = std::move(errors.back()); + lock.unlock(); + + REQUIRE(!error.is_fatal); + REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::Transient); + REQUIRE(error.should_client_reset); + REQUIRE(!error.should_client_reset.value()); + REQUIRE(static_cast(error.raw_error_code) == + sync::ProtocolError::initial_sync_not_completed); } SECTION("unknown errors without actions are application bugs") { + auto error_future = install_error_handler(config); auto r = Realm::get_shared_realm(config); wait_for_download(*r); nlohmann::json error_body = { @@ -332,9 +428,69 @@ TEST_CASE("app: error handling integration test", "[sync][flx][baas]") { REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("Unknown sync protocol error code 299")); REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error")); + REQUIRE(r->sync_session()->state() == SyncSession::State::Inactive); } + SECTION("cannot resume after fatal error") { + enum class BarrierState { WaitingForSuspend, WaitingForResume, Done }; + std::mutex barrier_mutex; + std::condition_variable barrier_cv; + BarrierState barrier_state = BarrierState::WaitingForSuspend; + config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr, + const SyncClientHookData& data) { + if (data.event != SyncClientHookEvent::SessionSuspended || !data.error_info->is_fatal) { + return SyncClientHookAction::NoAction; + } + + std::unique_lock lock{barrier_mutex}; + REALM_ASSERT(barrier_state == BarrierState::WaitingForSuspend); + barrier_state = BarrierState::WaitingForResume; + barrier_cv.notify_one(); + barrier_cv.wait(lock, [&] { + return barrier_state == BarrierState::Done; + }); + return SyncClientHookAction::NoAction; + }; + auto error_future = install_error_handler(config); + auto r = Realm::get_shared_realm(config); + wait_for_upload(*r); + nlohmann::json error_body = { + {"tryAgain", false}, + {"message", "fake error"}, + {"shouldClientReset", false}, + {"isRecoveryModeDisabled", false}, + }; + nlohmann::json test_command = {{"command", "ECHO_ERROR"}, + {"args", nlohmann::json{{"errorCode", 299}, {"errorBody", error_body}}}}; + auto test_cmd_res = + wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump())) + .get(); + REQUIRE(test_cmd_res == "{}"); + + // Resume the session while the error is being handled but before the session is marked inactive. + { + std::unique_lock lock{barrier_mutex}; + barrier_cv.wait(lock, [&] { + return barrier_state == BarrierState::WaitingForResume; + }); + r->sync_session()->handle_reconnect(); + barrier_state = BarrierState::Done; + barrier_cv.notify_one(); + } + + auto error = wait_for_future(std::move(error_future)).get(); + REQUIRE(error.status == ErrorCodes::UnknownError); + REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug); + REQUIRE(error.is_fatal); + REQUIRE_THAT(error.status.reason(), + Catch::Matchers::ContainsSubstring("Unknown sync protocol error code 299")); + REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error")); + REQUIRE(r->sync_session()->state() == SyncSession::State::Inactive); + } + + SECTION("handles unknown actions gracefully") { + auto error_future = install_error_handler(config); auto r = Realm::get_shared_realm(config); wait_for_download(*r); nlohmann::json error_body = { @@ -356,10 +512,12 @@ TEST_CASE("app: error handling integration test", "[sync][flx][baas]") { REQUIRE(error.is_fatal); REQUIRE_THAT(error.status.reason(), !Catch::Matchers::ContainsSubstring("Unknown sync protocol error code")); REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error")); + REQUIRE(r->sync_session()->state() == SyncSession::State::Inactive); } SECTION("unknown connection-level errors are still errors") { + auto error_future = install_error_handler(config); auto r = Realm::get_shared_realm(config); wait_for_download(*r); nlohmann::json error_body = {{"tryAgain", false}, @@ -377,9 +535,11 @@ TEST_CASE("app: error handling integration test", "[sync][flx][baas]") { REQUIRE(error.status == ErrorCodes::SyncProtocolInvariantFailed); REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ProtocolViolation); REQUIRE(error.is_fatal); + REQUIRE(r->sync_session()->state() == SyncSession::State::Inactive); } SECTION("client reset errors") { + auto error_future = install_error_handler(config); auto r = Realm::get_shared_realm(config); wait_for_download(*r); nlohmann::json error_body = {{"tryAgain", false}, @@ -400,6 +560,33 @@ TEST_CASE("app: error handling integration test", "[sync][flx][baas]") { REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset); REQUIRE(error.is_client_reset_requested()); REQUIRE(error.is_fatal); + // since the client resync mode is manual here we should have ended up in an inactive state. + REQUIRE(r->sync_session()->state() == SyncSession::State::Inactive); + } + + SECTION("log out user action") { + auto error_future = install_error_handler(config); + auto r = Realm::get_shared_realm(config); + wait_for_download(*r); + nlohmann::json error_body = {{"tryAgain", false}, + {"message", "fake error"}, + {"shouldClientReset", false}, + {"isRecoveryModeDisabled", false}, + {"action", "LogOutUser"}}; + nlohmann::json test_command = { + {"command", "ECHO_ERROR"}, + {"args", nlohmann::json{{"errorCode", sync::ProtocolError::user_mismatch}, {"errorBody", error_body}}}}; + auto test_cmd_res = + wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump())) + .get(); + REQUIRE(test_cmd_res == "{}"); + auto error = wait_for_future(std::move(error_future)).get(); + REQUIRE(error.status == ErrorCodes::SyncUserMismatch); + REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::LogOutUser); + REQUIRE(!error.is_client_reset_requested()); + REQUIRE(error.is_fatal); + REQUIRE(r->sync_session()->state() == SyncSession::State::Inactive); + REQUIRE(!r->sync_session()->user()->is_logged_in()); } SECTION("teardown") { diff --git a/test/object-store/sync/session/session.cpp b/test/object-store/sync/session/session.cpp index 7839507ff9e..ed8d9d76981 100644 --- a/test/object-store/sync/session/session.cpp +++ b/test/object-store/sync/session/session.cpp @@ -405,8 +405,8 @@ TEST_CASE("sync: error handling", "[sync][session]") { return sessions_are_active(*session); }); - sync::SessionErrorInfo err{Status{ErrorCodes::UnknownError, "unknown error"}, true}; - err.server_requests_action = ProtocolErrorInfo::Action::Transient; + sync::SessionErrorInfo err{Status{ErrorCodes::UnknownError, "unknown error"}, true, + ProtocolErrorInfo::Action::Transient}; SyncSession::OnlyForTesting::handle_error(*session, std::move(err)); CHECK(!sessions_are_inactive(*session)); // Error is non-fatal so it's not reported to the error handler @@ -425,8 +425,8 @@ TEST_CASE("sync: error handling", "[sync][session]") { auto code = GENERATE(ProtocolError::bad_client_file_ident, ProtocolError::bad_server_version, ProtocolError::diverging_histories); - sync::SessionErrorInfo initial_error{sync::protocol_error_to_status(code, "Something bad happened"), true}; - initial_error.server_requests_action = ProtocolErrorInfo::Action::ClientReset; + sync::SessionErrorInfo initial_error{sync::protocol_error_to_status(code, "Something bad happened"), true, + ProtocolErrorInfo::Action::ClientReset}; std::time_t just_before_raw = std::time(nullptr); SyncSession::OnlyForTesting::handle_error(*session, std::move(initial_error)); REQUIRE(session->state() == SyncSession::State::Inactive); @@ -535,8 +535,8 @@ TEST_CASE("sync: stop policy behavior", "[sync][session]") { SECTION("transitions to Inactive if a fatal error occurs") { sync::SessionErrorInfo err{Status{ErrorCodes::SyncProtocolInvariantFailed, "Not a real error message"}, - sync::IsFatal{true}}; - err.server_requests_action = realm::sync::ProtocolErrorInfo::Action::ProtocolViolation; + sync::IsFatal{true}, + realm::sync::ProtocolErrorInfo::Action::ProtocolViolation}; SyncSession::OnlyForTesting::handle_error(*session, std::move(err)); CHECK(sessions_are_inactive(*session)); // The session shouldn't report fatal errors when in the dying state. @@ -546,8 +546,7 @@ TEST_CASE("sync: stop policy behavior", "[sync][session]") { SECTION("ignores non-fatal errors and does not transition to Inactive") { // Fire a simulated *non-fatal* error. sync::SessionErrorInfo err{Status{ErrorCodes::ConnectionClosed, "Not a real error message"}, - sync::IsFatal{false}}; - err.server_requests_action = realm::sync::ProtocolErrorInfo::Action::Transient; + sync::IsFatal{false}, realm::sync::ProtocolErrorInfo::Action::Transient}; SyncSession::OnlyForTesting::handle_error(*session, std::move(err)); REQUIRE(session->state() == SyncSession::State::Dying); CHECK(!error_handler_invoked); diff --git a/test/object-store/sync/session/wait_for_completion.cpp b/test/object-store/sync/session/wait_for_completion.cpp index 02f164d973d..278861e29a4 100644 --- a/test/object-store/sync/session/wait_for_completion.cpp +++ b/test/object-store/sync/session/wait_for_completion.cpp @@ -106,8 +106,8 @@ TEST_CASE("SyncSession: wait_for_download_completion() API", "[sync][pbs][sessio }); REQUIRE(handler_called == false); // Now trigger an error - sync::SessionErrorInfo err{err_status, sync::IsFatal{true}}; - err.server_requests_action = sync::ProtocolErrorInfo::Action::ProtocolViolation; + sync::SessionErrorInfo err{err_status, sync::IsFatal{true}, + sync::ProtocolErrorInfo::Action::ProtocolViolation}; SyncSession::OnlyForTesting::handle_error(*session, std::move(err)); EventLoop::main().run_until([&] { return error_count > 0; diff --git a/test/test_sync.cpp b/test/test_sync.cpp index ccaec4b1a13..af848ba7c18 100644 --- a/test/test_sync.cpp +++ b/test/test_sync.cpp @@ -3961,52 +3961,6 @@ TEST(Sync_CancelReconnectDelay) session.wait_for_download_complete_or_client_stopped(); } } - - // After session-level error, and at session-level. - { - ClientServerFixture fixture{server_dir, test_context, std::move(fixture_config)}; - fixture.start(); - - // Add a session for the purpose of keeping the connection open - Session session_x = fixture.make_bound_session(db_x, "/x"); - session_x.wait_for_download_complete_or_client_stopped(); - - BowlOfStonesSemaphore bowl; - auto handler = [&](const SessionErrorInfo& info) { - if (CHECK_EQUAL(info.status, ErrorCodes::BadSyncPartitionValue)) - bowl.add_stone(); - }; - Session session = fixture.make_session(db, "/.."); - session.set_error_handler(std::move(handler)); - session.bind(); - bowl.get_stone(); - - session.cancel_reconnect_delay(); - bowl.get_stone(); - } - - // After session-level error, and at client-level. - { - ClientServerFixture fixture{server_dir, test_context, std::move(fixture_config)}; - fixture.start(); - - // Add a session for the purpose of keeping the connection open - Session session_x = fixture.make_bound_session(db_x, "/x"); - session_x.wait_for_download_complete_or_client_stopped(); - - BowlOfStonesSemaphore bowl; - auto handler = [&](const SessionErrorInfo& info) { - if (CHECK_EQUAL(info.status, ErrorCodes::BadSyncPartitionValue)) - bowl.add_stone(); - }; - Session session = fixture.make_session(db, "/.."); - session.set_error_handler(std::move(handler)); - session.bind(); - bowl.get_stone(); - - fixture.cancel_reconnect_delay(); - bowl.get_stone(); - } }