diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e36c5c91e5..ba7b2d55ee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * Access token refresh for websockets was not updating the location metadata ([#6630](https://github.com/realm/realm-core/issues/6630), since v13.9.3) * Fix several UBSan failures which did not appear to result in functional bugs ([#6649](https://github.com/realm/realm-core/pull/6649)). * Fix an out-of-bounds read in sectioned results when sectioned are removed by modifying all objects in that section to no longer appear in that section ([#6649](https://github.com/realm/realm-core/pull/6649), since v13.12.0) +* Using both synchronous and asynchronous transactions on the same thread or scheduler could hit the assertion failure "!realm.is_in_transaction()" if one of the callbacks for an asynchronous transaction happened to be scheduled during a synchronous transaction ([#6659](https://github.com/realm/realm-core/issues/6659), since v11.8.0) ### Breaking changes * None. diff --git a/src/realm/object-store/shared_realm.cpp b/src/realm/object-store/shared_realm.cpp index c348161863e..1d6974cbb6c 100644 --- a/src/realm/object-store/shared_realm.cpp +++ b/src/realm/object-store/shared_realm.cpp @@ -789,6 +789,12 @@ void Realm::run_writes() // writes as we can't add commits while in that state return; } + if (is_in_transaction()) { + // This is scheduled asynchronously after acquiring the write lock, so + // in that time a synchronous transaction may have been started. If so, + // we'll be re-invoked when that transaction ends. + return; + } CountGuard running_writes(m_is_running_async_writes); int run_limit = 20; // max number of commits without full sync to disk diff --git a/test/object-store/realm.cpp b/test/object-store/realm.cpp index 9c945808c71..aa064738403 100644 --- a/test/object-store/realm.cpp +++ b/test/object-store/realm.cpp @@ -1895,6 +1895,7 @@ TEST_CASE("SharedRealm: async writes") { "an error"); REQUIRE(realm->is_closed()); } +#endif SECTION("exception thrown from async commit completion callback with error handler") { Realm::AsyncHandle h; realm->set_async_error_handler([&](Realm::AsyncHandle handle, std::exception_ptr error) { @@ -1912,6 +1913,7 @@ TEST_CASE("SharedRealm: async writes") { wait_for_done(); verify_persisted_count(1); } +#ifndef _WIN32 SECTION("exception thrown from async commit completion callback without error handler") { realm->begin_transaction(); table->create_object(); @@ -1924,6 +1926,7 @@ TEST_CASE("SharedRealm: async writes") { "an error"); REQUIRE(table->size() == 1); } +#endif if (_impl::SimulatedFailure::is_enabled()) { SECTION("error in the synchronous part of async commit") { @@ -2340,15 +2343,38 @@ TEST_CASE("SharedRealm: async writes") { REQUIRE(table->size() == 6); } + SECTION("async writes which would run inside sync writes are deferred") { + realm->async_begin_transaction([&] { + done = true; + }); + + // Wait for the background thread to hold the write lock (without letting + // the event loop run so that the scheduled task isn't run) + DBOptions options; + options.encryption_key = config.encryption_key.data(); + auto db = DB::create(make_in_realm_history(), config.path, options); + while (db->start_write(true)) + millisleep(1); + + realm->begin_transaction(); + + // Invoke the pending callback + util::EventLoop::main().run_pending(); + // Should not have run the async write block + REQUIRE(done == false); + + // Should run the async write block once the synchronous transaction is done + realm->cancel_transaction(); + REQUIRE(done == false); + util::EventLoop::main().run_pending(); + REQUIRE(done == true); + } + util::EventLoop::main().run_until([&] { return !realm || !realm->has_pending_async_work(); }); -#endif - -#ifdef _WIN32 _impl::RealmCoordinator::clear_all_caches(); -#endif } // Our libuv scheduler currently does not support background threads, so we can // only run this on apple platforms diff --git a/test/object-store/util/event_loop.cpp b/test/object-store/util/event_loop.cpp index 05d45ff2148..6463179e12e 100644 --- a/test/object-store/util/event_loop.cpp +++ b/test/object-store/util/event_loop.cpp @@ -81,6 +81,9 @@ struct EventLoop::Impl { // Schedule execution of the given function on the event loop. void perform(util::UniqueFunction); + // Run the event loop until all currently pending work has been run. + void run_pending(); + ~Impl(); private: @@ -124,6 +127,11 @@ void EventLoop::perform(util::UniqueFunction function) return m_impl->perform(std::move(function)); } +void EventLoop::run_pending() +{ + return m_impl->run_pending(); +} + #if REALM_USE_UV bool EventLoop::has_implementation() @@ -204,6 +212,11 @@ void EventLoop::Impl::perform(util::UniqueFunction f) uv_async_send(&m_perform_work); } +void EventLoop::Impl::run_pending() +{ + uv_run(m_loop, UV_RUN_NOWAIT); +} + #elif REALM_PLATFORM_APPLE bool EventLoop::has_implementation() @@ -254,6 +267,12 @@ void EventLoop::Impl::perform(util::UniqueFunction func) CFRunLoopWakeUp(m_loop.get()); } +void EventLoop::Impl::run_pending() +{ + while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource) + ; +} + #else bool EventLoop::has_implementation() @@ -273,5 +292,9 @@ void EventLoop::Impl::perform(util::UniqueFunction) { printf("WARNING: there is no event loop implementation and nothing is happening.\n"); } +void EventLoop::Impl::run_pending() +{ + printf("WARNING: there is no event loop implementation and nothing is happening.\n"); +} #endif diff --git a/test/object-store/util/event_loop.hpp b/test/object-store/util/event_loop.hpp index 8c191d698ea..a4a2ee31944 100644 --- a/test/object-store/util/event_loop.hpp +++ b/test/object-store/util/event_loop.hpp @@ -39,6 +39,9 @@ struct EventLoop { // Schedule execution of the given function on the event loop. void perform(util::UniqueFunction); + // Run the event loop until all currently pending work has been run. + void run_pending(); + EventLoop(EventLoop&&) = default; EventLoop& operator=(EventLoop&&) = default; ~EventLoop();