From 0c9eb3ad61a412f6ebed4d0dc08f593743cee8d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 01:33:45 +0900 Subject: [PATCH 01/14] Add realm factory helper methods to run work on the correct context Avoids constructing a new `Realm` instance when called from the update thread without worrying about disposal. --- osu.Game/Database/RealmContextFactory.cs | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 31dbb0c6c4..50e456a0c8 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -169,6 +169,41 @@ namespace osu.Game.Database /// public bool Compact() => Realm.Compact(getConfiguration()); + /// + /// Run work on realm with a return value. + /// + /// + /// Handles correct context management automatically. + /// + /// The work to run. + /// The return type. + public T Run(Func action) + { + if (ThreadSafety.IsUpdateThread) + return action(Context); + + using (var realm = CreateContext()) + return action(realm); + } + + /// + /// Run work on realm. + /// + /// + /// Handles correct context management automatically. + /// + /// The work to run. + public void Run(Action action) + { + if (ThreadSafety.IsUpdateThread) + action(Context); + else + { + using (var realm = CreateContext()) + action(realm); + } + } + public Realm CreateContext() { if (isDisposed) From a5d2047f055bbbda917d8acec11f09a363b202ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 01:34:20 +0900 Subject: [PATCH 02/14] Fix various cases of creating realm contexts from update thread when not necessary --- osu.Game/Beatmaps/BeatmapManager.cs | 39 ++++++++++--------- osu.Game/Beatmaps/BeatmapModelManager.cs | 7 ++-- .../Settings/Sections/Input/KeyBindingRow.cs | 4 +- osu.Game/Scoring/ScoreModelManager.cs | 3 +- osu.Game/Skinning/SkinManager.cs | 9 ++--- osu.Game/Stores/BeatmapImporter.cs | 3 +- osu.Game/Stores/RealmArchiveModelManager.cs | 8 ++-- 7 files changed, 36 insertions(+), 37 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index ee649ad960..cc765657cc 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -119,15 +119,17 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to hide. public void Hide(BeatmapInfo beatmapInfo) { - using (var realm = contextFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) + contextFactory.Run(realm => { - if (!beatmapInfo.IsManaged) - beatmapInfo = realm.Find(beatmapInfo.ID); + using (var transaction = realm.BeginWrite()) + { + if (!beatmapInfo.IsManaged) + beatmapInfo = realm.Find(beatmapInfo.ID); - beatmapInfo.Hidden = true; - transaction.Commit(); - } + beatmapInfo.Hidden = true; + transaction.Commit(); + } + }); } /// @@ -136,15 +138,17 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to restore. public void Restore(BeatmapInfo beatmapInfo) { - using (var realm = contextFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) + contextFactory.Run(realm => { - if (!beatmapInfo.IsManaged) - beatmapInfo = realm.Find(beatmapInfo.ID); + using (var transaction = realm.BeginWrite()) + { + if (!beatmapInfo.IsManaged) + beatmapInfo = realm.Find(beatmapInfo.ID); - beatmapInfo.Hidden = false; - transaction.Commit(); - } + beatmapInfo.Hidden = false; + transaction.Commit(); + } + }); } public void RestoreAll() @@ -176,8 +180,7 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public ILive? QueryBeatmapSet(Expression> query) { - using (var context = contextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.ToLive(contextFactory); + return contextFactory.Run(realm => realm.All().FirstOrDefault(query)?.ToLive(contextFactory)); } #region Delegation to BeatmapModelManager (methods which previously existed locally). @@ -305,13 +308,13 @@ namespace osu.Game.Beatmaps // If we seem to be missing files, now is a good time to re-fetch. if (importedBeatmap?.BeatmapSet?.Files.Count == 0) { - using (var realm = contextFactory.CreateContext()) + contextFactory.Run(realm => { var refetch = realm.Find(importedBeatmap.ID)?.Detach(); if (refetch != null) importedBeatmap = refetch; - } + }); } return workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 3822c6e121..a4ba13a88d 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -98,17 +98,16 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) { - using (var context = ContextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.Detach(); + return ContextFactory.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); } public void Update(BeatmapSetInfo item) { - using (var realm = ContextFactory.CreateContext()) + ContextFactory.Run(realm => { var existing = realm.Find(item.ID); realm.Write(r => item.CopyChangesToRealm(existing)); - } + }); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index e0a1a82326..60aff91301 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -386,11 +386,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateStoreFromButton(KeyButton button) { - using (var realm = realmFactory.CreateContext()) + realmFactory.Run(realm => { var binding = realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); - } + }); } private void updateIsDefaultValue() diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 5ba152fad3..5e560effa1 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -74,8 +74,7 @@ namespace osu.Game.Scoring public override bool IsAvailableLocally(ScoreInfo model) { - using (var context = ContextFactory.CreateContext()) - return context.All().Any(b => b.OnlineID == model.OnlineID); + return ContextFactory.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); } } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index cde21b78c1..3f6e5754fb 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -113,10 +113,10 @@ namespace osu.Game.Skinning public void SelectRandomSkin() { - using (var context = contextFactory.CreateContext()) + contextFactory.Run(realm => { // choose from only user skins, removing the current selection to ensure a new one is chosen. - var randomChoices = context.All().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); + var randomChoices = realm.All().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); if (randomChoices.Length == 0) { @@ -127,7 +127,7 @@ namespace osu.Game.Skinning var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); CurrentSkinInfo.Value = chosen.ToLive(contextFactory); - } + }); } /// @@ -182,8 +182,7 @@ namespace osu.Game.Skinning /// The first result for the provided query, or null if no results were found. public ILive Query(Expression> query) { - using (var context = contextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.ToLive(contextFactory); + return contextFactory.Run(realm => realm.All().FirstOrDefault(query)?.ToLive(contextFactory)); } public event Action SourceChanged; diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index d285a6b61c..61178014ef 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -165,8 +165,7 @@ namespace osu.Game.Stores public override bool IsAvailableLocally(BeatmapSetInfo model) { - using (var context = ContextFactory.CreateContext()) - return context.All().Any(b => b.OnlineID == model.OnlineID); + return ContextFactory.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); } public override string HumanisedModelName => "beatmap"; diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index b456dae343..115fbf721d 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -165,7 +165,7 @@ namespace osu.Game.Stores public bool Delete(TModel item) { - using (var realm = ContextFactory.CreateContext()) + return ContextFactory.Run(realm => { if (!item.IsManaged) item = realm.Find(item.ID); @@ -175,12 +175,12 @@ namespace osu.Game.Stores realm.Write(r => item.DeletePending = true); return true; - } + }); } public void Undelete(TModel item) { - using (var realm = ContextFactory.CreateContext()) + ContextFactory.Run(realm => { if (!item.IsManaged) item = realm.Find(item.ID); @@ -189,7 +189,7 @@ namespace osu.Game.Stores return; realm.Write(r => item.DeletePending = false); - } + }); } public abstract bool IsAvailableLocally(TModel model); From dde10d1ba214691e8f41616a0f3fe5b1949867cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 16:38:07 +0900 Subject: [PATCH 03/14] Remove unused `IRealmFactory` interface --- osu.Game/Database/IRealmFactory.cs | 20 -------------------- osu.Game/Database/RealmContextFactory.cs | 2 +- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 osu.Game/Database/IRealmFactory.cs diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs deleted file mode 100644 index a957424584..0000000000 --- a/osu.Game/Database/IRealmFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Realms; - -namespace osu.Game.Database -{ - public interface IRealmFactory - { - /// - /// The main realm context, bound to the update thread. - /// - Realm Context { get; } - - /// - /// Create a new realm context for use on the current thread. - /// - Realm CreateContext(); - } -} diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 50e456a0c8..c1b159eac7 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. /// - public class RealmContextFactory : IDisposable, IRealmFactory + public class RealmContextFactory : IDisposable { private readonly Storage storage; From a59105635e36c31baf63217ea74d472ec09dc239 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 16:40:20 +0900 Subject: [PATCH 04/14] Make `CreateContext` private --- osu.Game/Database/RealmContextFactory.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c1b159eac7..5c9d2d7c5a 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -72,13 +72,13 @@ namespace osu.Game.Database get { if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"Use {nameof(CreateContext)} when performing realm operations from a non-update thread"); + throw new InvalidOperationException(@$"Use {nameof(createContext)} when performing realm operations from a non-update thread"); lock (contextLock) { if (context == null) { - context = CreateContext(); + context = createContext(); Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); } @@ -124,7 +124,7 @@ namespace osu.Game.Database private void cleanupPendingDeletions() { - using (var realm = CreateContext()) + using (var realm = createContext()) using (var transaction = realm.BeginWrite()) { var pendingDeleteScores = realm.All().Where(s => s.DeletePending); @@ -182,7 +182,7 @@ namespace osu.Game.Database if (ThreadSafety.IsUpdateThread) return action(Context); - using (var realm = CreateContext()) + using (var realm = createContext()) return action(realm); } @@ -199,12 +199,12 @@ namespace osu.Game.Database action(Context); else { - using (var realm = CreateContext()) + using (var realm = createContext()) action(realm); } } - public Realm CreateContext() + private Realm createContext() { if (isDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); From da0a803e6c2df35194fd5a21e98f5940955d9f5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 17:08:02 +0900 Subject: [PATCH 05/14] Add `RealmContextFactory.Write` helper method --- osu.Game/Database/RealmContextFactory.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 5c9d2d7c5a..ea33fec2a0 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -204,6 +204,24 @@ namespace osu.Game.Database } } + /// + /// Write changes to realm. + /// + /// + /// Handles correct context management and transaction committing automatically. + /// + /// The work to run. + public void Write(Action action) + { + if (ThreadSafety.IsUpdateThread) + Context.Write(action); + else + { + using (var realm = createContext()) + realm.Write(action); + } + } + private Realm createContext() { if (isDisposed) From 114c9e8c1f985b2df61cd8e65e9a3bef9f94bb57 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 17:08:20 +0900 Subject: [PATCH 06/14] Update all usages of `CreateContext` to use either `Run` or `Write` --- .../Beatmaps/IO/BeatmapImportHelper.cs | 3 +- osu.Game.Tests/Database/GeneralUsageTests.cs | 20 ++-- osu.Game.Tests/Database/RealmLiveTests.cs | 57 +++++----- .../Database/TestRealmKeyBindingStore.cs | 27 ++--- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 4 +- .../Visual/Ranking/TestSceneResultsScreen.cs | 4 +- .../TestSceneDeleteLocalScore.cs | 4 +- osu.Game/Beatmaps/BeatmapManager.cs | 26 ++--- osu.Game/Beatmaps/BeatmapModelManager.cs | 4 +- osu.Game/Database/EFToRealmMigrator.cs | 104 +++++++++-------- osu.Game/Database/RealmLive.cs | 7 +- osu.Game/Input/RealmKeyBindingStore.cs | 36 +++--- .../Sections/Input/KeyBindingsSubsection.cs | 5 +- .../Configuration/RulesetConfigManager.cs | 18 +-- osu.Game/Rulesets/RulesetStore.cs | 107 +++++++++--------- osu.Game/Scoring/ScoreManager.cs | 11 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 6 +- osu.Game/Skinning/SkinManager.cs | 19 ++-- osu.Game/Skinning/SkinModelManager.cs | 4 +- osu.Game/Stores/RealmArchiveModelImporter.cs | 4 +- osu.Game/Stores/RealmFileStore.cs | 7 +- 22 files changed, 230 insertions(+), 250 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs index 44f6943871..7aa2dc7093 100644 --- a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs +++ b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs @@ -55,8 +55,7 @@ namespace osu.Game.Tests.Beatmaps.IO { var realmContextFactory = osu.Dependencies.Get(); - using (var realm = realmContextFactory.CreateContext()) - BeatmapImporterTests.EnsureLoaded(realm, timeout); + realmContextFactory.Run(realm => BeatmapImporterTests.EnsureLoaded(realm, timeout)); // TODO: add back some extra checks outside of the realm ones? // var set = queryBeatmapSets().First(); diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 0961ad71e4..9ebe94b383 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Database [Test] public void TestConstructRealm() { - RunTestWithRealm((realmFactory, _) => { realmFactory.CreateContext().Refresh(); }); + RunTestWithRealm((realmFactory, _) => { realmFactory.Run(realm => realm.Refresh()); }); } [Test] @@ -46,23 +46,21 @@ namespace osu.Game.Tests.Database { bool callbackRan = false; - using (var context = realmFactory.CreateContext()) + realmFactory.Run(realm => { - var subscription = context.All().QueryAsyncWithNotifications((sender, changes, error) => + var subscription = realm.All().QueryAsyncWithNotifications((sender, changes, error) => { - using (realmFactory.CreateContext()) + realmFactory.Run(_ => { callbackRan = true; - } + }); }); // Force the callback above to run. - using (realmFactory.CreateContext()) - { - } + realmFactory.Run(r => r.Refresh()); subscription?.Dispose(); - } + }); Assert.IsTrue(callbackRan); }); @@ -78,12 +76,12 @@ namespace osu.Game.Tests.Database Task.Factory.StartNew(() => { - using (realmFactory.CreateContext()) + realmFactory.Run(_ => { hasThreadedUsage.Set(); stopThreadedUsage.Wait(); - } + }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler); hasThreadedUsage.Wait(); diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 187fcd3ca7..2f16df4624 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -23,9 +23,9 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory); + ILive beatmap = realmFactory.Run(realm => realm.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory)); - ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(realmFactory); + ILive beatmap2 = realmFactory.Run(realm => realm.All().First().ToLive(realmFactory)); Assert.AreEqual(beatmap, beatmap2); }); @@ -38,14 +38,14 @@ namespace osu.Game.Tests.Database { var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); - ILive liveBeatmap; + ILive? liveBeatmap = null; - using (var context = realmFactory.CreateContext()) + realmFactory.Run(realm => { - context.Write(r => r.Add(beatmap)); + realm.Write(r => r.Add(beatmap)); liveBeatmap = beatmap.ToLive(realmFactory); - } + }); using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) { @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Database storage.Migrate(migratedStorage); - Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); + Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden)); } }); } @@ -67,8 +67,7 @@ namespace osu.Game.Tests.Database var liveBeatmap = beatmap.ToLive(realmFactory); - using (var context = realmFactory.CreateContext()) - context.Write(r => r.Add(beatmap)); + realmFactory.Run(realm => realm.Write(r => r.Add(beatmap))); Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); }); @@ -99,12 +98,12 @@ namespace osu.Game.Tests.Database ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - using (var threadContext = realmFactory.CreateContext()) + realmFactory.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); - } + }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); Debug.Assert(liveBeatmap != null); @@ -128,12 +127,12 @@ namespace osu.Game.Tests.Database ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - using (var threadContext = realmFactory.CreateContext()) + realmFactory.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); - } + }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); Debug.Assert(liveBeatmap != null); @@ -170,12 +169,12 @@ namespace osu.Game.Tests.Database Task.Factory.StartNew(() => { - using (var threadContext = realmFactory.CreateContext()) + realmFactory.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); - } + }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); Debug.Assert(liveBeatmap != null); @@ -189,13 +188,13 @@ namespace osu.Game.Tests.Database }); // Can't be used, even from within a valid context. - using (realmFactory.CreateContext()) + realmFactory.Run(threadContext => { Assert.Throws(() => { var __ = liveBeatmap.Value; }); - } + }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }); } @@ -208,12 +207,12 @@ namespace osu.Game.Tests.Database ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - using (var threadContext = realmFactory.CreateContext()) + realmFactory.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); - } + }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); Debug.Assert(liveBeatmap != null); @@ -235,50 +234,50 @@ namespace osu.Game.Tests.Database { int changesTriggered = 0; - using (var updateThreadContext = realmFactory.CreateContext()) + realmFactory.Run(outerRealm => { - updateThreadContext.All().QueryAsyncWithNotifications(gotChange); + outerRealm.All().QueryAsyncWithNotifications(gotChange); ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - using (var threadContext = realmFactory.CreateContext()) + realmFactory.Run(innerRealm => { var ruleset = CreateRuleset(); - var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); + var beatmap = innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); // add a second beatmap to ensure that a full refresh occurs below. // not just a refresh from the resolved Live. - threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); + innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); - } + }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); Debug.Assert(liveBeatmap != null); // not yet seen by main context - Assert.AreEqual(0, updateThreadContext.All().Count()); + Assert.AreEqual(0, outerRealm.All().Count()); Assert.AreEqual(0, changesTriggered); liveBeatmap.PerformRead(resolved => { // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. // ReSharper disable once AccessToDisposedClosure - Assert.AreEqual(2, updateThreadContext.All().Count()); + Assert.AreEqual(2, outerRealm.All().Count()); Assert.AreEqual(1, changesTriggered); // can access properties without a crash. Assert.IsFalse(resolved.Hidden); // ReSharper disable once AccessToDisposedClosure - updateThreadContext.Write(r => + outerRealm.Write(r => { // can use with the main context. r.Remove(resolved); }); }); - } + }); void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) { diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index e3c1d42667..c1041e9fd6 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -60,15 +60,12 @@ namespace osu.Game.Tests.Database KeyBindingContainer testContainer = new TestKeyBindingContainer(); // Add some excess bindings for an action which only supports 1. - using (var realm = realmContextFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) + realmContextFactory.Write(realm => { realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A))); realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S))); realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D))); - - transaction.Commit(); - } + }); Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3)); @@ -79,13 +76,13 @@ namespace osu.Game.Tests.Database private int queryCount(GlobalAction? match = null) { - using (var realm = realmContextFactory.CreateContext()) + return realmContextFactory.Run(realm => { var results = realm.All(); if (match.HasValue) results = results.Where(k => k.ActionInt == (int)match.Value); return results.Count(); - } + }); } [Test] @@ -95,26 +92,26 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer, Enumerable.Empty()); - using (var primaryRealm = realmContextFactory.CreateContext()) + realmContextFactory.Run(outerRealm => { - var backBinding = primaryRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); + var backBinding = outerRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); var tsr = ThreadSafeReference.Create(backBinding); - using (var threadedContext = realmContextFactory.CreateContext()) + realmContextFactory.Run(innerRealm => { - var binding = threadedContext.ResolveReference(tsr); - threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); - } + var binding = innerRealm.ResolveReference(tsr); + innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); + }); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); // check still correct after re-query. - backBinding = primaryRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); + backBinding = outerRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); - } + }); } [TearDown] diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 8c24b2eef8..1d639c6418 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -60,8 +60,8 @@ namespace osu.Game.Tests.Online testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapSet = testBeatmapInfo.BeatmapSet; - ContextFactory.Context.Write(r => r.RemoveAll()); - ContextFactory.Context.Write(r => r.RemoveAll()); + ContextFactory.Write(r => r.RemoveAll()); + ContextFactory.Write(r => r.RemoveAll()); selectedItem.Value = new PlaylistItem { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 62500babc1..a77480ee54 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Ranking { base.LoadComplete(); - using (var realm = realmContextFactory.CreateContext()) + realmContextFactory.Run(realm => { var beatmapInfo = realm.All() .Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0) @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Ranking if (beatmapInfo != null) Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); - } + }); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 1e14e4b3e5..f43354514b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -122,11 +122,11 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void Setup() => Schedule(() => { - using (var realm = realmFactory.CreateContext()) + realmFactory.Run(realm => { // Due to soft deletions, we can re-use deleted scores between test runs scoreManager.Undelete(realm.All().Where(s => s.DeletePending).ToList()); - } + }); leaderboard.Scores = null; leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index cc765657cc..a9340e1250 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -153,14 +153,16 @@ namespace osu.Game.Beatmaps public void RestoreAll() { - using (var realm = contextFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) + contextFactory.Run(realm => { - foreach (var beatmap in realm.All().Where(b => b.Hidden)) - beatmap.Hidden = false; + using (var transaction = realm.BeginWrite()) + { + foreach (var beatmap in realm.All().Where(b => b.Hidden)) + beatmap.Hidden = false; - transaction.Commit(); - } + transaction.Commit(); + } + }); } /// @@ -169,8 +171,7 @@ namespace osu.Game.Beatmaps /// A list of available . public List GetAllUsableBeatmapSets() { - using (var context = contextFactory.CreateContext()) - return context.All().Where(b => !b.DeletePending).Detach(); + return contextFactory.Run(realm => realm.All().Where(b => !b.DeletePending).Detach()); } /// @@ -235,21 +236,20 @@ namespace osu.Game.Beatmaps public void Delete(Expression>? filter = null, bool silent = false) { - using (var context = contextFactory.CreateContext()) + contextFactory.Run(realm => { - var items = context.All().Where(s => !s.DeletePending && !s.Protected); + var items = realm.All().Where(s => !s.DeletePending && !s.Protected); if (filter != null) items = items.Where(filter); beatmapModelManager.Delete(items.ToList(), silent); - } + }); } public void UndeleteAll() { - using (var context = contextFactory.CreateContext()) - beatmapModelManager.Undelete(context.All().Where(s => s.DeletePending).ToList()); + contextFactory.Run(realm => beatmapModelManager.Undelete(realm.All().Where(s => s.DeletePending).ToList())); } public void Undelete(List items, bool silent = false) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index a4ba13a88d..44d6af5b73 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -103,10 +103,10 @@ namespace osu.Game.Beatmaps public void Update(BeatmapSetInfo item) { - ContextFactory.Run(realm => + ContextFactory.Write(realm => { var existing = realm.Find(item.ID); - realm.Write(r => item.CopyChangesToRealm(existing)); + item.CopyChangesToRealm(existing); }); } } diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 0f726f8ee5..58321efcc9 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -73,7 +73,7 @@ namespace osu.Game.Database int count = existingBeatmapSets.Count(); - using (var realm = realmContextFactory.CreateContext()) + realmContextFactory.Run(realm => { Logger.Log($"Found {count} beatmaps in EF", LoggingTarget.Database); @@ -160,7 +160,7 @@ namespace osu.Game.Database Logger.Log($"Successfully migrated {count} beatmaps to realm", LoggingTarget.Database); } - } + }); } private BeatmapMetadata getBestMetadata(EFBeatmapMetadata? beatmapMetadata, EFBeatmapMetadata? beatmapSetMetadata) @@ -206,7 +206,7 @@ namespace osu.Game.Database int count = existingScores.Count(); - using (var realm = realmContextFactory.CreateContext()) + realmContextFactory.Run(realm => { Logger.Log($"Found {count} scores in EF", LoggingTarget.Database); @@ -276,7 +276,7 @@ namespace osu.Game.Database Logger.Log($"Successfully migrated {count} scores to realm", LoggingTarget.Database); } - } + }); } private void migrateSkins(OsuDbContext db) @@ -307,37 +307,39 @@ namespace osu.Game.Database break; } - using (var realm = realmContextFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) + realmContextFactory.Run(realm => { - // only migrate data if the realm database is empty. - // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. - if (!realm.All().Any(s => !s.Protected)) + using (var transaction = realm.BeginWrite()) { - Logger.Log($"Migrating {existingSkins.Count} skins", LoggingTarget.Database); - - foreach (var skin in existingSkins) + // only migrate data if the realm database is empty. + // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. + if (!realm.All().Any(s => !s.Protected)) { - var realmSkin = new SkinInfo + Logger.Log($"Migrating {existingSkins.Count} skins", LoggingTarget.Database); + + foreach (var skin in existingSkins) { - Name = skin.Name, - Creator = skin.Creator, - Hash = skin.Hash, - Protected = false, - InstantiationInfo = skin.InstantiationInfo, - }; + var realmSkin = new SkinInfo + { + Name = skin.Name, + Creator = skin.Creator, + Hash = skin.Hash, + Protected = false, + InstantiationInfo = skin.InstantiationInfo, + }; - migrateFiles(skin, realm, realmSkin); + migrateFiles(skin, realm, realmSkin); - realm.Add(realmSkin); + realm.Add(realmSkin); - if (skin.ID == userSkinInt) - userSkinChoice.Value = realmSkin.ID.ToString(); + if (skin.ID == userSkinInt) + userSkinChoice.Value = realmSkin.ID.ToString(); + } } - } - transaction.Commit(); - } + transaction.Commit(); + } + }); } private static void migrateFiles(IHasFiles fileSource, Realm realm, IHasRealmFiles realmObject) where T : INamedFileInfo @@ -365,36 +367,38 @@ namespace osu.Game.Database Logger.Log("Beginning settings migration to realm", LoggingTarget.Database); ensureBackup(); - using (var realm = realmContextFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) + realmContextFactory.Run(realm => { - // only migrate data if the realm database is empty. - if (!realm.All().Any()) + using (var transaction = realm.BeginWrite()) { - Logger.Log($"Migrating {existingSettings.Count} settings", LoggingTarget.Database); - - foreach (var dkb in existingSettings) + // only migrate data if the realm database is empty. + if (!realm.All().Any()) { - if (dkb.RulesetID == null) - continue; + Logger.Log($"Migrating {existingSettings.Count} settings", LoggingTarget.Database); - string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value); - - if (string.IsNullOrEmpty(shortName)) - continue; - - realm.Add(new RealmRulesetSetting + foreach (var dkb in existingSettings) { - Key = dkb.Key, - Value = dkb.StringValue, - RulesetName = shortName, - Variant = dkb.Variant ?? 0, - }); - } - } + if (dkb.RulesetID == null) + continue; - transaction.Commit(); - } + string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value); + + if (string.IsNullOrEmpty(shortName)) + continue; + + realm.Add(new RealmRulesetSetting + { + Key = dkb.Key, + Value = dkb.StringValue, + RulesetName = shortName, + Variant = dkb.Variant ?? 0, + }); + } + } + + transaction.Commit(); + } + }); } private string? getRulesetShortNameFromLegacyID(long rulesetId) => diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 6594224666..05367160f3 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -51,8 +51,7 @@ namespace osu.Game.Database return; } - using (var realm = realmFactory.CreateContext()) - perform(realm.Find(ID)); + realmFactory.Run(realm => perform(realm.Find(ID))); } /// @@ -64,7 +63,7 @@ namespace osu.Game.Database if (!IsManaged) return perform(data); - using (var realm = realmFactory.CreateContext()) + return realmFactory.Run(realm => { var returnData = perform(realm.Find(ID)); @@ -72,7 +71,7 @@ namespace osu.Game.Database throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}."); return returnData; - } + }); } /// diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 99f5752cfb..60f7eb2198 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -34,7 +34,7 @@ namespace osu.Game.Input { List combinations = new List(); - using (var context = realmFactory.CreateContext()) + realmFactory.Run(context => { foreach (var action in context.All().Where(b => string.IsNullOrEmpty(b.RulesetName) && (GlobalAction)b.ActionInt == globalAction)) { @@ -44,7 +44,7 @@ namespace osu.Game.Input if (str.Length > 0) combinations.Add(str); } - } + }); return combinations; } @@ -56,24 +56,26 @@ namespace osu.Game.Input /// The rulesets to populate defaults from. public void Register(KeyBindingContainer container, IEnumerable rulesets) { - using (var realm = realmFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) + realmFactory.Run(realm => { - // intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed. - // this is much faster as a result. - var existingBindings = realm.All().ToList(); - - insertDefaults(realm, existingBindings, container.DefaultKeyBindings); - - foreach (var ruleset in rulesets) + using (var transaction = realm.BeginWrite()) { - var instance = ruleset.CreateInstance(); - foreach (int variant in instance.AvailableVariants) - insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant); - } + // intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed. + // this is much faster as a result. + var existingBindings = realm.All().ToList(); - transaction.Commit(); - } + insertDefaults(realm, existingBindings, container.DefaultKeyBindings); + + foreach (var ruleset in rulesets) + { + var instance = ruleset.CreateInstance(); + foreach (int variant in instance.AvailableVariants) + insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant); + } + + transaction.Commit(); + } + }); } private void insertDefaults(Realm realm, List existingBindings, IEnumerable defaults, string? rulesetName = null, int? variant = null) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 94c7c66538..9075dfefd4 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -34,10 +34,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input { string rulesetName = Ruleset?.ShortName; - List bindings; + List bindings = null; - using (var realm = realmFactory.CreateContext()) - bindings = realm.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).Detach(); + realmFactory.Run(realm => bindings = realm.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).Detach()); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index 17678775e9..60a6b70221 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -56,21 +56,15 @@ namespace osu.Game.Rulesets.Configuration pendingWrites.Clear(); } - if (realmFactory == null) - return true; - - using (var context = realmFactory.CreateContext()) + realmFactory?.Write(realm => { - context.Write(realm => + foreach (var c in changed) { - foreach (var c in changed) - { - var setting = realm.All().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString()); + var setting = realm.All().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString()); - setting.Value = ConfigStore[c].ToString(); - } - }); - } + setting.Value = ConfigStore[c].ToString(); + } + }); return true; } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index c675fbbf63..a9e5ff797c 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -100,74 +100,71 @@ namespace osu.Game.Rulesets private void addMissingRulesets() { - using (var context = realmFactory.CreateContext()) + realmFactory.Write(realm => { - context.Write(realm => + var rulesets = realm.All(); + + List instances = loadedAssemblies.Values + .Select(r => Activator.CreateInstance(r) as Ruleset) + .Where(r => r != null) + .Select(r => r.AsNonNull()) + .ToList(); + + // add all legacy rulesets first to ensure they have exclusive choice of primary key. + foreach (var r in instances.Where(r => r is ILegacyRuleset)) { - var rulesets = realm.All(); + if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null) + realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); + } - List instances = loadedAssemblies.Values - .Select(r => Activator.CreateInstance(r) as Ruleset) - .Where(r => r != null) - .Select(r => r.AsNonNull()) - .ToList(); - - // add all legacy rulesets first to ensure they have exclusive choice of primary key. - foreach (var r in instances.Where(r => r is ILegacyRuleset)) + // add any other rulesets which have assemblies present but are not yet in the database. + foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) + { + if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) { - if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null) + var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName); + + if (existingSameShortName != null) + { + // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName. + // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one. + // in such cases, update the instantiation info of the existing entry to point to the new one. + existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; + } + else realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); } + } - // add any other rulesets which have assemblies present but are not yet in the database. - foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) + List detachedRulesets = new List(); + + // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. + foreach (var r in rulesets.OrderBy(r => r.OnlineID)) + { + try { - if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) - { - var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName); + var resolvedType = Type.GetType(r.InstantiationInfo) + ?? throw new RulesetLoadException(@"Type could not be resolved"); - if (existingSameShortName != null) - { - // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName. - // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one. - // in such cases, update the instantiation info of the existing entry to point to the new one. - existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; - } - else - realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); - } + var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo + ?? throw new RulesetLoadException(@"Instantiation failure"); + + r.Name = instanceInfo.Name; + r.ShortName = instanceInfo.ShortName; + r.InstantiationInfo = instanceInfo.InstantiationInfo; + r.Available = true; + + detachedRulesets.Add(r.Clone()); } - - List detachedRulesets = new List(); - - // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. - foreach (var r in rulesets.OrderBy(r => r.OnlineID)) + catch (Exception ex) { - try - { - var resolvedType = Type.GetType(r.InstantiationInfo) - ?? throw new RulesetLoadException(@"Type could not be resolved"); - - var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo - ?? throw new RulesetLoadException(@"Instantiation failure"); - - r.Name = instanceInfo.Name; - r.ShortName = instanceInfo.ShortName; - r.InstantiationInfo = instanceInfo.InstantiationInfo; - r.Available = true; - - detachedRulesets.Add(r.Clone()); - } - catch (Exception ex) - { - r.Available = false; - Logger.Log($"Could not load ruleset {r}: {ex.Message}"); - } + r.Available = false; + Logger.Log($"Could not load ruleset {r}: {ex.Message}"); } + } - availableRulesets.AddRange(detachedRulesets); - }); - } + availableRulesets.AddRange(detachedRulesets); + }); } private void loadFromAppDomain() diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index ccf3226792..f895134f97 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -51,8 +51,7 @@ namespace osu.Game.Scoring /// The first result for the provided query, or null if no results were found. public ScoreInfo Query(Expression> query) { - using (var context = contextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.Detach(); + return contextFactory.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); } /// @@ -255,16 +254,16 @@ namespace osu.Game.Scoring public void Delete([CanBeNull] Expression> filter = null, bool silent = false) { - using (var context = contextFactory.CreateContext()) + contextFactory.Run(realm => { - var items = context.All() - .Where(s => !s.DeletePending); + var items = realm.All() + .Where(s => !s.DeletePending); if (filter != null) items = items.Where(filter); scoreModelManager.Delete(items.ToList(), silent); - } + }); } public void Delete(List items, bool silent = false) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 75ad0511e6..fe3d407a64 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -178,8 +178,7 @@ namespace osu.Game.Screens.Select if (!loadedTestBeatmaps) { - using (var realm = realmFactory.CreateContext()) - loadBeatmapSets(getBeatmapSets(realm)); + realmFactory.Run(realm => loadBeatmapSets(getBeatmapSets(realm))); } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 49f2ea5d64..da52b43ab6 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { - using (var realm = realmFactory.CreateContext()) + realmFactory.Run(realm => { var scores = realm.All() .AsEnumerable() @@ -171,9 +171,9 @@ namespace osu.Game.Screens.Select.Leaderboards scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) .ContinueWith(ordered => scoresCallback?.Invoke(ordered.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); + }); - return null; - } + return null; } if (api?.IsLoggedIn != true) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 3f6e5754fb..82bcd3b292 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -87,17 +87,14 @@ namespace osu.Game.Skinning }; // Ensure the default entries are present. - using (var context = contextFactory.CreateContext()) - using (var transaction = context.BeginWrite()) + contextFactory.Write(realm => { foreach (var skin in defaultSkins) { - if (context.Find(skin.SkinInfo.ID) == null) - context.Add(skin.SkinInfo.Value); + if (realm.Find(skin.SkinInfo.ID) == null) + realm.Add(skin.SkinInfo.Value); } - - transaction.Commit(); - } + }); CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin); @@ -292,10 +289,10 @@ namespace osu.Game.Skinning public void Delete([CanBeNull] Expression> filter = null, bool silent = false) { - using (var context = contextFactory.CreateContext()) + contextFactory.Run(realm => { - var items = context.All() - .Where(s => !s.Protected && !s.DeletePending); + var items = realm.All() + .Where(s => !s.Protected && !s.DeletePending); if (filter != null) items = items.Where(filter); @@ -306,7 +303,7 @@ namespace osu.Game.Skinning scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()); skinModelManager.Delete(items.ToList(), silent); - } + }); } #endregion diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index b8313f63a3..a1926913a9 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -205,7 +205,7 @@ namespace osu.Game.Skinning private void populateMissingHashes() { - using (var realm = ContextFactory.CreateContext()) + ContextFactory.Run(realm => { var skinsWithoutHashes = realm.All().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray(); @@ -221,7 +221,7 @@ namespace osu.Game.Skinning Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid"); } } - } + }); } private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources); diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 2ea7aecc94..3d8e9f2703 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -320,7 +320,7 @@ namespace osu.Game.Stores /// An optional cancellation token. public virtual Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { - using (var realm = ContextFactory.CreateContext()) + return ContextFactory.Run(realm => { cancellationToken.ThrowIfCancellationRequested(); @@ -414,7 +414,7 @@ namespace osu.Game.Stores } return Task.FromResult((ILive?)item.ToLive(ContextFactory)); - } + }); } private string computeHashFast(ArchiveReader reader) diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs index f9abbda4c0..ca371e29be 100644 --- a/osu.Game/Stores/RealmFileStore.cs +++ b/osu.Game/Stores/RealmFileStore.cs @@ -92,8 +92,7 @@ namespace osu.Game.Stores int removedFiles = 0; // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. - using (var realm = realmFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) + realmFactory.Write(realm => { // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) var files = realm.All().ToList(); @@ -116,9 +115,7 @@ namespace osu.Game.Stores Logger.Error(e, $@"Could not delete databased file {file.Hash}"); } } - - transaction.Commit(); - } + }); Logger.Log($@"Finished realm file store cleanup ({removedFiles} of {totalFiles} deleted)"); } From d2655c082546033490792d6f3e4d1806ba964e8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 17:27:30 +0900 Subject: [PATCH 07/14] Fix `RealmLive` not necessarily being in refreshed state due to potentially using update context --- osu.Game/Database/RealmLive.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 05367160f3..75d84d7cf1 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -51,7 +51,21 @@ namespace osu.Game.Database return; } - realmFactory.Run(realm => perform(realm.Find(ID))); + realmFactory.Run(realm => + { + var found = realm.Find(ID); + + if (found == null) + { + // It may be that we access this from the update thread before a refresh has taken place. + // To ensure that behaviour matches what we'd expect (the object *is* available), force + // a refresh to bring in any off-thread changes immediately. + realm.Refresh(); + found = realm.Find(ID); + } + + perform(found); + }); } /// From 81b5717ae793e334fdcf8efd72d20debfb49e9ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 17:33:03 +0900 Subject: [PATCH 08/14] Fix `RealmLive` failing to retrieve due to lack of refresh --- osu.Game/Database/RealmLive.cs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 75d84d7cf1..df5e165f8e 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -53,18 +53,7 @@ namespace osu.Game.Database realmFactory.Run(realm => { - var found = realm.Find(ID); - - if (found == null) - { - // It may be that we access this from the update thread before a refresh has taken place. - // To ensure that behaviour matches what we'd expect (the object *is* available), force - // a refresh to bring in any off-thread changes immediately. - realm.Refresh(); - found = realm.Find(ID); - } - - perform(found); + perform(retrieveFromID(realm, ID)); }); } @@ -79,7 +68,7 @@ namespace osu.Game.Database return realmFactory.Run(realm => { - var returnData = perform(realm.Find(ID)); + var returnData = perform(retrieveFromID(realm, ID)); if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}."); @@ -119,6 +108,22 @@ namespace osu.Game.Database } } + private T retrieveFromID(Realm realm, Guid id) + { + var found = realm.Find(ID); + + if (found == null) + { + // It may be that we access this from the update thread before a refresh has taken place. + // To ensure that behaviour matches what we'd expect (the object *is* available), force + // a refresh to bring in any off-thread changes immediately. + realm.Refresh(); + found = realm.Find(ID); + } + + return found; + } + public bool Equals(ILive? other) => ID == other?.ID; public override string ToString() => PerformRead(i => i.ToString()); From 495636538fc23101ec641bdf66eb0ba8b3f6a88c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 17:33:26 +0900 Subject: [PATCH 09/14] Add forced refresh on `GetAllUsableBeatmapSets()` This is commonly used in tests in a way where it's not feasible to guarantee correct results unless a refresh is called. This method shouldn't really be used outside of tests anyway, but that's for a folow-up effort. --- osu.Game/Beatmaps/BeatmapManager.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a9340e1250..43e4b482bd 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -171,7 +171,11 @@ namespace osu.Game.Beatmaps /// A list of available . public List GetAllUsableBeatmapSets() { - return contextFactory.Run(realm => realm.All().Where(b => !b.DeletePending).Detach()); + return contextFactory.Run(realm => + { + realm.Refresh(); + return realm.All().Where(b => !b.DeletePending).Detach(); + }); } /// From b23f4674b12251a94fd8bb0f054adcdc60f8abf2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 12:02:18 +0900 Subject: [PATCH 10/14] Update outdated exception message Co-authored-by: Salman Ahmed --- osu.Game/Database/RealmContextFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 888ffb1dd5..ea6a4b9636 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -72,7 +72,7 @@ namespace osu.Game.Database get { if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"Use {nameof(createContext)} when performing realm operations from a non-update thread"); + throw new InvalidOperationException(@$"Use {nameof(Run)}/{nameof(Write)} when performing realm operations from a non-update thread"); lock (contextLock) { From 7025191fdd45767321565cf28bfbb403b29148ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 12:02:44 +0900 Subject: [PATCH 11/14] Move target field outside of `Run` usage Co-authored-by: Salman Ahmed --- .../Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 9075dfefd4..2ee3372f80 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input List bindings = null; - realmFactory.Run(realm => bindings = realm.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).Detach()); + bindings = realmFactory.Run(realm => realm.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).Detach()); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { From a89954d67f0b1f3b77edab3b8b8f7e71c1db1de8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 12:12:13 +0900 Subject: [PATCH 12/14] Update benchmarks in line with new structure --- osu.Game.Benchmarks/BenchmarkRealmReads.cs | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game.Benchmarks/BenchmarkRealmReads.cs b/osu.Game.Benchmarks/BenchmarkRealmReads.cs index 5b5bdf595d..bb22fab51c 100644 --- a/osu.Game.Benchmarks/BenchmarkRealmReads.cs +++ b/osu.Game.Benchmarks/BenchmarkRealmReads.cs @@ -29,8 +29,10 @@ namespace osu.Game.Benchmarks realmFactory = new RealmContextFactory(storage, "client"); - using (var context = realmFactory.CreateContext()) - context.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo }))); + realmFactory.Run(realm => + { + realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo }))); + }); updateThread = new UpdateThread(() => { }, null); updateThread.Start(); @@ -39,15 +41,15 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkDirectPropertyRead() { - using (var context = realmFactory.CreateContext()) + realmFactory.Run(realm => { - var beatmapSet = context.All().First(); + var beatmapSet = realm.All().First(); for (int i = 0; i < ReadsPerFetch; i++) { string _ = beatmapSet.Beatmaps.First().Hash; } - } + }); } [Benchmark] @@ -78,15 +80,15 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkRealmLivePropertyRead() { - using (var context = realmFactory.CreateContext()) + realmFactory.Run(realm => { - var beatmapSet = context.All().First().ToLive(realmFactory); + var beatmapSet = realm.All().First().ToLive(realmFactory); for (int i = 0; i < ReadsPerFetch; i++) { string _ = beatmapSet.PerformRead(b => b.Beatmaps.First().Hash); } - } + }); } [Benchmark] @@ -117,15 +119,15 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkDetachedPropertyRead() { - using (var context = realmFactory.CreateContext()) + realmFactory.Run(realm => { - var beatmapSet = context.All().First().Detach(); + var beatmapSet = realm.All().First().Detach(); for (int i = 0; i < ReadsPerFetch; i++) { string _ = beatmapSet.Beatmaps.First().Hash; } - } + }); } [GlobalCleanup] From c9db0181d0611554f68e70ac4ed2da1fc96e9550 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 12:24:05 +0900 Subject: [PATCH 13/14] Attempt to fix test failures on windows due to context being held open --- osu.Game.Tests/Database/RealmLiveTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 2f16df4624..7b1cf763d6 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -47,6 +47,11 @@ namespace osu.Game.Tests.Database liveBeatmap = beatmap.ToLive(realmFactory); }); + using (realmFactory.BlockAllOperations()) + { + // recycle realm before migrating + } + using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) { migratedStorage.DeleteDirectory(string.Empty); From 25dbe6b27c092e5ce992e7cb4d7663cf3e8656df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 12:58:30 +0900 Subject: [PATCH 14/14] Fix unused null assignment --- .../Settings/Sections/Input/KeyBindingsSubsection.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 2ee3372f80..5b8a52240e 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -34,9 +34,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input { string rulesetName = Ruleset?.ShortName; - List bindings = null; - - bindings = realmFactory.Run(realm => realm.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).Detach()); + var bindings = realmFactory.Run(realm => realm.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant) + .Detach()); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) {