1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 06:03:08 +08:00

Merge branch 'master' into realm-integration/skins-rebase

This commit is contained in:
Dean Herbert 2021-11-30 19:59:59 +09:00
commit ef3d0ee0db
3 changed files with 65 additions and 85 deletions

View File

@ -62,53 +62,6 @@ namespace osu.Game.Tests.Database
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
} }
[Test]
public void TestValueAccessWithOpenContext()
{
RunTestWithRealm((realmFactory, _) =>
{
ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
// TODO: The commented code is the behaviour we hope to obtain, but is temporarily disabled.
// See https://github.com/ppy/osu/pull/15851
using (realmFactory.CreateContext())
{
Assert.Throws<InvalidOperationException>(() =>
{
var __ = liveBeatmap.Value;
});
}
// Assert.DoesNotThrow(() =>
// {
// using (realmFactory.CreateContext())
// {
// var resolved = liveBeatmap.Value;
//
// Assert.IsTrue(resolved.Realm.IsClosed);
// Assert.IsTrue(resolved.IsValid);
//
// // can access properties without a crash.
// Assert.IsFalse(resolved.Hidden);
// }
// });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test] [Test]
public void TestScopedReadWithoutContext() public void TestScopedReadWithoutContext()
{ {
@ -164,6 +117,60 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestValueAccessNonManaged()
{
RunTestWithRealm((realmFactory, _) =>
{
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
var liveBeatmap = beatmap.ToLive();
Assert.DoesNotThrow(() =>
{
var __ = liveBeatmap.Value;
});
});
}
[Test]
public void TestValueAccessWithOpenContextFails()
{
RunTestWithRealm((realmFactory, _) =>
{
ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
// Can't be used, without a valid context.
Assert.Throws<InvalidOperationException>(() =>
{
var __ = liveBeatmap.Value;
});
// Can't be used, even from within a valid context.
using (realmFactory.CreateContext())
{
Assert.Throws<InvalidOperationException>(() =>
{
var __ = liveBeatmap.Value;
});
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test] [Test]
public void TestValueAccessWithoutOpenContextFails() public void TestValueAccessWithoutOpenContextFails()
{ {
@ -225,8 +232,6 @@ namespace osu.Game.Tests.Database
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count()); Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(0, changesTriggered); Assert.AreEqual(0, changesTriggered);
// TODO: Originally the following was using `liveBeatmap.Value`. This has been temporarily disabled.
// See https://github.com/ppy/osu/pull/15851
liveBeatmap.PerformRead(resolved => liveBeatmap.PerformRead(resolved =>
{ {
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
@ -234,11 +239,6 @@ namespace osu.Game.Tests.Database
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count()); Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(1, changesTriggered); Assert.AreEqual(1, changesTriggered);
// TODO: as above, temporarily disabled as it doesn't make sense inside a `PerformRead`.
// // even though the realm that this instance was resolved for was closed, it's still valid.
// Assert.IsTrue(resolved.Realm.IsClosed);
// Assert.IsTrue(resolved.IsValid);
// can access properties without a crash. // can access properties without a crash.
Assert.IsFalse(resolved.Hidden); Assert.IsFalse(resolved.Hidden);

View File

@ -38,10 +38,10 @@ namespace osu.Game.Database
bool IsManaged { get; } bool IsManaged { get; }
/// <summary> /// <summary>
/// Resolve the value of this instance on the current thread's context. /// Resolve the value of this instance on the update thread.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// After resolving the data should not be passed between threads. /// After resolving, the data should not be passed between threads.
/// </remarks> /// </remarks>
T Value { get; } T Value { get; }
} }

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Threading; using osu.Framework.Development;
using Realms; using Realms;
#nullable enable #nullable enable
@ -19,9 +19,6 @@ namespace osu.Game.Database
public bool IsManaged => data.IsManaged; public bool IsManaged => data.IsManaged;
private readonly SynchronizationContext? fetchedContext;
private readonly int fetchedThreadId;
/// <summary> /// <summary>
/// The original live data used to create this instance. /// The original live data used to create this instance.
/// </summary> /// </summary>
@ -35,12 +32,6 @@ namespace osu.Game.Database
{ {
this.data = data; this.data = data;
if (data.IsManaged)
{
fetchedContext = SynchronizationContext.Current;
fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
}
ID = data.ID; ID = data.ID;
} }
@ -50,7 +41,7 @@ namespace osu.Game.Database
/// <param name="perform">The action to perform.</param> /// <param name="perform">The action to perform.</param>
public void PerformRead(Action<T> perform) public void PerformRead(Action<T> perform)
{ {
if (originalDataValid) if (!IsManaged)
{ {
perform(data); perform(data);
return; return;
@ -69,7 +60,7 @@ namespace osu.Game.Database
if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn))) if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}."); throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
if (originalDataValid) if (!IsManaged)
return perform(data); return perform(data);
using (var realm = Realm.GetInstance(data.Realm.Config)) using (var realm = Realm.GetInstance(data.Realm.Config))
@ -97,31 +88,20 @@ namespace osu.Game.Database
{ {
get get
{ {
if (originalDataValid) if (!IsManaged)
return data; return data;
if (!isCorrectThread) if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException($"Can't use {nameof(Value)} unless on the same thread the original data was fetched from."); throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads");
// When using Value, we rely on garbage collection for the realm instance used to retrieve the instance.
// As we are sure that this is on the update thread, there should always be an open and constantly refreshing realm instance to ensure file size growth is a non-issue.
var realm = Realm.GetInstance(data.Realm.Config); var realm = Realm.GetInstance(data.Realm.Config);
var retrieved = realm.Find<T>(ID);
if (!retrieved.IsValid) return realm.Find<T>(ID);
throw new InvalidOperationException("Attempted to access value without an open context");
return retrieved;
} }
} }
// TODO: Revisit adding these conditionals back as an optimisation: || (isCorrectThread && data.IsValid);
// They have temporarily been removed due to an oversight involving .AsQueryable, see https://github.com/realm/realm-dotnet/discussions/2734.
// This means we are fetching a new context every `PerformRead` or `PerformWrite`, even when on the correct thread.
private bool originalDataValid => !IsManaged;
// this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72)
private bool isCorrectThread
=> (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId;
public bool Equals(ILive<T>? other) => ID == other?.ID; public bool Equals(ILive<T>? other) => ID == other?.ID;
public override string ToString() => PerformRead(i => i.ToString()); public override string ToString() => PerformRead(i => i.ToString());