diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index ed5931dd2b..71617b258d 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -26,6 +26,11 @@ namespace osu.Game.Database /// private readonly object writeLock = new object(); + /// + /// Lock object which is held during sections. + /// + private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1); + private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); @@ -33,17 +38,12 @@ namespace osu.Game.Database private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); private static readonly GlobalStatistic active_usages = GlobalStatistics.Get("Realm", "Active usages"); - private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true); - private Realm context; public Realm Context { get { - if (IsDisposed) - throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory"); - if (context == null) { context = createContext(); @@ -64,7 +64,7 @@ namespace osu.Game.Database public RealmUsage GetForRead() { reads.Value++; - return new RealmUsage(this); + return new RealmUsage(createContext()); } public RealmWriteUsage GetForWrite() @@ -73,8 +73,28 @@ namespace osu.Game.Database pending_writes.Value++; Monitor.Enter(writeLock); + return new RealmWriteUsage(createContext(), writeComplete); + } - return new RealmWriteUsage(this); + /// + /// Flush any active contexts and block any further writes. + /// + /// + /// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm. + /// ie. to move the realm backing file to a new location. + /// + /// An which should be disposed to end the blocking section. + public IDisposable BlockAllOperations() + { + if (IsDisposed) + throw new ObjectDisposedException(nameof(RealmContextFactory)); + + blockingLock.Wait(); + flushContexts(); + + return new InvokeOnDisposal(this, endBlockingSection); + + static void endBlockingSection(RealmContextFactory factory) => factory.blockingLock.Release(); } protected override void Update() @@ -87,15 +107,31 @@ namespace osu.Game.Database private Realm createContext() { - blockingResetEvent.Wait(); - - contexts_created.Value++; - - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + try { - SchemaVersion = schema_version, - MigrationCallback = onMigration, - }); + if (IsDisposed) + throw new ObjectDisposedException(nameof(RealmContextFactory)); + + blockingLock.Wait(); + + contexts_created.Value++; + + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + { + SchemaVersion = schema_version, + MigrationCallback = onMigration, + }); + } + finally + { + blockingLock.Release(); + } + } + + private void writeComplete() + { + Monitor.Exit(writeLock); + pending_writes.Value--; } private void onMigration(Migration migration, ulong lastSchemaVersion) @@ -109,26 +145,6 @@ namespace osu.Game.Database } } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - BlockAllOperations(); - } - - public IDisposable BlockAllOperations() - { - blockingResetEvent.Reset(); - flushContexts(); - - return new InvokeOnDisposal(this, r => endBlockingSection()); - } - - private void endBlockingSection() - { - blockingResetEvent.Set(); - } - private void flushContexts() { var previousContext = context; @@ -141,6 +157,18 @@ namespace osu.Game.Database previousContext?.Dispose(); } + protected override void Dispose(bool isDisposing) + { + if (!IsDisposed) + { + // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. + BlockAllOperations(); + blockingLock?.Dispose(); + } + + base.Dispose(isDisposing); + } + /// /// A usage of realm from an arbitrary thread. /// @@ -148,13 +176,10 @@ namespace osu.Game.Database { public readonly Realm Realm; - protected readonly RealmContextFactory Factory; - - internal RealmUsage(RealmContextFactory factory) + internal RealmUsage(Realm context) { active_usages.Value++; - Factory = factory; - Realm = factory.createContext(); + Realm = context; } /// @@ -172,11 +197,13 @@ namespace osu.Game.Database /// public class RealmWriteUsage : RealmUsage { + private readonly Action onWriteComplete; private readonly Transaction transaction; - internal RealmWriteUsage(RealmContextFactory factory) - : base(factory) + internal RealmWriteUsage(Realm context, Action onWriteComplete) + : base(context) { + this.onWriteComplete = onWriteComplete; transaction = Realm.BeginWrite(); } @@ -200,8 +227,7 @@ namespace osu.Game.Database base.Dispose(); - Monitor.Exit(Factory.writeLock); - pending_writes.Value--; + onWriteComplete(); } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 6195d8e1ea..bf1b449292 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -24,6 +24,7 @@ using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Logging; +using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Database; using osu.Game.Input; @@ -156,6 +157,8 @@ namespace osu.Game private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST); + private IBindable updateThreadState; + public OsuGameBase() { UseDevelopmentServer = DebugUtils.IsDebugBuild; @@ -182,6 +185,10 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); + + updateThreadState = Host.UpdateThread.State.GetBoundCopy(); + updateThreadState.BindValueChanged(updateThreadStateChanged); + AddInternal(realmFactory); dependencies.CacheAs(Storage); @@ -356,6 +363,23 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); } + private IDisposable blocking; + + private void updateThreadStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) + { + case GameThreadState.Running: + blocking?.Dispose(); + blocking = null; + break; + + case GameThreadState.Paused: + blocking = realmFactory.BlockAllOperations(); + break; + } + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 432c52c2e9..4a33f9e296 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -159,28 +159,6 @@ namespace osu.Game.Overlays.Toolbar }; } - private RealmKeyBinding realmKeyBinding; - - protected override void LoadComplete() - { - base.LoadComplete(); - - if (Hotkey == null) return; - - realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); - - if (realmKeyBinding != null) - { - realmKeyBinding.PropertyChanged += (sender, args) => - { - if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) - updateKeyBindingTooltip(); - }; - } - - updateKeyBindingTooltip(); - } - protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnClick(ClickEvent e) @@ -196,6 +174,7 @@ namespace osu.Game.Overlays.Toolbar HoverBackground.FadeIn(200); tooltipContainer.FadeIn(100); + return base.OnHover(e); } @@ -222,6 +201,10 @@ namespace osu.Game.Overlays.Toolbar private void updateKeyBindingTooltip() { + if (Hotkey == null) return; + + var realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); + if (realmKeyBinding != null) { var keyBindingString = realmKeyBinding.KeyCombination.ReadableString();