From 87f6f74795beb592dd50e5994f2f48bca590eac6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 May 2022 21:03:55 +0900 Subject: [PATCH 1/3] Add failing test coverage of adding a file to a detached beatmap across threads --- .../Database/BeatmapImporterTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index f9c13a8169..b7bfe14402 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -136,6 +136,37 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestAddFileToAsyncImportedBeatmap() + { + RunTestWithRealm((realm, storage) => + { + BeatmapSetInfo? detachedSet = null; + + using (var importer = new BeatmapModelManager(realm, storage)) + using (new RealmRulesetStore(realm, storage)) + { + Task.Run(async () => + { + Live? beatmapSet; + + using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) + // ReSharper disable once AccessToDisposedClosure + beatmapSet = await importer.Import(reader); + + Assert.NotNull(beatmapSet); + Debug.Assert(beatmapSet != null); + + // Intentionally detach on async thread as to not trigger a refresh on the main thread. + beatmapSet.PerformRead(s => detachedSet = s.Detach()); + }).WaitSafely(); + + Debug.Assert(detachedSet != null); + importer.AddFile(detachedSet, new MemoryStream(), "test"); + } + }); + } + [Test] public void TestImportBeatmapThenCleanup() { From 33f024212ffaaa1d70c38b20ff46e42b77fd8654 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 May 2022 20:47:26 +0900 Subject: [PATCH 2/3] Fix realm refetch operations potentially being unsafe As seen in test failure https://github.com/ppy/osu/runs/6357384721?check_suite_focus=true. --- osu.Game/Database/RealmAccess.cs | 20 +++++++++++++++++ osu.Game/Stores/RealmArchiveModelManager.cs | 25 ++++++++++++++------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index b0a70b51d0..937876a70e 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -344,6 +344,26 @@ namespace osu.Game.Database } } + /// + /// Write changes to realm. + /// + /// The work to run. + public T Write(Func action) + { + if (ThreadSafety.IsUpdateThread) + { + total_writes_update.Value++; + return Realm.Write(action); + } + else + { + total_writes_async.Value++; + + using (var realm = getRealmInstance()) + return realm.Write(action); + } + } + /// /// Write changes to realm. /// diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 57e51b79aa..e349efe5ad 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -45,11 +45,16 @@ namespace osu.Game.Stores // This method should be removed as soon as all the surrounding pieces support non-detached operations. if (!item.IsManaged) { - var managed = Realm.Realm.Find(item.ID); - managed.Realm.Write(() => operation(managed)); + // Importantly, begin the realm write *before* re-fetching, else the update realm may not be in a consistent state + // (ie. if an async import finished very recently). + Realm.Realm.Write(realm => + { + var managed = Realm.Realm.Find(item.ID); + operation(managed); - item.Files.Clear(); - item.Files.AddRange(managed.Files.Detach()); + item.Files.Clear(); + item.Files.AddRange(managed.Files.Detach()); + }); } else operation(item); @@ -165,7 +170,9 @@ namespace osu.Game.Stores public bool Delete(TModel item) { - return Realm.Run(realm => + // Importantly, begin the realm write *before* re-fetching, else the update realm may not be in a consistent state + // (ie. if an async import finished very recently). + return Realm.Write(realm => { if (!item.IsManaged) item = realm.Find(item.ID); @@ -173,14 +180,16 @@ namespace osu.Game.Stores if (item?.DeletePending != false) return false; - realm.Write(r => item.DeletePending = true); + item.DeletePending = true; return true; }); } public void Undelete(TModel item) { - Realm.Run(realm => + // Importantly, begin the realm write *before* re-fetching, else the update realm may not be in a consistent state + // (ie. if an async import finished very recently). + Realm.Write(realm => { if (!item.IsManaged) item = realm.Find(item.ID); @@ -188,7 +197,7 @@ namespace osu.Game.Stores if (item?.DeletePending != true) return; - realm.Write(r => item.DeletePending = false); + item.DeletePending = false; }); } From ec231e0f312eeeecb8229a0fe1ee0660935b455f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 May 2022 00:45:17 +0900 Subject: [PATCH 3/3] Use more local realm reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Stores/RealmArchiveModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index e349efe5ad..cc8229b436 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -49,7 +49,7 @@ namespace osu.Game.Stores // (ie. if an async import finished very recently). Realm.Realm.Write(realm => { - var managed = Realm.Realm.Find(item.ID); + var managed = realm.Find(item.ID); operation(managed); item.Files.Clear();