From 467eafec261966706936904f69f18041ad7e5870 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 3 Apr 2024 08:20:39 -0700 Subject: [PATCH] Truncate the metadata realm in-place rather than deleting and recreating it (#7526) This solves some locking problems in multi-process scenarios. The flow of checking if the key is valid, deleting the file if not, and then recreating the file if needed needs to be done with an exclusive lock on the file held. This can't be done with DB::call_with_lock() because there isn't a good way to initialize the new file from within the call_with_lock() callback, and instead needs to be pushed into the file initialization guarded by the control mutex. Co-authored-by: Finn Schiermer Andersen --- CHANGELOG.md | 1 + src/realm/alloc_slab.cpp | 139 ++++---- src/realm/alloc_slab.hpp | 20 +- src/realm/db.cpp | 14 +- src/realm/db.hpp | 13 +- src/realm/db_options.hpp | 7 + .../object-store/impl/realm_coordinator.cpp | 4 +- src/realm/object-store/shared_realm.hpp | 7 + .../object-store/sync/impl/sync_metadata.cpp | 11 +- test/benchmark-common-tasks/main.cpp | 4 +- test/test_client_reset.cpp | 3 +- test/test_lang_bind_helper.cpp | 6 +- test/test_shared.cpp | 304 ++++++++++++------ test/test_upgrade_database.cpp | 10 +- 14 files changed, 341 insertions(+), 202 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd29dd712c3..54eae0ed7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Internals * Update libuv used in object store tests from v1.35.0 to v1.48.0 ([PR #7508](https://github.com/realm/realm-core/pull/7508)). * Made `set_default_logger` nullable in the bindgen spec.yml (PR [#7515](https://github.com/realm/realm-core/pull/7515)). +* Recreating the sync metadata Realm when the encryption key changes is now done in a multi-process safe manner ([PR #7526](https://github.com/realm/realm-core/pull/7526)). * Added `App::default_base_url()` static accessor for SDKs to retrieve the default base URL from Core. ([PR #7534](https://github.com/realm/realm-core/pull/7534)) * Realm2JSON tool will now correctly upgrade file to current fileformat. diff --git a/src/realm/alloc_slab.cpp b/src/realm/alloc_slab.cpp index d79b4724c70..60b441a940b 100644 --- a/src/realm/alloc_slab.cpp +++ b/src/realm/alloc_slab.cpp @@ -813,17 +813,39 @@ ref_type SlabAlloc::attach_file(const std::string& path, Config& cfg, util::Writ // the call below to set_encryption_key. m_file.set_encryption_key(cfg.encryption_key); + note_reader_start(this); + util::ScopeExit reader_end_guard([this]() noexcept { + note_reader_end(this); + }); size_t size = 0; // The size of a database file must not exceed what can be encoded in // size_t. if (REALM_UNLIKELY(int_cast_with_overflow_detect(m_file.get_size(), size))) throw InvalidDatabase("Realm file too large", path); - if (cfg.encryption_key && size == 0 && physical_file_size != 0) { + if (cfg.clear_file_on_error && cfg.session_initiator) { + if (size == 0 && physical_file_size != 0) { + cfg.clear_file = true; + } + else if (size > 0) { + try { + read_and_validate_header(m_file, path, size, cfg.session_initiator, m_write_observer); + } + catch (const InvalidDatabase&) { + cfg.clear_file = true; + } + } + } + if (cfg.clear_file) { + m_file.resize(0); + size = 0; + physical_file_size = 0; + } + else if (cfg.encryption_key && !cfg.clear_file && size == 0 && physical_file_size != 0) { // The opened file holds data, but is so small it cannot have // been created with encryption throw InvalidDatabase("Attempt to open unencrypted file with encryption key", path); } - if (size == 0 || cfg.clear_file) { + if (size == 0) { if (REALM_UNLIKELY(cfg.read_only)) throw InvalidDatabase("Read-only access to empty Realm file", path); @@ -833,7 +855,7 @@ ref_type SlabAlloc::attach_file(const std::string& path, Config& cfg, util::Writ // mappings_for_file in the encryption layer. So the prealloc() is required before // interacting with the encryption layer in File::write(). // Pre-alloc initial space - m_file.prealloc(initial_size); // Throws + m_file.prealloc(initial_size); // Throws // seek() back to the start of the file in preparation for writing the header // This sequence of File operations is protected from races by // DB::m_controlmutex, so we know we are the only ones operating on the file @@ -847,65 +869,9 @@ ref_type SlabAlloc::attach_file(const std::string& path, Config& cfg, util::Writ size = initial_size; } - ref_type top_ref; - note_reader_start(this); - util::ScopeExit reader_end_guard([this]() noexcept { - note_reader_end(this); - }); - - try { - // we'll read header and (potentially) footer - File::Map map_header(m_file, File::access_ReadOnly, sizeof(Header), 0, m_write_observer); - realm::util::encryption_read_barrier(map_header, 0, sizeof(Header)); - auto header = reinterpret_cast(map_header.get_addr()); - File::Map map_footer; - const StreamingFooter* footer = nullptr; - if (is_file_on_streaming_form(*header) && size >= sizeof(StreamingFooter) + sizeof(Header)) { - size_t footer_ref = size - sizeof(StreamingFooter); - size_t footer_page_base = footer_ref & ~(page_size() - 1); - size_t footer_offset = footer_ref - footer_page_base; - map_footer = File::Map(m_file, footer_page_base, File::access_ReadOnly, - sizeof(StreamingFooter) + footer_offset, 0, m_write_observer); - realm::util::encryption_read_barrier(map_footer, footer_offset, sizeof(StreamingFooter)); - footer = reinterpret_cast(map_footer.get_addr() + footer_offset); - } - - top_ref = validate_header(header, footer, size, path, cfg.encryption_key != nullptr); // Throws - m_attach_mode = cfg.is_shared ? attach_SharedFile : attach_UnsharedFile; - m_data = map_header.get_addr(); // <-- needed below - - if (cfg.session_initiator && is_file_on_streaming_form(*header)) { - // Don't compare file format version fields as they are allowed to differ. - // Also don't compare reserved fields. - REALM_ASSERT_EX(header->m_flags == 0, header->m_flags, get_file_path_for_assertions()); - REALM_ASSERT_EX(header->m_mnemonic[0] == uint8_t('T'), header->m_mnemonic[0], - get_file_path_for_assertions()); - REALM_ASSERT_EX(header->m_mnemonic[1] == uint8_t('-'), header->m_mnemonic[1], - get_file_path_for_assertions()); - REALM_ASSERT_EX(header->m_mnemonic[2] == uint8_t('D'), header->m_mnemonic[2], - get_file_path_for_assertions()); - REALM_ASSERT_EX(header->m_mnemonic[3] == uint8_t('B'), header->m_mnemonic[3], - get_file_path_for_assertions()); - REALM_ASSERT_EX(header->m_top_ref[0] == 0xFFFFFFFFFFFFFFFFULL, header->m_top_ref[0], - get_file_path_for_assertions()); - REALM_ASSERT_EX(header->m_top_ref[1] == 0, header->m_top_ref[1], get_file_path_for_assertions()); - REALM_ASSERT_EX(footer->m_magic_cookie == footer_magic_cookie, footer->m_magic_cookie, - get_file_path_for_assertions()); - } - } - catch (const InvalidDatabase&) { - throw; - } - catch (const DecryptionFailed& e) { - throw InvalidDatabase(util::format("Realm file decryption failed (%1)", e.what()), path); - } - catch (const std::exception& e) { - throw InvalidDatabase(e.what(), path); - } - catch (...) { - throw InvalidDatabase("unknown error", path); - } + ref_type top_ref = read_and_validate_header(m_file, path, size, cfg.session_initiator, m_write_observer); + m_attach_mode = cfg.is_shared ? attach_SharedFile : attach_UnsharedFile; // m_data not valid at this point! m_baseline = 0; // make sure that any call to begin_read cause any slab to be placed in free @@ -1040,6 +1006,57 @@ void SlabAlloc::attach_empty() m_ref_translation_ptr = new RefTranslation[1]; } +ref_type SlabAlloc::read_and_validate_header(util::File& file, const std::string& path, size_t size, + bool session_initiator, util::WriteObserver* write_observer) +{ + try { + // we'll read header and (potentially) footer + File::Map map_header(file, File::access_ReadOnly, sizeof(Header), 0, write_observer); + realm::util::encryption_read_barrier(map_header, 0, sizeof(Header)); + auto header = reinterpret_cast(map_header.get_addr()); + + File::Map map_footer; + const StreamingFooter* footer = nullptr; + if (is_file_on_streaming_form(*header) && size >= sizeof(StreamingFooter) + sizeof(Header)) { + size_t footer_ref = size - sizeof(StreamingFooter); + size_t footer_page_base = footer_ref & ~(page_size() - 1); + size_t footer_offset = footer_ref - footer_page_base; + map_footer = File::Map(file, footer_page_base, File::access_ReadOnly, + sizeof(StreamingFooter) + footer_offset, 0, write_observer); + realm::util::encryption_read_barrier(map_footer, footer_offset, sizeof(StreamingFooter)); + footer = reinterpret_cast(map_footer.get_addr() + footer_offset); + } + + auto top_ref = validate_header(header, footer, size, path, file.get_encryption_key() != nullptr); // Throws + + if (session_initiator && is_file_on_streaming_form(*header)) { + // Don't compare file format version fields as they are allowed to differ. + // Also don't compare reserved fields. + REALM_ASSERT_EX(header->m_flags == 0, header->m_flags, path); + REALM_ASSERT_EX(header->m_mnemonic[0] == uint8_t('T'), header->m_mnemonic[0], path); + REALM_ASSERT_EX(header->m_mnemonic[1] == uint8_t('-'), header->m_mnemonic[1], path); + REALM_ASSERT_EX(header->m_mnemonic[2] == uint8_t('D'), header->m_mnemonic[2], path); + REALM_ASSERT_EX(header->m_mnemonic[3] == uint8_t('B'), header->m_mnemonic[3], path); + REALM_ASSERT_EX(header->m_top_ref[0] == 0xFFFFFFFFFFFFFFFFULL, header->m_top_ref[0], path); + REALM_ASSERT_EX(header->m_top_ref[1] == 0, header->m_top_ref[1], path); + REALM_ASSERT_EX(footer->m_magic_cookie == footer_magic_cookie, footer->m_magic_cookie, path); + } + return top_ref; + } + catch (const InvalidDatabase&) { + throw; + } + catch (const DecryptionFailed& e) { + throw InvalidDatabase(util::format("Realm file decryption failed (%1)", e.what()), path); + } + catch (const std::exception& e) { + throw InvalidDatabase(e.what(), path); + } + catch (...) { + throw InvalidDatabase("unknown error", path); + } +} + void SlabAlloc::throw_header_exception(std::string msg, const Header& header, const std::string& path) { char buf[256]; diff --git a/src/realm/alloc_slab.hpp b/src/realm/alloc_slab.hpp index 7e9ce1f115b..e1ad8a0ca9f 100644 --- a/src/realm/alloc_slab.hpp +++ b/src/realm/alloc_slab.hpp @@ -108,15 +108,20 @@ class SlabAlloc : public Allocator { /// Always initialize the file as if it was a newly /// created file and ignore any pre-existing contents. Requires that /// Config::session_initiator be true as well. + /// + /// \var Config::clear_file_on_error + /// If the file being opened is not a valid Realm file (possibly due to a + /// decryption failure), reinitialize it as if clear_file was set. struct Config { + const char* encryption_key = nullptr; bool is_shared = false; bool read_only = false; bool no_create = false; bool skip_validate = false; bool session_initiator = false; bool clear_file = false; + bool clear_file_on_error = false; bool disable_sync = false; - const char* encryption_key = nullptr; }; struct Retry {}; @@ -363,6 +368,11 @@ class SlabAlloc : public Allocator { void note_reader_start(const void* reader_id); void note_reader_end(const void* reader_id) noexcept; + /// Read the header (and possibly footer) from the file, returning the top ref if it's valid and throwing + /// InvalidDatabase otherwise. + static ref_type read_and_validate_header(util::File& file, const std::string& path, size_t size, + bool session_initiator, util::WriteObserver* write_observer); + void verify() const override; #ifdef REALM_DEBUG void enable_debug(bool enable) @@ -703,10 +713,10 @@ class SlabAlloc : public Allocator { /// corrupted, or if the specified encryption key is incorrect. This /// function will not detect all forms of corruption, though. /// Returns the top_ref for the latest commit. - ref_type validate_header(const char* data, size_t len, const std::string& path); - ref_type validate_header(const Header* header, const StreamingFooter* footer, size_t size, - const std::string& path, bool is_encrypted = false); - void throw_header_exception(std::string msg, const Header& header, const std::string& path); + static ref_type validate_header(const char* data, size_t len, const std::string& path); + static ref_type validate_header(const Header* header, const StreamingFooter* footer, size_t size, + const std::string& path, bool is_encrypted = false); + static void throw_header_exception(std::string msg, const Header& header, const std::string& path); static bool is_file_on_streaming_form(const Header& header); /// Read the top_ref from the given buffer and set m_file_on_streaming_form diff --git a/src/realm/db.cpp b/src/realm/db.cpp index f982be15652..ac2967bfdb9 100644 --- a/src/realm/db.cpp +++ b/src/realm/db.cpp @@ -898,7 +898,7 @@ std::string DBOptions::sys_tmp_dir = getenv("TMPDIR") ? getenv("TMPDIR") : ""; // initializing process crashes and leaves the shared memory in an // undefined state. -void DB::open(const std::string& path, bool no_create_file, const DBOptions& options) +void DB::open(const std::string& path, const DBOptions& options) { // Exception safety: Since do_open() is called from constructors, if it // throws, it must leave the file closed. @@ -1144,10 +1144,11 @@ void DB::open(const std::string& path, bool no_create_file, const DBOptions& opt cfg.read_only = false; cfg.skip_validate = !begin_new_session; cfg.disable_sync = options.durability == Durability::MemOnly || options.durability == Durability::Unsafe; + cfg.clear_file_on_error = options.clear_on_invalid_file; // only the session initiator is allowed to create the database, all other // must assume that it already exists. - cfg.no_create = (begin_new_session ? no_create_file : true); + cfg.no_create = (begin_new_session ? options.no_create : true); // if we're opening a MemOnly file that isn't already opened by // someone else then it's a file which should have been deleted on @@ -1499,8 +1500,7 @@ void DB::open(Replication& repl, const std::string& file, const DBOptions& optio set_replication(&repl); - bool no_create = false; - open(file, no_create, options); // Throws + open(file, options); // Throws } class DBLogger : public Logger { @@ -1532,7 +1532,7 @@ void DB::set_logger(const std::shared_ptr& logger) noexcept m_logger = std::make_shared(logger, m_log_id); } -void DB::open(Replication& repl, const DBOptions options) +void DB::open(Replication& repl, const DBOptions& options) { REALM_ASSERT(!is_attached()); repl.initialize(*this); // Throws @@ -2808,10 +2808,10 @@ inline DB::DB(Private, const DBOptions& options) } } -DBRef DB::create(const std::string& file, bool no_create, const DBOptions& options) NO_THREAD_SAFETY_ANALYSIS +DBRef DB::create(const std::string& file, const DBOptions& options) NO_THREAD_SAFETY_ANALYSIS { DBRef retval = std::make_shared(Private(), options); - retval->open(file, no_create, options); + retval->open(file, options); return retval; } diff --git a/src/realm/db.hpp b/src/realm/db.hpp index ac0b555b32e..af8fc23633c 100644 --- a/src/realm/db.hpp +++ b/src/realm/db.hpp @@ -128,7 +128,7 @@ class DB : public std::enable_shared_from_this { // calling DB::close(), but after that no new association can be established. To reopen the // file (or another file), a new DB object is needed. The specified Replication instance, if // any, must remain in existence for as long as the DB. - static DBRef create(const std::string& file, bool no_create = false, const DBOptions& options = DBOptions()); + static DBRef create(const std::string& file, const DBOptions& options = DBOptions()); static DBRef create(Replication& repl, const std::string& file, const DBOptions& options = DBOptions()); static DBRef create(std::unique_ptr repl, const std::string& file, const DBOptions& options = DBOptions()); @@ -534,10 +534,6 @@ class DB : public std::enable_shared_from_this { /// /// \param file Filesystem path to a Realm database file. /// - /// \param no_create If the database file does not already exist, it will be - /// created (unless this is set to true.) When multiple threads are involved, - /// it is safe to let the first thread, that gets to it, create the file. - /// /// \param options See DBOptions for details of each option. /// Sensible defaults are provided if this parameter is left out. /// @@ -553,13 +549,12 @@ class DB : public std::enable_shared_from_this { /// \throw UnsupportedFileFormatVersion if the file format version or /// history schema version is one which this version of Realm does not know /// how to migrate from. - void open(const std::string& file, bool no_create = false, const DBOptions& options = DBOptions()) - REQUIRES(!m_mutex); + void open(const std::string& file, const DBOptions& options = DBOptions()) REQUIRES(!m_mutex); void open(BinaryData, bool take_ownership = true) REQUIRES(!m_mutex); void open(Replication&, const std::string& file, const DBOptions& options = DBOptions()) REQUIRES(!m_mutex); - void open(Replication& repl, const DBOptions options = DBOptions()) REQUIRES(!m_mutex); + void open(Replication& repl, const DBOptions& options = DBOptions()) REQUIRES(!m_mutex); - void do_open(const std::string& file, bool no_create, const DBOptions& options); + void do_open(const std::string& file, const DBOptions& options); Replication* const* get_repl() const noexcept { diff --git a/src/realm/db_options.hpp b/src/realm/db_options.hpp index 781e561a96a..a11eded2352 100644 --- a/src/realm/db_options.hpp +++ b/src/realm/db_options.hpp @@ -88,6 +88,9 @@ struct DBOptions { /// Disable automatic backup at file format upgrade by setting to false bool backup_at_file_format_change = true; + /// Disable creating new files if the DB to open does not already exist. + bool no_create = false; + /// List of versions we can upgrade from BackupHandler::VersionList accepted_versions = BackupHandler::accepted_versions_; @@ -99,6 +102,10 @@ struct DBOptions { /// a performance impact. bool enable_async_writes = false; + /// If set, opening a file which is not a Realm file or cannot be decrypted + /// will clear and reinitialize the file. + bool clear_on_invalid_file = false; + /// sys_tmp_dir will be used if the temp_dir is empty when creating DBOptions. /// It must be writable and allowed to create pipe/fifo file on it. /// set_sys_tmp_dir is not a thread-safe call and it is only supposed to be called once diff --git a/src/realm/object-store/impl/realm_coordinator.cpp b/src/realm/object-store/impl/realm_coordinator.cpp index 552a5cee04d..00f0a191655 100644 --- a/src/realm/object-store/impl/realm_coordinator.cpp +++ b/src/realm/object-store/impl/realm_coordinator.cpp @@ -479,6 +479,7 @@ bool RealmCoordinator::open_db() } options.encryption_key = m_config.encryption_key.data(); options.allow_file_format_upgrade = !m_config.disable_format_upgrade && !schema_mode_reset_file; + options.clear_on_invalid_file = m_config.clear_on_invalid_file; if (history) { options.backup_at_file_format_change = m_config.backup_at_file_format_change; #ifdef __EMSCRIPTEN__ @@ -496,7 +497,8 @@ bool RealmCoordinator::open_db() #endif } else { - m_db = DB::create(m_config.path, true, options); + options.no_create = true; + m_db = DB::create(m_config.path, options); } } catch (realm::FileFormatUpgradeRequired const&) { diff --git a/src/realm/object-store/shared_realm.hpp b/src/realm/object-store/shared_realm.hpp index 5031b0f19d4..5e98c96149f 100644 --- a/src/realm/object-store/shared_realm.hpp +++ b/src/realm/object-store/shared_realm.hpp @@ -184,6 +184,13 @@ struct RealmConfig { // everything can be done deterministically on one thread, and // speeds up tests that don't need notifications. bool automatic_change_notifications = true; + + // For internal use and should not be exposed by SDKs. + // + // If the file is invalid or can't be decrypted with the given encryption + // key, clear it and reinitialize it as a new file. This is used for the + // sync metadata realm which is automatically deleted if it can't be used. + bool clear_on_invalid_file = false; }; class Realm : public std::enable_shared_from_this { diff --git a/src/realm/object-store/sync/impl/sync_metadata.cpp b/src/realm/object-store/sync/impl/sync_metadata.cpp index 81cafc2e97d..af75fc0a55b 100644 --- a/src/realm/object-store/sync/impl/sync_metadata.cpp +++ b/src/realm/object-store/sync/impl/sync_metadata.cpp @@ -527,12 +527,7 @@ std::shared_ptr SyncMetadataManager::try_get_realm() const std::shared_ptr SyncMetadataManager::open_realm(bool should_encrypt, bool caller_supplied_key) { if (caller_supplied_key || !should_encrypt || !REALM_PLATFORM_APPLE) { - if (auto realm = try_get_realm()) - return realm; - - // Encryption key changed, so delete the existing metadata realm and - // recreate it - util::File::remove(m_metadata_config.path); + m_metadata_config.clear_on_invalid_file = true; return get_realm(); } @@ -560,8 +555,8 @@ std::shared_ptr SyncMetadataManager::open_realm(bool should_encrypt, bool return realm; // We weren't able to open the existing file with either the stored key - // or no key, so just delete it. - util::File::remove(m_metadata_config.path); + // or no key, so just recreate it + m_metadata_config.clear_on_invalid_file = true; } // We now have no metadata Realm. If we don't have an existing stored key, diff --git a/test/benchmark-common-tasks/main.cpp b/test/benchmark-common-tasks/main.cpp index b4872dcf6e3..234b4ed4a52 100644 --- a/test/benchmark-common-tasks/main.cpp +++ b/test/benchmark-common-tasks/main.cpp @@ -2021,7 +2021,7 @@ struct BenchmarkNonInitiatorOpen : Benchmark { DBRef do_open() { - return DB::create(*path, false, DBOptions(m_durability, m_encryption_key)); + return DB::create(*path, DBOptions(m_durability, m_encryption_key)); } void before_all(DBRef) @@ -2502,7 +2502,7 @@ void run_benchmark(BenchmarkResults& results, bool force_full = false) realm::test_util::DBTestPathGuard realm_path( test_util::get_test_path("benchmark_common_tasks_" + ident, ".realm")); DBRef group; - group = DB::create(realm_path, false, DBOptions(level, key)); + group = DB::create(realm_path, DBOptions(level, key)); benchmark.before_all(group); // Warm-up and initial measuring: diff --git a/test/test_client_reset.cpp b/test/test_client_reset.cpp index c6beed04601..077fa5626d6 100644 --- a/test/test_client_reset.cpp +++ b/test/test_client_reset.cpp @@ -975,7 +975,8 @@ TEST(ClientReset_NoChanges) db->write_copy(path_backup, nullptr); DBOptions options; options.is_immutable = true; - auto backup_db = DB::create(path_backup, true, options); + options.no_create = true; + auto backup_db = DB::create(path_backup, options); const ClientResyncMode modes[] = {ClientResyncMode::Recover, ClientResyncMode::DiscardLocal, ClientResyncMode::RecoverOrDiscard}; diff --git a/test/test_lang_bind_helper.cpp b/test/test_lang_bind_helper.cpp index 75d4a7e2a68..d8fdb175a4c 100644 --- a/test/test_lang_bind_helper.cpp +++ b/test/test_lang_bind_helper.cpp @@ -5195,7 +5195,7 @@ TEST(LangBindHelper_SessionHistoryConsistency) // session participants must still agree { // No history - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); // Out-of-Realm history std::unique_ptr hist = realm::make_in_realm_history(); @@ -5224,7 +5224,7 @@ TEST(LangBindHelper_InRealmHistory_Upgrade) SHARED_GROUP_TEST_PATH(path_2); { // No history - DBRef sg = DB::create(path_2, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path_2, DBOptions(crypt_key())); WriteTransaction wt(sg); wt.commit(); } @@ -5249,7 +5249,7 @@ TEST(LangBindHelper_InRealmHistory_Downgrade) } { // No history - CHECK_THROW(DB::create(path, false, DBOptions(crypt_key())), IncompatibleHistories); + CHECK_THROW(DB::create(path, DBOptions(crypt_key())), IncompatibleHistories); } } diff --git a/test/test_shared.cpp b/test/test_shared.cpp index 91a14ab5ff2..4a3cab6cabc 100644 --- a/test/test_shared.cpp +++ b/test/test_shared.cpp @@ -218,7 +218,9 @@ void writer(DBRef sg, uint64_t id) void killer(TestContext& test_context, int pid, std::string path, int id) { { - DBRef sg = DB::create(path, true, DBOptions(crypt_key())); + DBOptions options(crypt_key()); + options.no_create = true; + DBRef sg = DB::create(path, options); bool done = false; do { sched_yield(); @@ -254,7 +256,9 @@ void killer(TestContext& test_context, int pid, std::string path, int id) CHECK_EQUAL(0, child_exit_status); { // Verify that we surely did kill the process before it could do all it's commits. - DBRef sg = DB::create(path, true); + DBOptions options; + options.no_create = true; + DBRef sg = DB::create(path, options); ReadTransaction rt(sg); rt.get_group().verify(); auto t1 = rt.get_table("test"); @@ -285,7 +289,7 @@ TEST_IF(Shared_PipelinedWritesWithKills, false) CHECK(RobustMutex::is_robust_on_this_platform); const int num_processes = 50; SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); { // Create table entries WriteTransaction wt(sg); @@ -351,7 +355,7 @@ ONLY(Shared_DiskSpace) std::string path = "test.realm"; - SharedGroup sg(path, false, DBOptions("1234567890123456789012345678901123456789012345678901234567890123")); + SharedGroup sg(path, DBOptions("1234567890123456789012345678901123456789012345678901234567890123")); // SharedGroup sg(path, false, SharedGroupOptions(nullptr)); int seed = time(0); @@ -641,7 +645,7 @@ TEST(Shared_Initial) auto key_str = crypt_key(); { // Create a new shared db - DBRef sg = DB::create(path, false, DBOptions(key_str)); + DBRef sg = DB::create(path, DBOptions(key_str)); // Verify that new group is empty { @@ -661,8 +665,7 @@ TEST(Shared_InitialMem) SHARED_GROUP_TEST_PATH(path); { // Create a new shared db - bool no_create = false; - DBRef sg = DB::create(path, no_create, DBOptions(DBOptions::Durability::MemOnly)); + DBRef sg = DB::create(path, DBOptions(DBOptions::Durability::MemOnly)); // Verify that new group is empty { @@ -687,10 +690,7 @@ TEST(Shared_InitialMem_StaleFile) // file open // Create a MemOnly realm at the path so that a lock file gets initialized - { - bool no_create = false; - DBRef r = DB::create(path, no_create, DBOptions(DBOptions::Durability::MemOnly)); - } + DB::create(path, DBOptions(DBOptions::Durability::MemOnly)); CHECK(!File::exists(path)); CHECK(File::exists(path.get_lock_path())); @@ -706,8 +706,7 @@ TEST(Shared_InitialMem_StaleFile) // Verify that we can still open the path as a MemOnly SharedGroup and that // it's cleaned up afterwards { - bool no_create = false; - DBRef sg = DB::create(path, no_create, DBOptions(DBOptions::Durability::MemOnly)); + DBRef db = DB::create(path, DBOptions(DBOptions::Durability::MemOnly)); CHECK(File::exists(path)); } CHECK(!File::exists(path)); @@ -720,11 +719,11 @@ TEST(Shared_Initial2) SHARED_GROUP_TEST_PATH(path); { // Create a new shared db - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); { // Open the same db again (in empty state) - DBRef sg2 = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg2 = DB::create(path, DBOptions(crypt_key())); // Verify that new group is empty { @@ -765,12 +764,11 @@ TEST(Shared_Initial2_Mem) SHARED_GROUP_TEST_PATH(path); { // Create a new shared db - bool no_create = false; - DBRef sg = DB::create(path, no_create, DBOptions(DBOptions::Durability::MemOnly)); + DBRef sg = DB::create(path, DBOptions(DBOptions::Durability::MemOnly)); { // Open the same db again (in empty state) - DBRef sg2 = DB::create(path, no_create, DBOptions(DBOptions::Durability::MemOnly)); + DBRef sg2 = DB::create(path, DBOptions(DBOptions::Durability::MemOnly)); // Verify that new group is empty { @@ -810,7 +808,7 @@ TEST(Shared_1) SHARED_GROUP_TEST_PATH(path); { // Create a new shared db - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); Timestamp first_timestamp_value{1, 1}; std::vector cols; @@ -911,7 +909,7 @@ TEST(Shared_try_begin_write) { SHARED_GROUP_TEST_PATH(path); // Create a new shared db - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); std::mutex thread_obtains_write_lock; std::condition_variable cv; std::mutex cv_lock; @@ -990,7 +988,7 @@ TEST(Shared_Rollback) SHARED_GROUP_TEST_PATH(path); { // Create a new shared db - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); std::vector cols; // Create first table in group (but rollback) @@ -1062,7 +1060,7 @@ TEST(Shared_Writes) SHARED_GROUP_TEST_PATH(path); { // Create a new shared db - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); std::vector cols; // Create first table in group @@ -1139,8 +1137,7 @@ TEST(Shared_ManyReaders) SHARED_GROUP_TEST_PATH(path); - bool no_create = false; - auto root_sg = DB::create(path, no_create, DBOptions(DBOptions::Durability::MemOnly)); + auto root_sg = DB::create(path, DBOptions(DBOptions::Durability::MemOnly)); // Add two tables { @@ -1162,7 +1159,7 @@ TEST(Shared_ManyReaders) // Create 8*N shared group accessors for (int i = 0; i < 8 * N; ++i) - shared_groups[i] = DB::create(path, no_create, DBOptions(DBOptions::Durability::MemOnly)); + shared_groups[i] = DB::create(path, DBOptions(DBOptions::Durability::MemOnly)); // Initiate 2*N read transactions with progressive changes for (int i = 0; i < 2 * N; ++i) { @@ -1364,7 +1361,7 @@ TEST(Shared_ManyReaders) // Check final state via new shared group { - DBRef sg = DB::create(path, no_create, DBOptions(DBOptions::Durability::MemOnly)); + DBRef sg = DB::create(path, DBOptions(DBOptions::Durability::MemOnly)); ReadTransaction rt(sg); #if !defined(_WIN32) || TEST_DURATION > 0 rt.get_group().verify(); @@ -1411,9 +1408,8 @@ TEST(Many_ConcurrentReaders) DBOptions options; options.logger = std::make_shared(logs); options.logger->set_level_threshold(Logger::Level::all); - constexpr bool no_create = false; for (int i = 0; i < 1000; ++i) { - DBRef sg_r = DB::create(path_str, no_create, options); + DBRef sg_r = DB::create(path_str, options); ReadTransaction rt(sg_r); ConstTableRef t = rt.get_table("table"); auto col_key = t->get_column_key("column"); @@ -1444,7 +1440,7 @@ TEST(Many_ConcurrentReaders) TEST(Shared_WritesSpecialOrder) { SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); const int num_rows = 5; // FIXME: Should be strictly greater than REALM_MAX_BPNODE_SIZE, but that takes too long time. @@ -1531,7 +1527,7 @@ TEST(Shared_WriterThreads) SHARED_GROUP_TEST_PATH(path); { // Create a new shared db - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); const int thread_count = 10; // Create first table in group @@ -1605,7 +1601,7 @@ TEST(Shared_RobustAgainstDeathDuringWrite) REALM_TERMINATE("fork() failed"); if (pid == 0) { // Child - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); WriteTransaction wt(sg); wt.get_group().verify(); wt.get_or_add_table("alpha"); @@ -1626,7 +1622,7 @@ TEST(Shared_RobustAgainstDeathDuringWrite) // Check that we can continue without dead-locking { - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); WriteTransaction wt(sg); wt.get_group().verify(); TableRef table = wt.get_or_add_table("beta"); @@ -1640,7 +1636,7 @@ TEST(Shared_RobustAgainstDeathDuringWrite) } { - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); ReadTransaction rt(sg); rt.get_group().verify(); CHECK(!rt.has_table("alpha")); @@ -1669,7 +1665,7 @@ TEST(Shared_SpaceOveruse) // Many transactions SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); // Do a lot of sequential transactions for (int i = 0; i != n_outer; ++i) { @@ -1712,7 +1708,7 @@ TEST(Shared_Notifications) { // Create a new shared db SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); TransactionRef tr1 = sg->start_read(); // No other instance have changed db since last transaction @@ -1720,7 +1716,7 @@ TEST(Shared_Notifications) { // Open the same db again (in empty state) - DBRef sg2 = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg2 = DB::create(path, DBOptions(crypt_key())); // Verify that new group is empty { @@ -1778,7 +1774,7 @@ TEST(Shared_FromSerialized) } // Open same file as shared group - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); // Verify that contents is there when shared { @@ -1798,7 +1794,7 @@ TEST(Shared_FromSerialized) TEST_IF(Shared_StringIndexBug1, TEST_DURATION >= 1) { SHARED_GROUP_TEST_PATH(path); - DBRef db = DB::create(path, false, DBOptions(crypt_key())); + DBRef db = DB::create(path, DBOptions(crypt_key())); { auto tr = db->start_write(); @@ -1824,7 +1820,7 @@ TEST_IF(Shared_StringIndexBug1, TEST_DURATION >= 1) TEST(Shared_StringIndexBug2) { SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); { WriteTransaction wt(sg); @@ -1856,7 +1852,7 @@ void rand_str(Random& random, char* res, size_t len) TEST(Shared_StringIndexBug3) { SHARED_GROUP_TEST_PATH(path); - DBRef db = DB::create(path, false, DBOptions(crypt_key())); + DBRef db = DB::create(path, DBOptions(crypt_key())); ColKey col; { auto tr = db->start_write(); @@ -1904,7 +1900,7 @@ TEST(Shared_ClearColumnWithBasicArrayRootLeaf) { SHARED_GROUP_TEST_PATH(path); { - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); WriteTransaction wt(sg); TableRef test = wt.add_table("Test"); auto col = test->add_column(type_Double, "foo"); @@ -1913,7 +1909,7 @@ TEST(Shared_ClearColumnWithBasicArrayRootLeaf) wt.commit(); } { - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); ReadTransaction rt(sg); ConstTableRef test = rt.get_table("Test"); auto col = test->get_column_key("foo"); @@ -1925,7 +1921,7 @@ TEST(Shared_ClearColumnWithLinksToSelf) { // Reproduction of issue found by fuzzer SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); { WriteTransaction wt(sg); TableRef test = wt.add_table("Test"); @@ -2045,7 +2041,7 @@ TEST(Shared_WaitForChange) for (int j = 0; j < num_threads; j++) shared_state[j] = 0; SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false); + DBRef sg = DB::create(path); auto waiter = [&](DBRef db, int i) { TransactionRef tr; @@ -2174,8 +2170,8 @@ TEST(Shared_MultipleSharersOfStreamingFormat) } { // See if we can handle overlapped accesses through multiple shared groups - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); - DBRef sg2 = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); + DBRef sg2 = DB::create(path, DBOptions(crypt_key())); { ReadTransaction rt(sg); rt.get_group().verify(); @@ -2204,9 +2200,9 @@ TEST(Shared_MultipleSharersOfStreamingFormat) TEST(Shared_EncryptionKeyCheck) { SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key(true))); - CHECK_THROW(DB::create(path, false, DBOptions()), InvalidDatabase); - DBRef sg3 = DB::create(path, false, DBOptions(crypt_key(true))); + DBRef sg = DB::create(path, DBOptions(crypt_key(true))); + CHECK_THROW(DB::create(path, DBOptions()), InvalidDatabase); + DBRef sg3 = DB::create(path, DBOptions(crypt_key(true))); } // opposite - if opened unencrypted, attempt to share it encrypted @@ -2214,9 +2210,9 @@ TEST(Shared_EncryptionKeyCheck) TEST(Shared_EncryptionKeyCheck_2) { SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions()); - CHECK_THROW(DB::create(path, false, DBOptions(crypt_key(true))), InvalidDatabase); - DBRef sg3 = DB::create(path, false, DBOptions()); + DBRef sg = DB::create(path, DBOptions()); + CHECK_THROW(DB::create(path, DBOptions(crypt_key(true))), InvalidDatabase); + DBRef sg3 = DB::create(path, DBOptions()); CHECK(sg3); } @@ -2228,9 +2224,9 @@ TEST(Shared_EncryptionKeyCheck_3) char second_key[64]; memcpy(second_key, first_key, 64); second_key[3] = ~second_key[3]; - DBRef sg = DB::create(path, false, DBOptions(first_key)); - CHECK_THROW(DB::create(path, false, DBOptions(second_key)), InvalidDatabase); - DBRef sg3 = DB::create(path, false, DBOptions(first_key)); + DBRef sg = DB::create(path, DBOptions(first_key)); + CHECK_THROW(DB::create(path, DBOptions(second_key)), InvalidDatabase); + DBRef sg3 = DB::create(path, DBOptions(first_key)); } TEST(Shared_EncryptionPageReadFailure) @@ -2238,7 +2234,7 @@ TEST(Shared_EncryptionPageReadFailure) SHARED_GROUP_TEST_PATH(path); constexpr size_t num_objects = 4096; { - DBRef sg = DB::create(path, false, DBOptions(crypt_key(true))); + DBRef sg = DB::create(path, DBOptions(crypt_key(true))); WriteTransaction wt(sg); TableRef table = wt.get_group().add_table_with_primary_key("foo", type_ObjectId, "pk"); auto str_col = table->add_column(type_String, "string"); @@ -2261,7 +2257,7 @@ TEST(Shared_EncryptionPageReadFailure) { bool did_throw = false; try { - DBRef sg = DB::create(path, false, DBOptions(crypt_key(true))); + DBRef sg = DB::create(path, DBOptions(crypt_key(true))); WriteTransaction wt(sg); TableRef table = wt.get_group().get_table("foo"); CHECK_EQUAL(table->size(), num_objects); @@ -2308,7 +2304,7 @@ TEST(Shared_VersionCount) TEST(Shared_MultipleRollbacks) { SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); TransactionRef wt = sg->start_write(); wt->rollback(); wt->rollback(); @@ -2318,7 +2314,7 @@ TEST(Shared_MultipleRollbacks) TEST(Shared_MultipleEndReads) { SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); TransactionRef reader = sg->start_read(); reader->end_read(); reader->end_read(); @@ -2330,7 +2326,7 @@ TEST(Shared_ReserveDiskSpace) { SHARED_GROUP_TEST_PATH(path); { - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); size_t orig_file_size = size_t(File(path).get_size()); // Check that reserve() does not change the file size if the @@ -2413,7 +2409,7 @@ TEST(Shared_MovingSearchIndex) // adjusted when columns are inserted or removed at a lower column_index. SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); // Create an int column, regular string column, and an enumeration strings // column, and equip them with search indexes. @@ -2534,12 +2530,11 @@ TEST(Shared_SessionDurabilityConsistency) SHARED_GROUP_TEST_PATH(path); { - bool no_create = false; DBOptions::Durability durability_1 = DBOptions::Durability::Full; - DBRef sg = DB::create(path, no_create, DBOptions(durability_1)); + DBRef sg = DB::create(path, DBOptions(durability_1)); DBOptions::Durability durability_2 = DBOptions::Durability::MemOnly; - CHECK_RUNTIME_ERROR(DB::create(path, no_create, DBOptions(durability_2)), ErrorCodes::IncompatibleSession); + CHECK_RUNTIME_ERROR(DB::create(path, DBOptions(durability_2)), ErrorCodes::IncompatibleSession); } } @@ -2748,7 +2743,7 @@ TEST_IF(Shared_encrypted_pin_and_write, false) SHARED_GROUP_TEST_PATH(path); { // initial table structure setup on main thread - DBRef sg = DB::create(path, false, DBOptions(crypt_key(true))); + DBRef sg = DB::create(path, DBOptions(crypt_key(true))); WriteTransaction wt(sg); Group& group = wt.get_group(); TableRef t = group.add_table("table"); @@ -2759,12 +2754,12 @@ TEST_IF(Shared_encrypted_pin_and_write, false) wt.commit(); } - DBRef sg_reader = DB::create(path, false, DBOptions(crypt_key(true))); + DBRef sg_reader = DB::create(path, DBOptions(crypt_key(true))); ReadTransaction rt(sg_reader); // hold first version auto do_many_writes = [&]() { - DBRef sg = DB::create(path, false, DBOptions(crypt_key(true))); + DBRef sg = DB::create(path, DBOptions(crypt_key(true))); const size_t base_size = 100000; std::string base(base_size, 'a'); // write many transactions to grow the file @@ -2806,7 +2801,7 @@ NONCONCURRENT_TEST(Shared_BigAllocations) { size_t string_length = 64 * 1024; SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); std::string long_string(string_length, 'a'); { WriteTransaction wt(sg); @@ -2844,7 +2839,7 @@ TEST_IF(Shared_CompactEncrypt, REALM_ENABLE_ENCRYPTION) const char* key1 = "KdrL2ieWyspILXIPetpkLD6rQYKhYnS6lvGsgk4qsJAMr1adQnKsYo3oTEYJDIfa"; const char* key2 = "ti6rOKviXrwxSGMPVk35Dp9Q4eku8Cu8YTtnnZKAejOTNIEv7TvXrYdjOPSNexMR"; { - auto db = DB::create(path, false, DBOptions(key1)); + auto db = DB::create(path, DBOptions(key1)); auto tr = db->start_write(); TableRef t = tr->add_table("table"); auto col = t->add_column(type_String, "Strings"); @@ -2874,7 +2869,9 @@ TEST_IF(Shared_CompactEncrypt, REALM_ENABLE_ENCRYPTION) } } { - auto db = DB::create(path, true, DBOptions()); + DBOptions options; + options.no_create = true; + auto db = DB::create(path, options); { auto rt = db->start_read(); CHECK(rt->has_table("table")); @@ -2890,7 +2887,7 @@ NONCONCURRENT_TEST(Shared_BigAllocationsMinimized) size_t string_length = 4 * 1024; SHARED_GROUP_TEST_PATH(path); std::string long_string(string_length, 'a'); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); { { WriteTransaction wt(sg); @@ -2927,7 +2924,7 @@ NONCONCURRENT_TEST(Shared_BigAllocationsMinimized) NONCONCURRENT_TEST(Shared_TopSizeNotEqualNine) { SHARED_GROUP_TEST_PATH(path); - DBRef sg = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg = DB::create(path, DBOptions(crypt_key())); { TransactionRef writer = sg->start_write(); @@ -2938,13 +2935,13 @@ NONCONCURRENT_TEST(Shared_TopSizeNotEqualNine) writer->commit(); } REALM_ASSERT_RELEASE(sg->compact()); - DBRef sg2 = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg2 = DB::create(path, DBOptions(crypt_key())); { TransactionRef writer = sg2->start_write(); writer->commit(); } TransactionRef reader2 = sg2->start_read(); - DBRef sg3 = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg3 = DB::create(path, DBOptions(crypt_key())); TransactionRef reader3 = sg3->start_read(); TransactionRef reader = sg->start_read(); } @@ -2955,7 +2952,7 @@ NONCONCURRENT_TEST(Shared_TopSizeNotEqualNine) TEST(Shared_Bptree_insert_failure) { SHARED_GROUP_TEST_PATH(path); - DBRef sg_w = DB::create(path, false, DBOptions(crypt_key())); + DBRef sg_w = DB::create(path, DBOptions(crypt_key())); TransactionRef writer = sg_w->start_write(); auto tk = writer->add_table("")->get_key(); @@ -2968,7 +2965,7 @@ TEST(Shared_Bptree_insert_failure) { // This intervening sg can do the same operation as the one doing compact, // but without failing: - DB sg2(path, false, DBOptions(crypt_key())); + DB sg2(path, DBOptions(crypt_key())); Group& g2 = const_cast(sg2.begin_write()); g2.get_table(tk)->add_empty_row(396); } @@ -3011,10 +3008,9 @@ TEST(Shared_LockFileInitSpinsOnZeroSize) { SHARED_GROUP_TEST_PATH(path); - bool no_create = false; DBOptions options; options.encryption_key = crypt_key(); - DBRef sg = DB::create(path, no_create, options); + DBRef sg = DB::create(path, options); sg->close(); CHECK(File::exists(path)); @@ -3046,7 +3042,7 @@ TEST(Shared_LockFileInitSpinsOnZeroSize) wait_for(1, mutex, test_stage); // we'll spin here without error until we can obtain the exclusive lock and initialise it ourselves - sg = DB::create(path, no_create, options); + sg = DB::create(path, options); CHECK(sg->is_attached()); sg->close(); @@ -3058,10 +3054,9 @@ TEST(Shared_LockFileSpinsOnInitComplete) { SHARED_GROUP_TEST_PATH(path); - bool no_create = false; DBOptions options; options.encryption_key = crypt_key(); - DBRef sg = DB::create(path, no_create, options); + DBRef sg = DB::create(path, options); sg->close(); CHECK(File::exists(path)); @@ -3093,7 +3088,7 @@ TEST(Shared_LockFileSpinsOnInitComplete) wait_for(1, mutex, test_stage); // we'll spin here without error until we can obtain the exclusive lock and initialise it ourselves - sg = DB::create(path, no_create, options); + sg = DB::create(path, options); CHECK(sg->is_attached()); sg->close(); @@ -3109,10 +3104,9 @@ TEST(Shared_LockFileOfWrongSizeThrows) SHARED_GROUP_TEST_PATH(path); - bool no_create = false; DBOptions options; options.encryption_key = crypt_key(); - DBRef sg = DB::create(path, no_create, options); + DBRef sg = DB::create(path, options); sg->close(); CHECK(File::exists(path)); @@ -3157,7 +3151,7 @@ TEST(Shared_LockFileOfWrongSizeThrows) // we expect to throw if init_complete = 1 but the file is not the expected size (< sizeof(SharedInfo)) // we go through 10 retry attempts before throwing - CHECK_THROW(DB::create(path, no_create, options), IncompatibleLockFile); + CHECK_THROW(DB::create(path, options), IncompatibleLockFile); CHECK(!sg->is_attached()); mutex.lock(); @@ -3172,10 +3166,9 @@ TEST(Shared_LockFileOfWrongVersionThrows) { SHARED_GROUP_TEST_PATH(path); - bool no_create = false; DBOptions options; options.encryption_key = crypt_key(); - DBRef sg = DB::create(path, no_create, options); + DBRef sg = DB::create(path, options); CHECK(File::exists(path)); CHECK(File::exists(path.get_lock_path())); @@ -3212,7 +3205,7 @@ TEST(Shared_LockFileOfWrongVersionThrows) sg->close(); // we expect to throw if info->shared_info_version != g_shared_info_version - CHECK_THROW(DB::create(path, no_create, options), IncompatibleLockFile); + CHECK_THROW(DB::create(path, options), IncompatibleLockFile); CHECK(!sg->is_attached()); mutex.lock(); @@ -3227,10 +3220,9 @@ TEST(Shared_LockFileOfWrongMutexSizeThrows) { SHARED_GROUP_TEST_PATH(path); - bool no_create = false; DBOptions options; options.encryption_key = crypt_key(); - DBRef sg = DB::create(path, no_create, options); + DBRef sg = DB::create(path, options); CHECK(File::exists(path)); CHECK(File::exists(path.get_lock_path())); @@ -3266,7 +3258,7 @@ TEST(Shared_LockFileOfWrongMutexSizeThrows) sg->close(); // we expect to throw if the mutex size is incorrect - CHECK_THROW(DB::create(path, no_create, options), IncompatibleLockFile); + CHECK_THROW(DB::create(path, options), IncompatibleLockFile); CHECK(!sg->is_attached()); mutex.lock(); @@ -3281,10 +3273,9 @@ TEST(Shared_LockFileOfWrongCondvarSizeThrows) { SHARED_GROUP_TEST_PATH(path); - bool no_create = false; DBOptions options; options.encryption_key = crypt_key(); - DBRef sg = DB::create(path, no_create, options); + DBRef sg = DB::create(path, options); CHECK(File::exists(path)); CHECK(File::exists(path.get_lock_path())); @@ -3319,7 +3310,7 @@ TEST(Shared_LockFileOfWrongCondvarSizeThrows) sg->close(); // we expect to throw if the condvar size is incorrect - CHECK_THROW(DB::create(path, no_create, options), IncompatibleLockFile); + CHECK_THROW(DB::create(path, options), IncompatibleLockFile); CHECK(!sg->is_attached()); mutex.lock(); @@ -3430,8 +3421,8 @@ TEST_IF(Shared_DecryptExisting, REALM_ENABLE_ENCRYPTION) #if 0 // set to 1 to generate the .realm file { File::try_remove(path); - //DB db(path, false, DBOptions(crypt_key(true))); - auto db = DB::create(path, false, DBOptions(crypt_key(true))); + //DB db(path, DBOptions(crypt_key(true))); + auto db = DB::create(path, DBOptions(crypt_key(true))); auto rt = db->start_write(); //Group& group = db.begin_write(); TableRef table = rt->add_table("table"); @@ -3508,7 +3499,9 @@ TEST(Shared_OpenAfterClose) wt = nullptr; db_w->close(); - db_w = DB::create(path, true, DBOptions(key)); + DBOptions options(key); + options.no_create = true; + db_w = DB::create(path, options); wt = db_w->start_write(); wt = nullptr; db_w->close(); @@ -3710,7 +3703,7 @@ TEST_IF(Shared_LinksToSameCluster, REALM_ENABLE_ENCRYPTION) TEST(Shared_GetCommitSize) { SHARED_GROUP_TEST_PATH(path); - DBRef db = DB::create(path, false, DBOptions(crypt_key())); + DBRef db = DB::create(path, DBOptions(crypt_key())); size_t size_before; size_t commit_size; { @@ -3769,7 +3762,7 @@ TEST_IF(Shared_LargeFile, TEST_DURATION > 0 && !REALM_ANDROID) SHARED_GROUP_TEST_PATH(path); DBOptions options; options.durability = DBOptions::Durability::MemOnly; - DBRef db = DB::create(path, false, options); + DBRef db = DB::create(path, options); auto tr = db->start_write(); @@ -3830,7 +3823,7 @@ TEST(Shared_EncryptionBug) DBOptions options; options.encryption_key = crypt_key(true); { - DBRef db = DB::create(path, false, options); + DBRef db = DB::create(path, options); { WriteTransaction wt(db); auto foo = wt.add_table("foo"); @@ -3851,7 +3844,7 @@ TEST(Shared_EncryptionBug) } { - DBRef db = DB::create(path, false, options); + DBRef db = DB::create(path, options); db->start_read()->verify(); } } @@ -4453,4 +4446,113 @@ NONCONCURRENT_TEST_IF(Shared_LockFileConcurrentInit, testing_supports_spawn_proc } } +TEST(Shared_ClearOnError_ReopenValidFile) +{ + SHARED_GROUP_TEST_PATH(path); + DBOptions options(crypt_key()); + options.clear_on_invalid_file = true; + + { + auto db = DB::create(path, options); + WriteTransaction wt(db); + wt.add_table("table"); + wt.commit(); + } + + { + // The file should not have been cleared + auto db = DB::create(path, options); + CHECK(db->start_read()->get_table("table")); + } +} + +TEST(Shared_ClearOnError_ResetInvalidFile) +{ + SHARED_GROUP_TEST_PATH(path); + DBOptions options; + options.clear_on_invalid_file = true; + + { + auto db = DB::create(path, options); + WriteTransaction wt(db); + wt.add_table("table"); + wt.commit(); + } + + { + // Overwrite the first byte of the mnemonic so that this isn't a valid file + util::File file(path, File::mode_Update); + file.seek(8); + file.write("\0", 1); + } + + { + // The file should have been cleared + auto db = DB::create(path, options); + CHECK_NOT(db->start_read()->get_table("table")); + } +} + +#if REALM_ENABLE_ENCRYPTION +TEST(Shared_ClearOnError_ChangeEncryptionKey) +{ + auto key_1 = "1234567890123456789012345678901123456789012345678901234567890123"; + auto key_2 = "2234567890123456789012345678901123456789012345678901234567890123"; + + SHARED_GROUP_TEST_PATH(path); + DBOptions options; + options.clear_on_invalid_file = true; + options.encryption_key = key_1; + + { + auto db = DB::create(path, options); + WriteTransaction wt(db); + wt.add_table("table"); + wt.commit(); + } + + { // change from first key to second + options.encryption_key = key_2; + auto db = DB::create(path, options); + WriteTransaction wt(db); + CHECK_NOT(wt.get_table("table")); + wt.add_table("table 2"); + wt.commit(); + } + + { // change from encrypted to unencrypted + options.encryption_key = nullptr; + auto db = DB::create(path, options); + WriteTransaction wt(db); + CHECK_NOT(wt.get_table("table 2")); + wt.add_table("table 3"); + wt.commit(); + } + + { // change from unencrypted to encrypted + options.encryption_key = key_1; + auto db = DB::create(path, options); + WriteTransaction wt(db); + CHECK_NOT(wt.get_table("table 3")); + wt.add_table("table 4"); + wt.commit(); + } + + { // sanity check that reopening encrypted with the same key works + auto db = DB::create(path, options); + CHECK(db->start_read()->get_table("table 4")); + } +} + +TEST(Shared_ClearOnError_CannotClearWhileFileIsOpen) +{ + SHARED_GROUP_TEST_PATH(path); + DBOptions options; + options.clear_on_invalid_file = true; + auto db = DB::create(make_in_realm_history(), path, options); + options.encryption_key = crypt_key(true); + CHECK_THROW(DB::create(make_in_realm_history(), path, options), InvalidDatabase); +} +#endif + #endif // TEST_SHARED diff --git a/test/test_upgrade_database.cpp b/test/test_upgrade_database.cpp index 788438cf7b7..da76d0542bf 100644 --- a/test/test_upgrade_database.cpp +++ b/test/test_upgrade_database.cpp @@ -83,12 +83,13 @@ TEST_IF(Upgrade_DatabaseWithCallback, REALM_MAX_BPNODE_SIZE == 1000) bool did_upgrade = false; DBOptions options; + options.no_create = true; options.upgrade_callback = [&](int old_version, int new_version) { did_upgrade = true; CHECK_EQUAL(old_version, 20); CHECK_EQUAL(new_version, Group::get_current_file_format_version()); }; - DB::create(temp_copy, true, options); + DB::create(temp_copy, options); CHECK(did_upgrade); } @@ -102,10 +103,11 @@ TEST_IF(Upgrade_DatabaseWithCallbackWithException, REALM_MAX_BPNODE_SIZE == 1000 // Callback that throws should revert the upgrade DBOptions options; + options.no_create = true; options.upgrade_callback = [&](int, int) { throw 123; }; - CHECK_THROW(DB::create(temp_copy, true, options), int); + CHECK_THROW(DB::create(temp_copy, options), int); // Callback should be triggered here because the file still needs to be upgraded bool did_upgrade = false; @@ -114,12 +116,12 @@ TEST_IF(Upgrade_DatabaseWithCallbackWithException, REALM_MAX_BPNODE_SIZE == 1000 CHECK_EQUAL(old_version, 20); CHECK_EQUAL(new_version, Group::get_current_file_format_version()); }; - DB::create(temp_copy, true, options); + DB::create(temp_copy, options); CHECK(did_upgrade); // Callback should not be triggered here because the file is already upgraded did_upgrade = false; - DB::create(temp_copy, true, options); + DB::create(temp_copy, options); CHECK_NOT(did_upgrade); }