1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 16:32:54 +08:00

Merge pull request #13618 from peppy/fix-realm-state-change-crashes

Fix realm threading issues to make it releaseable
This commit is contained in:
Dan Balasescu 2021-06-28 19:54:49 +09:00 committed by GitHub
commit 8950757b61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 66 deletions

View File

@ -26,6 +26,11 @@ namespace osu.Game.Database
/// </summary> /// </summary>
private readonly object writeLock = new object(); private readonly object writeLock = new object();
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections.
/// </summary>
private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1);
private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Realm", "Get (Read)"); private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Realm", "Get (Read)");
private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)"); private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)");
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes");
@ -33,17 +38,12 @@ namespace osu.Game.Database
private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes"); private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes");
private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages"); private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages");
private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true);
private Realm context; private Realm context;
public Realm Context public Realm Context
{ {
get get
{ {
if (IsDisposed)
throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory");
if (context == null) if (context == null)
{ {
context = createContext(); context = createContext();
@ -64,7 +64,7 @@ namespace osu.Game.Database
public RealmUsage GetForRead() public RealmUsage GetForRead()
{ {
reads.Value++; reads.Value++;
return new RealmUsage(this); return new RealmUsage(createContext());
} }
public RealmWriteUsage GetForWrite() public RealmWriteUsage GetForWrite()
@ -73,8 +73,28 @@ namespace osu.Game.Database
pending_writes.Value++; pending_writes.Value++;
Monitor.Enter(writeLock); Monitor.Enter(writeLock);
return new RealmWriteUsage(createContext(), writeComplete);
}
return new RealmWriteUsage(this); /// <summary>
/// Flush any active contexts and block any further writes.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>An <see cref="IDisposable"/> which should be disposed to end the blocking section.</returns>
public IDisposable BlockAllOperations()
{
if (IsDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
blockingLock.Wait();
flushContexts();
return new InvokeOnDisposal<RealmContextFactory>(this, endBlockingSection);
static void endBlockingSection(RealmContextFactory factory) => factory.blockingLock.Release();
} }
protected override void Update() protected override void Update()
@ -87,15 +107,31 @@ namespace osu.Game.Database
private Realm createContext() private Realm createContext()
{ {
blockingResetEvent.Wait(); try
contexts_created.Value++;
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
{ {
SchemaVersion = schema_version, if (IsDisposed)
MigrationCallback = onMigration, 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) 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<RealmContextFactory>(this, r => endBlockingSection());
}
private void endBlockingSection()
{
blockingResetEvent.Set();
}
private void flushContexts() private void flushContexts()
{ {
var previousContext = context; var previousContext = context;
@ -141,6 +157,18 @@ namespace osu.Game.Database
previousContext?.Dispose(); 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);
}
/// <summary> /// <summary>
/// A usage of realm from an arbitrary thread. /// A usage of realm from an arbitrary thread.
/// </summary> /// </summary>
@ -148,13 +176,10 @@ namespace osu.Game.Database
{ {
public readonly Realm Realm; public readonly Realm Realm;
protected readonly RealmContextFactory Factory; internal RealmUsage(Realm context)
internal RealmUsage(RealmContextFactory factory)
{ {
active_usages.Value++; active_usages.Value++;
Factory = factory; Realm = context;
Realm = factory.createContext();
} }
/// <summary> /// <summary>
@ -172,11 +197,13 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public class RealmWriteUsage : RealmUsage public class RealmWriteUsage : RealmUsage
{ {
private readonly Action onWriteComplete;
private readonly Transaction transaction; private readonly Transaction transaction;
internal RealmWriteUsage(RealmContextFactory factory) internal RealmWriteUsage(Realm context, Action onWriteComplete)
: base(factory) : base(context)
{ {
this.onWriteComplete = onWriteComplete;
transaction = Realm.BeginWrite(); transaction = Realm.BeginWrite();
} }
@ -200,8 +227,7 @@ namespace osu.Game.Database
base.Dispose(); base.Dispose();
Monitor.Exit(Factory.writeLock); onWriteComplete();
pending_writes.Value--;
} }
} }
} }

View File

@ -24,6 +24,7 @@ using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Input; using osu.Game.Input;
@ -156,6 +157,8 @@ 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);
private IBindable<GameThreadState> updateThreadState;
public OsuGameBase() public OsuGameBase()
{ {
UseDevelopmentServer = DebugUtils.IsDebugBuild; UseDevelopmentServer = DebugUtils.IsDebugBuild;
@ -182,6 +185,10 @@ namespace osu.Game
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); dependencies.Cache(realmFactory = new RealmContextFactory(Storage));
updateThreadState = Host.UpdateThread.State.GetBoundCopy();
updateThreadState.BindValueChanged(updateThreadStateChanged);
AddInternal(realmFactory); AddInternal(realmFactory);
dependencies.CacheAs(Storage); dependencies.CacheAs(Storage);
@ -356,6 +363,23 @@ namespace osu.Game
Ruleset.BindValueChanged(onRulesetChanged); Ruleset.BindValueChanged(onRulesetChanged);
} }
private IDisposable blocking;
private void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state)
{
switch (state.NewValue)
{
case GameThreadState.Running:
blocking?.Dispose();
blocking = null;
break;
case GameThreadState.Paused:
blocking = realmFactory.BlockAllOperations();
break;
}
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();

View File

@ -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<RealmKeyBinding>().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 OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
@ -196,6 +174,7 @@ namespace osu.Game.Overlays.Toolbar
HoverBackground.FadeIn(200); HoverBackground.FadeIn(200);
tooltipContainer.FadeIn(100); tooltipContainer.FadeIn(100);
return base.OnHover(e); return base.OnHover(e);
} }
@ -222,6 +201,10 @@ namespace osu.Game.Overlays.Toolbar
private void updateKeyBindingTooltip() private void updateKeyBindingTooltip()
{ {
if (Hotkey == null) return;
var realmKeyBinding = realmFactory.Context.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value);
if (realmKeyBinding != null) if (realmKeyBinding != null)
{ {
var keyBindingString = realmKeyBinding.KeyCombination.ReadableString(); var keyBindingString = realmKeyBinding.KeyCombination.ReadableString();