From 4aa15f695565b168fdc05cce6e834e50b9acb34e Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Mon, 3 Mar 2025 09:12:48 -0800 Subject: [PATCH] Enable automatic client reset handling for audit Realms (#8072) Audit Realms typically don't get client resets due to being very short lived, but restarting sync while one is partially uploaded can result in a bad client file ident error which also blocks uploading any other unuploaded files. Standard automatic client reset is pretty inefficient for these files, but it works fine and the optimal thing would be fairly complicated. --- CHANGELOG.md | 2 +- src/realm/object-store/audit.mm | 3 +- test/object-store/audit.cpp | 108 ++++++++++++++++++++++++++++++-- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 021a2f960e..66424d1efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Enhancements * (PR [#????](https://github.com/realm/realm-core/pull/????)) -* None. +* Enable automatic client reset recovery for audit Realm files ([PR #8072](https://github.com/realm/realm-core/pull/8072)). ### Fixed * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) diff --git a/src/realm/object-store/audit.mm b/src/realm/object-store/audit.mm index 85313cf96c..49dda63a94 100644 --- a/src/realm/object-store/audit.mm +++ b/src/realm/object-store/audit.mm @@ -840,6 +840,7 @@ explicit AuditRealmPool(Private, std::shared_ptr user, const AuditConf RealmConfig config; config.path = db->get_path(); config.sync_config = std::make_shared(m_user, prefixed_partition(partition)); + config.sync_config->client_resync_mode = ClientResyncMode::Recover; wait_for_upload(m_user->sync_manager()->get_session(db, config)); return; } @@ -897,7 +898,7 @@ explicit AuditRealmPool(Private, std::shared_ptr user, const AuditConf std::string partition = ObjectId::gen().to_string(); auto sync_config = std::make_shared(m_user, prefixed_partition(partition)); sync_config->apply_server_changes = false; - sync_config->client_resync_mode = ClientResyncMode::Manual; + sync_config->client_resync_mode = ClientResyncMode::Recover; sync_config->recovery_directory = std::string("io.realm.audit"); sync_config->stop_policy = SyncSessionStopPolicy::Immediately; sync_config->error_handler = [error_handler = m_error_handler, weak_self = weak_from_this()](auto, diff --git a/test/object-store/audit.cpp b/test/object-store/audit.cpp index 3e3432f015..e4ebd9f4f7 100644 --- a/test/object-store/audit.cpp +++ b/test/object-store/audit.cpp @@ -22,9 +22,11 @@ #include #include -#include -#include #include +#include +#include +#include +#include #include #include @@ -41,10 +43,7 @@ #include #include -#include - #include - #include using namespace realm; @@ -70,6 +69,14 @@ struct AuditEvent { std::map metadata; }; +std::ostream& operator<<(std::ostream& os, const std::vector& events) +{ + for (auto& event : events) { + util::format(os, "%1: %2\n", event.event, event.data); + } + return os; +} + util::Optional to_optional_string(StringData sd) { return sd ? util::Optional(sd) : none; @@ -1455,6 +1462,7 @@ TEST_CASE("audit management", "[sync][pbs][audit]") { audit->wait_for_completion(); auto events = get_audit_events(test_session); + INFO(events); REQUIRE(events.size() == 6); std::string str = events[0].data.dump(); // initial @@ -1928,6 +1936,96 @@ TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") { } } + SECTION("creating audit event while offline uploads event when logged back in") { + auto sync_user = session.app()->current_user(); + auto creds = create_user_and_log_in(session.app()); + auto audit_user = session.app()->current_user(); + config.audit_config->audit_user = audit_user; + config.audit_config->sync_error_handler = [&](SyncError error) { + REALM_ASSERT(ErrorCodes::error_categories(error.status.code()).test(ErrorCategory::app_error)); + }; + auto realm = Realm::get_shared_realm(config); + + audit_user->log_out(); + generate_event(realm); + log_in_user(session.app(), creds); + + REQUIRE(get_audit_events_from_baas(session, *sync_user, 1).size() == 1); + } + + SECTION("files with invalid client file idents are recovered") { + auto sync_user = session.app()->current_user(); + auto creds = create_user_and_log_in(session.app()); + auto audit_user = session.app()->current_user(); + config.audit_config->audit_user = audit_user; + config.audit_config->sync_error_handler = [&](SyncError error) { + REALM_ASSERT(ErrorCodes::error_categories(error.status.code()).test(ErrorCategory::app_error)); + }; + auto realm = Realm::get_shared_realm(config); + audit_user->log_out(); + + auto audit = realm->audit_context(); + REQUIRE(audit); + + // Set a small shard size so that we don't have to write an absurd + // amount of data to test this + audit_test_hooks::set_maximum_shard_size(32 * 1024); + auto cleanup = util::make_scope_exit([]() noexcept { + audit_test_hooks::set_maximum_shard_size(256 * 1024 * 1024); + }); + + realm->begin_transaction(); + auto table = realm->read_group().get_table("class_object"); + std::vector objects; + for (int i = 0; i < 2000; ++i) + objects.push_back(table->create_object_with_primary_key(i)); + realm->commit_transaction(); + + // Write a lot of audit scopes while unable to sync + for (int i = 0; i < 50; ++i) { + auto scope = audit->begin_scope(util::format("scope %1", i)); + Results(realm, table->where()).snapshot(); + audit->end_scope(scope, assert_no_error); + } + audit->wait_for_completion(); + + // Client file idents aren't reread while a session is active, so we need + // to close all of the open audit Realms awaiting upload + realm->close(); + realm = nullptr; + auto sync_manager = session.sync_manager(); + for (auto& session : sync_manager->get_all_sessions()) { + session->shutdown_and_wait(); + } + + // Set the client file ident for all pending Realms to an invalid one so + // that they'll get client resets + auto root = util::format("%1/realm-audit/%2/%3/audit", *session.config().storage_path, + session.app()->app_id(), audit_user->user_id()); + std::string file_name; + util::DirScanner dir(root); + while (dir.next(file_name)) { + if (!StringData(file_name).ends_with(".realm") || StringData(file_name).contains(".backup.")) + continue; + sync::ClientReplication repl; + auto db = DB::create(repl, root + "/" + file_name); + static_cast(repl._get_history_write())->set_client_file_ident({123, 456}, false); + } + + // Log the user back in and reopen the parent Realm to start trying to upload the audit data + log_in_user(session.app(), creds); + realm = Realm::get_shared_realm(config); + audit = realm->audit_context(); + REQUIRE(audit); + audit->wait_for_uploads(); + + auto events = get_audit_events_from_baas(session, *sync_user, 50); + REQUIRE(events.size() == 50); + for (int i = 0; i < 50; ++i) { + REQUIRE(events[i].activity == util::format("scope %1", i)); + } + } + #if 0 // This test takes ~10 minutes to run SECTION("large audit scope") { auto realm = Realm::get_shared_realm(config);