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:
commit
8950757b61
@ -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--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
Loading…
Reference in New Issue
Block a user