Fix potential deadlocking behaviour (and convert ResetEvent to Semaphore)

This commit is contained in:
Dean Herbert 2021-06-24 14:37:26 +09:00
parent c85edd2b55
commit 63ab40ec24

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,8 +38,6 @@ 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
@ -64,7 +67,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 +76,13 @@ 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); private void writeComplete()
{
Monitor.Exit(writeLock);
pending_writes.Value--;
} }
protected override void Update() protected override void Update()
@ -87,15 +95,22 @@ 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, blockingLock.Wait();
MigrationCallback = onMigration,
}); contexts_created.Value++;
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
{
SchemaVersion = schema_version,
MigrationCallback = onMigration,
});
}
finally
{
blockingLock.Release();
}
} }
private void onMigration(Migration migration, ulong lastSchemaVersion) private void onMigration(Migration migration, ulong lastSchemaVersion)
@ -113,21 +128,23 @@ namespace osu.Game.Database
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
BlockAllOperations(); // In the standard case, operations will already be blocked by the Update thread "pausing" from GameHost exit.
// This avoids waiting (forever) on an already entered semaphore.
if (context != null || active_usages.Value > 0)
BlockAllOperations();
blockingLock?.Dispose();
} }
public IDisposable BlockAllOperations() public IDisposable BlockAllOperations()
{ {
blockingResetEvent.Reset(); blockingLock.Wait();
flushContexts(); flushContexts();
return new InvokeOnDisposal<RealmContextFactory>(this, r => endBlockingSection()); return new InvokeOnDisposal<RealmContextFactory>(this, endBlockingSection);
} }
private void endBlockingSection() private static void endBlockingSection(RealmContextFactory factory) => factory.blockingLock.Release();
{
blockingResetEvent.Set();
}
private void flushContexts() private void flushContexts()
{ {
@ -148,13 +165,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 +186,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 +216,7 @@ namespace osu.Game.Database
base.Dispose(); base.Dispose();
Monitor.Exit(Factory.writeLock); onWriteComplete();
pending_writes.Value--;
} }
} }
} }