Merge pull request #16544 from peppy/realm-migration-ui

Show realm migration progress via a simple UI
This commit is contained in:
Dean Herbert 2022-01-22 23:24:14 +09:00 committed by GitHub
commit 1a1f9d9bec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 145 additions and 64 deletions

View File

@ -5,7 +5,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using osu.Framework.Development;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Statistics; using osu.Framework.Statistics;
@ -151,9 +150,6 @@ namespace osu.Game.Database
{ {
Logger.Log($"Creating full EF database backup at {backupFilename}", LoggingTarget.Database); Logger.Log($"Creating full EF database backup at {backupFilename}", LoggingTarget.Database);
if (DebugUtils.IsDebugBuild)
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
using (var source = storage.GetStream(DATABASE_NAME)) using (var source = storage.GetStream(DATABASE_NAME))
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew)) using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
source.CopyTo(destination); source.CopyTo(destination);

View File

@ -1,43 +1,102 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using Realms; using Realms;
#nullable enable #nullable enable
namespace osu.Game.Database namespace osu.Game.Database
{ {
internal class EFToRealmMigrator internal class EFToRealmMigrator : CompositeDrawable
{ {
private readonly DatabaseContextFactory efContextFactory; public bool FinishedMigrating { get; private set; }
private readonly RealmContextFactory realmContextFactory;
private readonly OsuConfigManager config;
private readonly Storage storage;
public EFToRealmMigrator(DatabaseContextFactory efContextFactory, RealmContextFactory realmContextFactory, OsuConfigManager config, Storage storage) [Resolved]
private DatabaseContextFactory efContextFactory { get; set; } = null!;
[Resolved]
private RealmContextFactory realmContextFactory { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private readonly OsuSpriteText currentOperationText;
public EFToRealmMigrator()
{ {
this.efContextFactory = efContextFactory; RelativeSizeAxes = Axes.Both;
this.realmContextFactory = realmContextFactory;
this.config = config; InternalChildren = new Drawable[]
this.storage = storage; {
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(10),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Database migration in progress",
Font = OsuFont.Default.With(size: 40)
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "This could take a few minutes depending on the speed of your disk(s).",
Font = OsuFont.Default.With(size: 30)
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Please keep the window open until this completes!",
Font = OsuFont.Default.With(size: 30)
},
new LoadingSpinner(true)
{
State = { Value = Visibility.Visible }
},
currentOperationText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 30)
},
}
},
};
} }
public void Run() protected override void LoadComplete()
{ {
createBackup(); base.LoadComplete();
Task.Factory.StartNew(() =>
{
using (var ef = efContextFactory.Get()) using (var ef = efContextFactory.Get())
{ {
migrateSettings(ef); migrateSettings(ef);
@ -48,8 +107,21 @@ namespace osu.Game.Database
// Delete the database permanently. // Delete the database permanently.
// Will cause future startups to not attempt migration. // Will cause future startups to not attempt migration.
Logger.Log("Migration successful, deleting EF database", LoggingTarget.Database); log("Migration successful, deleting EF database");
efContextFactory.ResetDatabase(); efContextFactory.ResetDatabase();
if (DebugUtils.IsDebugBuild)
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
{
FinishedMigrating = true;
});
}
private void log(string message)
{
Logger.Log(message, LoggingTarget.Database);
Scheduler.AddOnce(m => currentOperationText.Text = m, message);
} }
private void migrateBeatmaps(OsuDbContext ef) private void migrateBeatmaps(OsuDbContext ef)
@ -62,12 +134,12 @@ namespace osu.Game.Database
.Include(s => s.Files).ThenInclude(f => f.FileInfo) .Include(s => s.Files).ThenInclude(f => f.FileInfo)
.Include(s => s.Metadata); .Include(s => s.Metadata);
Logger.Log("Beginning beatmaps migration to realm", LoggingTarget.Database); log("Beginning beatmaps migration to realm");
// previous entries in EF are removed post migration. // previous entries in EF are removed post migration.
if (!existingBeatmapSets.Any()) if (!existingBeatmapSets.Any())
{ {
Logger.Log("No beatmaps found to migrate", LoggingTarget.Database); log("No beatmaps found to migrate");
return; return;
} }
@ -75,13 +147,13 @@ namespace osu.Game.Database
realmContextFactory.Run(realm => realmContextFactory.Run(realm =>
{ {
Logger.Log($"Found {count} beatmaps in EF", LoggingTarget.Database); log($"Found {count} beatmaps in EF");
// only migrate data if the realm database is empty. // only migrate data if the realm database is empty.
// note that this cannot be written as: `realm.All<BeatmapSetInfo>().All(s => s.Protected)`, because realm does not support `.All()`. // note that this cannot be written as: `realm.All<BeatmapSetInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
if (realm.All<BeatmapSetInfo>().Any(s => !s.Protected)) if (realm.All<BeatmapSetInfo>().Any(s => !s.Protected))
{ {
Logger.Log("Skipping migration as realm already has beatmaps loaded", LoggingTarget.Database); log("Skipping migration as realm already has beatmaps loaded");
} }
else else
{ {
@ -96,7 +168,7 @@ namespace osu.Game.Database
{ {
transaction.Commit(); transaction.Commit();
transaction = realm.BeginWrite(); transaction = realm.BeginWrite();
Logger.Log($"Migrated {written}/{count} beatmaps...", LoggingTarget.Database); log($"Migrated {written}/{count} beatmaps...");
} }
var realmBeatmapSet = new BeatmapSetInfo var realmBeatmapSet = new BeatmapSetInfo
@ -156,7 +228,7 @@ namespace osu.Game.Database
transaction.Commit(); transaction.Commit();
} }
Logger.Log($"Successfully migrated {count} beatmaps to realm", LoggingTarget.Database); log($"Successfully migrated {count} beatmaps to realm");
} }
}); });
} }
@ -193,12 +265,12 @@ namespace osu.Game.Database
.Include(s => s.Files) .Include(s => s.Files)
.ThenInclude(f => f.FileInfo); .ThenInclude(f => f.FileInfo);
Logger.Log("Beginning scores migration to realm", LoggingTarget.Database); log("Beginning scores migration to realm");
// previous entries in EF are removed post migration. // previous entries in EF are removed post migration.
if (!existingScores.Any()) if (!existingScores.Any())
{ {
Logger.Log("No scores found to migrate", LoggingTarget.Database); log("No scores found to migrate");
return; return;
} }
@ -206,12 +278,12 @@ namespace osu.Game.Database
realmContextFactory.Run(realm => realmContextFactory.Run(realm =>
{ {
Logger.Log($"Found {count} scores in EF", LoggingTarget.Database); log($"Found {count} scores in EF");
// only migrate data if the realm database is empty. // only migrate data if the realm database is empty.
if (realm.All<ScoreInfo>().Any()) if (realm.All<ScoreInfo>().Any())
{ {
Logger.Log("Skipping migration as realm already has scores loaded", LoggingTarget.Database); log("Skipping migration as realm already has scores loaded");
} }
else else
{ {
@ -226,7 +298,7 @@ namespace osu.Game.Database
{ {
transaction.Commit(); transaction.Commit();
transaction = realm.BeginWrite(); transaction = realm.BeginWrite();
Logger.Log($"Migrated {written}/{count} scores...", LoggingTarget.Database); log($"Migrated {written}/{count} scores...");
} }
var beatmap = realm.All<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash); var beatmap = realm.All<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash);
@ -270,7 +342,7 @@ namespace osu.Game.Database
transaction.Commit(); transaction.Commit();
} }
Logger.Log($"Successfully migrated {count} scores to realm", LoggingTarget.Database); log($"Successfully migrated {count} scores to realm");
} }
}); });
} }
@ -309,7 +381,7 @@ namespace osu.Game.Database
// note that this cannot be written as: `realm.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`. // note that this cannot be written as: `realm.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
if (!realm.All<SkinInfo>().Any(s => !s.Protected)) if (!realm.All<SkinInfo>().Any(s => !s.Protected))
{ {
Logger.Log($"Migrating {existingSkins.Count} skins", LoggingTarget.Database); log($"Migrating {existingSkins.Count} skins");
foreach (var skin in existingSkins) foreach (var skin in existingSkins)
{ {
@ -358,7 +430,7 @@ namespace osu.Game.Database
if (!existingSettings.Any()) if (!existingSettings.Any())
return; return;
Logger.Log("Beginning settings migration to realm", LoggingTarget.Database); log("Beginning settings migration to realm");
realmContextFactory.Run(realm => realmContextFactory.Run(realm =>
{ {
@ -367,7 +439,7 @@ namespace osu.Game.Database
// only migrate data if the realm database is empty. // only migrate data if the realm database is empty.
if (!realm.All<RealmRulesetSetting>().Any()) if (!realm.All<RealmRulesetSetting>().Any())
{ {
Logger.Log($"Migrating {existingSettings.Count} settings", LoggingTarget.Database); log($"Migrating {existingSettings.Count} settings");
foreach (var dkb in existingSettings) foreach (var dkb in existingSettings)
{ {
@ -396,17 +468,5 @@ namespace osu.Game.Database
private string? getRulesetShortNameFromLegacyID(long rulesetId) => private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName; efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
private void createBackup()
{
string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
efContextFactory.CreateBackup($"client.{migration}.db");
realmContextFactory.CreateBackup($"client.{migration}.realm");
using (var source = storage.GetStream("collection.db"))
using (var destination = storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew))
source.CopyTo(destination);
}
} }
} }

View File

@ -161,6 +161,11 @@ namespace osu.Game
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(global_track_volume_adjust); private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(global_track_volume_adjust);
/// <summary>
/// A legacy EF context factory if migration has not been performed to realm yet.
/// </summary>
protected DatabaseContextFactory EFContextFactory { get; private set; }
public OsuGameBase() public OsuGameBase()
{ {
UseDevelopmentServer = DebugUtils.IsDebugBuild; UseDevelopmentServer = DebugUtils.IsDebugBuild;
@ -184,18 +189,28 @@ namespace osu.Game
Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly));
DatabaseContextFactory efContextFactory = Storage.Exists(DatabaseContextFactory.DATABASE_NAME) if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
? new DatabaseContextFactory(Storage) dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage));
: null;
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", efContextFactory)); dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", EFContextFactory));
dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage)); dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage));
dependencies.CacheAs<IRulesetStore>(RulesetStore); dependencies.CacheAs<IRulesetStore>(RulesetStore);
// A non-null context factory means there's still content to migrate. // Backup is taken here rather than in EFToRealmMigrator to avoid recycling realm contexts
if (efContextFactory != null) // after initial usages below. It can be moved once a direction is established for handling re-subscription.
new EFToRealmMigrator(efContextFactory, realmFactory, LocalConfig, Storage).Run(); // See https://github.com/ppy/osu/pull/16547 for more discussion.
if (EFContextFactory != null)
{
string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
EFContextFactory.CreateBackup($"client.{migration}.db");
realmFactory.CreateBackup($"client.{migration}.realm");
using (var source = Storage.GetStream("collection.db"))
using (var destination = Storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew))
source.CopyTo(destination);
}
dependencies.CacheAs(Storage); dependencies.CacheAs(Storage);

View File

@ -12,6 +12,7 @@ using osu.Game.Screens.Menu;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using IntroSequence = osu.Game.Configuration.IntroSequence; using IntroSequence = osu.Game.Configuration.IntroSequence;
@ -63,6 +64,11 @@ namespace osu.Game.Screens
protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler(); protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler();
[Resolved(canBeNull: true)]
private DatabaseContextFactory efContextFactory { get; set; }
private EFToRealmMigrator realmMigrator;
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);
@ -70,6 +76,10 @@ namespace osu.Game.Screens
LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal); LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal);
LoadComponentAsync(loadableScreen = CreateLoadableScreen()); LoadComponentAsync(loadableScreen = CreateLoadableScreen());
// A non-null context factory means there's still content to migrate.
if (efContextFactory != null)
LoadComponentAsync(realmMigrator = new EFToRealmMigrator(), AddInternal);
LoadComponentAsync(spinner = new LoadingSpinner(true, true) LoadComponentAsync(spinner = new LoadingSpinner(true, true)
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
@ -86,7 +96,7 @@ namespace osu.Game.Screens
private void checkIfLoaded() private void checkIfLoaded()
{ {
if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling) if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling || realmMigrator?.FinishedMigrating == false)
{ {
Schedule(checkIfLoaded); Schedule(checkIfLoaded);
return; return;