From d2062ff97fda0507ca1a1c12f853eb6713f548c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 15:23:16 +0900 Subject: [PATCH 1/8] Reformat realm migrations list for legibility --- osu.Game/Database/RealmContextFactory.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0bb46a379a..e773ea9767 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -33,10 +33,10 @@ namespace osu.Game.Database /// /// Version history: - /// 6 First tracked version (~20211018) - /// 7 Changed OnlineID fields to non-nullable to add indexing support (20211018) - /// 8 Rebind scroll adjust keys to not have control modifier (20211029) - /// 9 Converted BeatmapMetadata.Author from string to RealmUser (20211104) + /// 6 ~2021-10-18 First tracked version. + /// 7 2021-10-18 Changed OnlineID fields to non-nullable to add indexing support. + /// 8 2021-10-29 Rebind scroll adjust keys to not have control modifier. + /// 9 2021-11-04 Converted BeatmapMetadata.Author from string to RealmUser. /// private const int schema_version = 9; From ca26b6c5408309256ebbc4d89324cc52d472823a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 18:51:37 +0900 Subject: [PATCH 2/8] Provide `RealmContextFactory` with the EF `RulesetStore` for migration purposes --- osu.Game/Database/RealmContextFactory.cs | 6 +++++- osu.Game/OsuGameBase.cs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index e773ea9767..1d0da3ea5c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -13,6 +13,7 @@ using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Game.Input.Bindings; using osu.Game.Models; +using osu.Game.Rulesets; using Realms; #nullable enable @@ -31,6 +32,8 @@ namespace osu.Game.Database /// public readonly string Filename; + private readonly RulesetStore? rulesets; + /// /// Version history: /// 6 ~2021-10-18 First tracked version. @@ -72,9 +75,10 @@ namespace osu.Game.Database } } - public RealmContextFactory(Storage storage, string filename) + public RealmContextFactory(Storage storage, string filename, RulesetStore? rulesets = null) { this.storage = storage; + this.rulesets = rulesets; Filename = filename; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e207d9ce3b..3b2f397a72 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -187,8 +187,9 @@ namespace osu.Game Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client")); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", RulesetStore)); dependencies.CacheAs(Storage); @@ -227,7 +228,6 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() From 329bae50b0e99b3450fc84daa9f3a4828c433c11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 18:07:28 +0900 Subject: [PATCH 3/8] Switch realm ruleset configuration to use ruleset's `ShortName` as key --- osu.Game/Configuration/RealmRulesetSetting.cs | 9 ++----- osu.Game/Database/RealmContextFactory.cs | 26 ++++++++++++++++++- osu.Game/OsuGameBase.cs | 3 ++- .../Configuration/RulesetConfigManager.cs | 13 ++++------ 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/osu.Game/Configuration/RealmRulesetSetting.cs b/osu.Game/Configuration/RealmRulesetSetting.cs index 07e56ad8dd..3fea35ee9d 100644 --- a/osu.Game/Configuration/RealmRulesetSetting.cs +++ b/osu.Game/Configuration/RealmRulesetSetting.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osu.Game.Database; using Realms; #nullable enable @@ -10,13 +8,10 @@ using Realms; namespace osu.Game.Configuration { [MapTo(@"RulesetSetting")] - public class RealmRulesetSetting : RealmObject, IHasGuidPrimaryKey + public class RealmRulesetSetting : RealmObject { - [PrimaryKey] - public Guid ID { get; set; } = Guid.NewGuid(); - [Indexed] - public int RulesetID { get; set; } + public string RulesetName { get; set; } = string.Empty; [Indexed] public int Variant { get; set; } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 1d0da3ea5c..70ea24b581 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Models; using osu.Game.Rulesets; @@ -40,8 +41,9 @@ namespace osu.Game.Database /// 7 2021-10-18 Changed OnlineID fields to non-nullable to add indexing support. /// 8 2021-10-29 Rebind scroll adjust keys to not have control modifier. /// 9 2021-11-04 Converted BeatmapMetadata.Author from string to RealmUser. + /// 10 2021-11-22 Use ShortName instead of RulesetID for ruleset settings. /// - private const int schema_version = 9; + private const int schema_version = 10; /// /// Lock object which is held during sections, blocking context creation during blocking periods. @@ -236,6 +238,28 @@ namespace osu.Game.Database }; } + break; + + case 10: + string rulesetSettingClassName = getMappedOrOriginalName(typeof(RealmRulesetSetting)); + + var oldSettings = migration.OldRealm.DynamicApi.All(rulesetSettingClassName); + var newSettings = migration.NewRealm.All().ToList(); + + for (int i = 0; i < newSettings.Count; i++) + { + dynamic? oldItem = oldSettings.ElementAt(i); + var newItem = newSettings.ElementAt(i); + + long rulesetId = oldItem.RulesetID; + string? rulesetName = rulesets?.GetRuleset((int)rulesetId)?.ShortName; + + if (string.IsNullOrEmpty(rulesetName)) + migration.NewRealm.Remove(newItem); + else + newItem.RulesetName = rulesetName; + } + break; } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3b2f397a72..d8050353c5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -456,7 +456,8 @@ namespace osu.Game { Key = dkb.Key, Value = dkb.StringValue, - RulesetID = dkb.RulesetID.Value, + // important: this RulesetStore must be the EF one. + RulesetName = RulesetStore.GetRuleset(dkb.RulesetID.Value).ShortName, Variant = dkb.Variant ?? 0, }); } diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index eec71a3623..17678775e9 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -20,16 +20,13 @@ namespace osu.Game.Rulesets.Configuration private List databasedSettings = new List(); - private readonly int rulesetId; + private readonly string rulesetName; protected RulesetConfigManager(SettingsStore store, RulesetInfo ruleset, int? variant = null) { realmFactory = store?.Realm; - if (realmFactory != null && !ruleset.ID.HasValue) - throw new InvalidOperationException("Attempted to add databased settings for a non-databased ruleset"); - - rulesetId = ruleset.ID ?? -1; + rulesetName = ruleset.ShortName; this.variant = variant ?? 0; @@ -43,7 +40,7 @@ namespace osu.Game.Rulesets.Configuration if (realmFactory != null) { // As long as RulesetConfigCache exists, there is no need to subscribe to realm events. - databasedSettings = realmFactory.Context.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + databasedSettings = realmFactory.Context.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).ToList(); } } @@ -68,7 +65,7 @@ namespace osu.Game.Rulesets.Configuration { foreach (var c in changed) { - var setting = realm.All().First(s => s.RulesetID == rulesetId && 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(); } @@ -94,7 +91,7 @@ namespace osu.Game.Rulesets.Configuration { Key = lookup.ToString(), Value = bindable.Value.ToString(), - RulesetID = rulesetId, + RulesetName = rulesetName, Variant = variant, }; From 1f321e29107559f3e099ea9acf9f79988be94dd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 11:48:56 +0900 Subject: [PATCH 4/8] Run EF migrations earlier to ensure it is complete before usage --- osu.Game/OsuGameBase.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d8050353c5..a2dd417491 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -187,6 +187,9 @@ namespace osu.Game Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); + + runMigrations(); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", RulesetStore)); @@ -204,8 +207,6 @@ namespace osu.Game Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; - runMigrations(); - dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Resources, Audio)); dependencies.CacheAs(SkinManager); From 505fede44d93ff68c5ad150d1641b25143bef701 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 16:27:28 +0900 Subject: [PATCH 5/8] Pass the full EF context rather than a legacy `RulesetStore` --- osu.Game/Database/RealmContextFactory.cs | 18 +++++++++++++----- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 70ea24b581..f04492d0e8 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -14,7 +14,6 @@ using osu.Framework.Statistics; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Models; -using osu.Game.Rulesets; using Realms; #nullable enable @@ -33,7 +32,7 @@ namespace osu.Game.Database /// public readonly string Filename; - private readonly RulesetStore? rulesets; + private readonly IDatabaseContextFactory? efContextFactory; /// /// Version history: @@ -77,10 +76,10 @@ namespace osu.Game.Database } } - public RealmContextFactory(Storage storage, string filename, RulesetStore? rulesets = null) + public RealmContextFactory(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null) { this.storage = storage; - this.rulesets = rulesets; + this.efContextFactory = efContextFactory; Filename = filename; @@ -252,7 +251,7 @@ namespace osu.Game.Database var newItem = newSettings.ElementAt(i); long rulesetId = oldItem.RulesetID; - string? rulesetName = rulesets?.GetRuleset((int)rulesetId)?.ShortName; + string? rulesetName = getRulesetShortNameFromLegacyID(rulesetId); if (string.IsNullOrEmpty(rulesetName)) migration.NewRealm.Remove(newItem); @@ -264,6 +263,15 @@ namespace osu.Game.Database } } + private string? getRulesetShortNameFromLegacyID(long rulesetId) + { + if (efContextFactory == null) + return null; + + using (var efContext = efContextFactory.Get()) + return efContext.RulesetInfo.First(r => r.ID == rulesetId)?.ShortName; + } + /// /// Flush any active contexts and block any further writes. /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a2dd417491..1890fd7d24 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -192,7 +192,7 @@ namespace osu.Game dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", RulesetStore)); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory)); dependencies.CacheAs(Storage); From b1b67238268bed5945aa1ae90d21dac16629114a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 17:47:43 +0900 Subject: [PATCH 6/8] Add xmldoc and verbatim string markers --- osu.Game/Database/RealmContextFactory.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index f04492d0e8..3949494766 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -49,8 +49,8 @@ namespace osu.Game.Database /// private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); - private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); - private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); + private static readonly GlobalStatistic refreshes = GlobalStatistics.Get(@"Realm", @"Dirty Refreshes"); + private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); private readonly object contextLock = new object(); private Realm? context; @@ -60,14 +60,14 @@ 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(); - Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); + Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); } // creating a context will ensure our schema is up-to-date and migrated. @@ -76,6 +76,12 @@ namespace osu.Game.Database } } + /// + /// Construct a new instance of a realm context factory. + /// + /// The game storage which will be used to create the realm backing file. + /// The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified. + /// An EF factory used only for migration purposes. public RealmContextFactory(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null) { this.storage = storage; @@ -83,7 +89,7 @@ namespace osu.Game.Database Filename = filename; - const string realm_extension = ".realm"; + const string realm_extension = @".realm"; if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) Filename += realm_extension; @@ -286,7 +292,7 @@ namespace osu.Game.Database throw new ObjectDisposedException(nameof(RealmContextFactory)); if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread."); + throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); @@ -310,7 +316,7 @@ namespace osu.Game.Database timeout -= sleep_length; if (timeout < 0) - throw new TimeoutException("Took too long to acquire lock"); + throw new TimeoutException(@"Took too long to acquire lock"); } } catch From 40cd998f997508a7f03f58d3d0d32c1283fb84ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 18:13:05 +0900 Subject: [PATCH 7/8] Fix incorrect disposal --- osu.Game/Database/RealmContextFactory.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 3949494766..d0d176c780 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -269,14 +269,8 @@ namespace osu.Game.Database } } - private string? getRulesetShortNameFromLegacyID(long rulesetId) - { - if (efContextFactory == null) - return null; - - using (var efContext = efContextFactory.Get()) - return efContext.RulesetInfo.First(r => r.ID == rulesetId)?.ShortName; - } + private string? getRulesetShortNameFromLegacyID(long rulesetId) => + efContextFactory?.Get().RulesetInfo.First(r => r.ID == rulesetId)?.ShortName; /// /// Flush any active contexts and block any further writes. From 6b134359c932e7c0bda1b6bc129ee7e539b8e7e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 19:15:52 +0900 Subject: [PATCH 8/8] Handle the case where the ruleset isn't found in the EF database any more --- 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 d0d176c780..0e602da190 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -270,7 +270,7 @@ namespace osu.Game.Database } private string? getRulesetShortNameFromLegacyID(long rulesetId) => - efContextFactory?.Get().RulesetInfo.First(r => r.ID == rulesetId)?.ShortName; + efContextFactory?.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName; /// /// Flush any active contexts and block any further writes.