Merge pull request #13767 from peppy/fix-realm-refresh-race

Fix thread safety of realm `Refresh` operation
This commit is contained in:
Dan Balasescu
2021-07-05 13:53:29 +09:00
committed by GitHub
3 changed files with 64 additions and 32 deletions

View File

@ -38,19 +38,28 @@ namespace osu.Game.Tests.Database
[Test]
public void TestDefaultsPopulationAndQuery()
{
Assert.That(query().Count, Is.EqualTo(0));
Assert.That(queryCount(), Is.EqualTo(0));
KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer);
Assert.That(query().Count, Is.EqualTo(3));
Assert.That(queryCount(), Is.EqualTo(3));
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1));
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2));
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1));
Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2));
}
private IQueryable<RealmKeyBinding> query() => realmContextFactory.Context.All<RealmKeyBinding>();
private int queryCount(GlobalAction? match = null)
{
using (var usage = realmContextFactory.GetForRead())
{
var results = usage.Realm.All<RealmKeyBinding>();
if (match.HasValue)
results = results.Where(k => k.ActionInt == (int)match.Value);
return results.Count();
}
}
[Test]
public void TestUpdateViaQueriedReference()
@ -59,7 +68,9 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer);
var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
using (var primaryUsage = realmContextFactory.GetForRead())
{
var backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
@ -76,9 +87,10 @@ namespace osu.Game.Tests.Database
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
// check still correct after re-query.
backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
}
[TearDown]
public void TearDown()

View File

@ -9,6 +9,7 @@ namespace osu.Game.Database
{
/// <summary>
/// The main realm context, bound to the update thread.
/// If querying from a non-update thread is needed, use <see cref="GetForRead"/> or <see cref="GetForWrite"/> to receive a context instead.
/// </summary>
Realm Context { get; }

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
@ -38,11 +40,18 @@ namespace osu.Game.Database
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 readonly object updateContextLock = new object();
private Realm context;
public Realm Context
{
get
{
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException($"Use {nameof(GetForRead)} or {nameof(GetForWrite)} when performing realm operations from a non-update thread");
lock (updateContextLock)
{
if (context == null)
{
@ -55,6 +64,7 @@ namespace osu.Game.Database
return context;
}
}
}
public RealmContextFactory(Storage storage)
{
@ -107,9 +117,12 @@ namespace osu.Game.Database
{
base.Update();
lock (updateContextLock)
{
if (context?.Refresh() == true)
refreshes.Value++;
}
}
private Realm createContext()
{
@ -154,9 +167,15 @@ namespace osu.Game.Database
private void flushContexts()
{
Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database);
Debug.Assert(blockingLock.CurrentCount == 0);
var previousContext = context;
Realm previousContext;
lock (updateContextLock)
{
previousContext = context;
context = null;
}
// wait for all threaded usages to finish
while (active_usages.Value > 0)