mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 13:37:25 +08:00
Merge pull request #15042 from peppy/realm-live
Add realm `Live<T>` implementation
This commit is contained in:
commit
a781930135
213
osu.Game.Tests/Database/RealmLiveTests.cs
Normal file
213
osu.Game.Tests/Database/RealmLiveTests.cs
Normal file
@ -0,0 +1,213 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
public class RealmLiveTests : RealmTest
|
||||
{
|
||||
[Test]
|
||||
public void TestLiveCastability()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
|
||||
|
||||
ILive<IBeatmapInfo> iBeatmap = beatmap;
|
||||
|
||||
Assert.AreEqual(0, iBeatmap.Value.Length);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueAccessWithOpenContext()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<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(() =>
|
||||
{
|
||||
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]
|
||||
public void TestScopedReadWithoutContext()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<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(() =>
|
||||
{
|
||||
liveBeatmap.PerformRead(beatmap =>
|
||||
{
|
||||
Assert.IsTrue(beatmap.IsValid);
|
||||
Assert.IsFalse(beatmap.Hidden);
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScopedWriteWithoutContext()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<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(() =>
|
||||
{
|
||||
liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; });
|
||||
liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); });
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueAccessWithoutOpenContextFails()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<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(() =>
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
var unused = liveBeatmap.Value;
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLiveAssumptions()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
int changesTriggered = 0;
|
||||
|
||||
using (var updateThreadContext = realmFactory.CreateContext())
|
||||
{
|
||||
updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange);
|
||||
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
{
|
||||
var ruleset = CreateRuleset();
|
||||
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||
|
||||
// add a second beatmap to ensure that a full refresh occurs below.
|
||||
// not just a refresh from the resolved Live.
|
||||
threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive();
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
|
||||
Debug.Assert(liveBeatmap != null);
|
||||
|
||||
// not yet seen by main context
|
||||
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
|
||||
Assert.AreEqual(0, changesTriggered);
|
||||
|
||||
var resolved = liveBeatmap.Value;
|
||||
|
||||
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
|
||||
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
|
||||
Assert.AreEqual(1, changesTriggered);
|
||||
|
||||
// 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.
|
||||
Assert.IsFalse(resolved.Hidden);
|
||||
|
||||
updateThreadContext.Write(r =>
|
||||
{
|
||||
// can use with the main context.
|
||||
r.Remove(resolved);
|
||||
});
|
||||
}
|
||||
|
||||
void gotChange(IRealmCollection<RealmBeatmap> sender, ChangeSet changes, Exception error)
|
||||
{
|
||||
changesTriggered++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
111
osu.Game/Database/RealmLive.cs
Normal file
111
osu.Game/Database/RealmLive.cs
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a method of working with realm objects over longer application lifetimes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The underlying object type.</typeparam>
|
||||
public class RealmLive<T> : ILive<T> where T : RealmObject, IHasGuidPrimaryKey
|
||||
{
|
||||
public Guid ID { get; }
|
||||
|
||||
private readonly SynchronizationContext? fetchedContext;
|
||||
private readonly int fetchedThreadId;
|
||||
|
||||
/// <summary>
|
||||
/// The original live data used to create this instance.
|
||||
/// </summary>
|
||||
private readonly T data;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new instance of live realm data.
|
||||
/// </summary>
|
||||
/// <param name="data">The realm data.</param>
|
||||
public RealmLive(T data)
|
||||
{
|
||||
this.data = data;
|
||||
|
||||
fetchedContext = SynchronizationContext.Current;
|
||||
fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
ID = data.ID;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a read operation on this live object.
|
||||
/// </summary>
|
||||
/// <param name="perform">The action to perform.</param>
|
||||
public void PerformRead(Action<T> perform)
|
||||
{
|
||||
if (originalDataValid)
|
||||
{
|
||||
perform(data);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var realm = Realm.GetInstance(data.Realm.Config))
|
||||
perform(realm.Find<T>(ID));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a read operation on this live object.
|
||||
/// </summary>
|
||||
/// <param name="perform">The action to perform.</param>
|
||||
public TReturn PerformRead<TReturn>(Func<T, TReturn> perform)
|
||||
{
|
||||
if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
|
||||
throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
|
||||
|
||||
if (originalDataValid)
|
||||
return perform(data);
|
||||
|
||||
using (var realm = Realm.GetInstance(data.Realm.Config))
|
||||
return perform(realm.Find<T>(ID));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a write operation on this live object.
|
||||
/// </summary>
|
||||
/// <param name="perform">The action to perform.</param>
|
||||
public void PerformWrite(Action<T> perform) =>
|
||||
PerformRead(t =>
|
||||
{
|
||||
var transaction = t.Realm.BeginWrite();
|
||||
perform(t);
|
||||
transaction.Commit();
|
||||
});
|
||||
|
||||
public T Value
|
||||
{
|
||||
get
|
||||
{
|
||||
if (originalDataValid)
|
||||
return data;
|
||||
|
||||
T retrieved;
|
||||
|
||||
using (var realm = Realm.GetInstance(data.Realm.Config))
|
||||
retrieved = realm.Find<T>(ID);
|
||||
|
||||
if (!retrieved.IsValid)
|
||||
throw new InvalidOperationException("Attempted to access value without an open context");
|
||||
|
||||
return retrieved;
|
||||
}
|
||||
}
|
||||
|
||||
private bool originalDataValid => isCorrectThread && data.IsValid;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AutoMapper;
|
||||
using osu.Game.Input.Bindings;
|
||||
using Realms;
|
||||
@ -47,5 +48,17 @@ namespace osu.Game.Database
|
||||
|
||||
return mapper.Map<T>(item);
|
||||
}
|
||||
|
||||
public static List<RealmLive<T>> ToLive<T>(this IEnumerable<T> realmList)
|
||||
where T : RealmObject, IHasGuidPrimaryKey
|
||||
{
|
||||
return realmList.Select(l => new RealmLive<T>(l)).ToList();
|
||||
}
|
||||
|
||||
public static RealmLive<T> ToLive<T>(this T realmObject)
|
||||
where T : RealmObject, IHasGuidPrimaryKey
|
||||
{
|
||||
return new RealmLive<T>(realmObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user