1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:33:30 +08:00

Refine RealmContext implementation API

This commit is contained in:
Dean Herbert 2021-09-30 23:42:40 +09:00
parent b8b61a196f
commit 9fa901f6aa
9 changed files with 174 additions and 254 deletions

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tests.Database
storage = new NativeStorage(directory.FullName);
realmContextFactory = new RealmContextFactory(storage);
realmContextFactory = new RealmContextFactory(storage, "test");
keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
}
@ -53,9 +53,9 @@ namespace osu.Game.Tests.Database
private int queryCount(GlobalAction? match = null)
{
using (var usage = realmContextFactory.GetForRead())
using (var realm = realmContextFactory.CreateContext())
{
var results = usage.Realm.All<RealmKeyBinding>();
var results = realm.All<RealmKeyBinding>();
if (match.HasValue)
results = results.Where(k => k.ActionInt == (int)match.Value);
return results.Count();
@ -69,26 +69,24 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
using (var primaryUsage = realmContextFactory.GetForRead())
using (var primaryRealm = realmContextFactory.CreateContext())
{
var backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
var backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
var tsr = ThreadSafeReference.Create(backBinding);
using (var usage = realmContextFactory.GetForWrite())
using (var threadedContext = realmContextFactory.CreateContext())
{
var binding = usage.Realm.ResolveReference(tsr);
binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
usage.Commit();
var binding = threadedContext.ResolveReference(tsr);
threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
}
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
// check still correct after re-query.
backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
}

View File

@ -9,20 +9,12 @@ 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; }
/// <summary>
/// Get a fresh context for read usage.
/// Create a new realm context for use on an arbitrary thread.
/// </summary>
RealmContextFactory.RealmUsage GetForRead();
/// <summary>
/// Request a context for write usage.
/// This method may block if a write is already active on a different thread.
/// </summary>
/// <returns>A usage containing a usable context.</returns>
RealmContextFactory.RealmWriteUsage GetForWrite();
Realm CreateContext();
}
}

View File

@ -2,7 +2,6 @@
// 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;
@ -10,80 +9,115 @@ using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
using osu.Game.Input.Bindings;
using Realms;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
/// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage.
/// </summary>
public class RealmContextFactory : Component, IRealmFactory
{
private readonly Storage storage;
private const string database_name = @"client";
/// <summary>
/// The filename of this realm.
/// </summary>
public readonly string Filename;
private const int schema_version = 6;
/// <summary>
/// Lock object which is held for the duration of a write operation (via <see cref="GetForWrite"/>).
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods.
/// </summary>
private readonly object writeLock = new object();
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1);
/// <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> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)");
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes");
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>("Realm", "Contexts (Created)");
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;
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");
throw new InvalidOperationException($"Use {nameof(CreateContext)} when performing realm operations from a non-update thread");
lock (updateContextLock)
if (context == null)
{
if (context == null)
{
context = createContext();
Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}");
}
// creating a context will ensure our schema is up-to-date and migrated.
return context;
context = createContext();
Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}");
}
// creating a context will ensure our schema is up-to-date and migrated.
return context;
}
}
public RealmContextFactory(Storage storage)
public RealmContextFactory(Storage storage, string filename)
{
this.storage = storage;
Filename = filename;
const string realm_extension = ".realm";
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
Filename += realm_extension;
}
public RealmUsage GetForRead()
public Realm CreateContext()
{
reads.Value++;
return new RealmUsage(createContext());
if (IsDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
return createContext();
}
public RealmWriteUsage GetForWrite()
{
writes.Value++;
pending_writes.Value++;
/// <summary>
/// Compact this realm.
/// </summary>
/// <returns></returns>
public bool Compact() => Realm.Compact(getConfiguration());
Monitor.Enter(writeLock);
return new RealmWriteUsage(createContext(), writeComplete);
protected override void Update()
{
base.Update();
if (context?.Refresh() == true)
refreshes.Value++;
}
private Realm createContext()
{
try
{
contextCreationLock.Wait();
contexts_created.Value++;
return Realm.GetInstance(getConfiguration());
}
finally
{
contextCreationLock.Release();
}
}
private RealmConfiguration getConfiguration()
{
return new RealmConfiguration(storage.GetFullPath(Filename, true))
{
SchemaVersion = schema_version,
MigrationCallback = onMigration,
};
}
private void onMigration(Migration migration, ulong lastSchemaVersion)
{
}
/// <summary>
@ -101,163 +135,32 @@ namespace osu.Game.Database
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
blockingLock.Wait();
flushContexts();
contextCreationLock.Wait();
context?.Dispose();
context = null;
return new InvokeOnDisposal<RealmContextFactory>(this, endBlockingSection);
static void endBlockingSection(RealmContextFactory factory)
{
factory.blockingLock.Release();
factory.contextCreationLock.Release();
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
}
}
protected override void Update()
{
base.Update();
lock (updateContextLock)
{
if (context?.Refresh() == true)
refreshes.Value++;
}
}
private Realm createContext()
{
try
{
if (IsDisposed)
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)
{
switch (lastSchemaVersion)
{
case 5:
// let's keep things simple. changing the type of the primary key is a bit involved.
migration.NewRealm.RemoveAll<RealmKeyBinding>();
break;
}
}
private void flushContexts()
{
Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database);
Debug.Assert(blockingLock.CurrentCount == 0);
Realm previousContext;
lock (updateContextLock)
{
previousContext = context;
context = null;
}
// wait for all threaded usages to finish
while (active_usages.Value > 0)
Thread.Sleep(50);
previousContext?.Dispose();
Logger.Log(@"Realm contexts flushed.", LoggingTarget.Database);
}
protected override void Dispose(bool isDisposing)
{
context?.Dispose();
if (!IsDisposed)
{
// intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal.
BlockAllOperations();
blockingLock?.Dispose();
contextCreationLock.Dispose();
}
base.Dispose(isDisposing);
}
/// <summary>
/// A usage of realm from an arbitrary thread.
/// </summary>
public class RealmUsage : IDisposable
{
public readonly Realm Realm;
internal RealmUsage(Realm context)
{
active_usages.Value++;
Realm = context;
}
/// <summary>
/// Disposes this instance, calling the initially captured action.
/// </summary>
public virtual void Dispose()
{
Realm?.Dispose();
active_usages.Value--;
}
}
/// <summary>
/// A transaction used for making changes to realm data.
/// </summary>
public class RealmWriteUsage : RealmUsage
{
private readonly Action onWriteComplete;
private readonly Transaction transaction;
internal RealmWriteUsage(Realm context, Action onWriteComplete)
: base(context)
{
this.onWriteComplete = onWriteComplete;
transaction = Realm.BeginWrite();
}
/// <summary>
/// Commit all changes made in this transaction.
/// </summary>
public void Commit() => transaction.Commit();
/// <summary>
/// Revert all changes made in this transaction.
/// </summary>
public void Rollback() => transaction.Rollback();
/// <summary>
/// Disposes this instance, calling the initially captured action.
/// </summary>
public override void Dispose()
{
// rollback if not explicitly committed.
transaction?.Dispose();
base.Dispose();
onWriteComplete();
}
}
}
}

View File

@ -1,51 +1,26 @@
// 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.Collections.Generic;
using AutoMapper;
using osu.Game.Input.Bindings;
using System;
using Realms;
namespace osu.Game.Database
{
public static class RealmExtensions
{
private static readonly IMapper mapper = new MapperConfiguration(c =>
public static void Write(this Realm realm, Action<Realm> function)
{
c.ShouldMapField = fi => false;
c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic;
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
}).CreateMapper();
/// <summary>
/// Create a detached copy of the each item in the collection.
/// </summary>
/// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
/// <typeparam name="T">The type of object.</typeparam>
/// <returns>A list containing non-managed copies of provided items.</returns>
public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObject
{
var list = new List<T>();
foreach (var obj in items)
list.Add(obj.Detach());
return list;
using var transaction = realm.BeginWrite();
function(realm);
transaction.Commit();
}
/// <summary>
/// Create a detached copy of the item.
/// </summary>
/// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
/// <typeparam name="T">The type of object.</typeparam>
/// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>
public static T Detach<T>(this T item) where T : RealmObject
public static T Write<T>(this Realm realm, Func<Realm, T> function)
{
if (!item.IsManaged)
return item;
return mapper.Map<T>(item);
using var transaction = realm.BeginWrite();
var result = function(realm);
transaction.Commit();
return result;
}
}
}

View File

@ -0,0 +1,51 @@
// 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.Collections.Generic;
using AutoMapper;
using osu.Game.Input.Bindings;
using Realms;
namespace osu.Game.Database
{
public static class RealmObjectExtensions
{
private static readonly IMapper mapper = new MapperConfiguration(c =>
{
c.ShouldMapField = fi => false;
c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic;
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
}).CreateMapper();
/// <summary>
/// Create a detached copy of the each item in the collection.
/// </summary>
/// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
/// <typeparam name="T">The type of object.</typeparam>
/// <returns>A list containing non-managed copies of provided items.</returns>
public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObject
{
var list = new List<T>();
foreach (var obj in items)
list.Add(obj.Detach());
return list;
}
/// <summary>
/// Create a detached copy of the item.
/// </summary>
/// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
/// <typeparam name="T">The type of object.</typeparam>
/// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>
public static T Detach<T>(this T item) where T : RealmObject
{
if (!item.IsManaged)
return item;
return mapper.Map<T>(item);
}
}
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings;
using osu.Game.Database;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using Realms;
#nullable enable
@ -30,9 +31,9 @@ namespace osu.Game.Input
{
List<string> combinations = new List<string>();
using (var context = realmFactory.GetForRead())
using (var context = realmFactory.CreateContext())
{
foreach (var action in context.Realm.All<RealmKeyBinding>().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction))
foreach (var action in context.All<RealmKeyBinding>().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction))
{
string str = action.KeyCombination.ReadableString();
@ -52,26 +53,27 @@ namespace osu.Game.Input
/// <param name="rulesets">The rulesets to populate defaults from.</param>
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets)
{
using (var usage = realmFactory.GetForWrite())
using (var realm = realmFactory.CreateContext())
using (var transaction = realm.BeginWrite())
{
// intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
// this is much faster as a result.
var existingBindings = usage.Realm.All<RealmKeyBinding>().ToList();
var existingBindings = realm.All<RealmKeyBinding>().ToList();
insertDefaults(usage, existingBindings, container.DefaultKeyBindings);
insertDefaults(realm, existingBindings, container.DefaultKeyBindings);
foreach (var ruleset in rulesets)
{
var instance = ruleset.CreateInstance();
foreach (var variant in instance.AvailableVariants)
insertDefaults(usage, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
}
usage.Commit();
transaction.Commit();
}
}
private void insertDefaults(RealmContextFactory.RealmUsage usage, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
private void insertDefaults(Realm realm, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
{
// compare counts in database vs defaults for each action type.
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
@ -83,7 +85,7 @@ namespace osu.Game.Input
continue;
// insert any defaults which are missing.
usage.Realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
{
KeyCombinationString = k.KeyCombination.ToString(),
ActionInt = (int)k.Action,

View File

@ -187,7 +187,7 @@ namespace osu.Game
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
dependencies.Cache(realmFactory = new RealmContextFactory(Storage));
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client"));
updateThreadState = Host.UpdateThread.State.GetBoundCopy();
updateThreadState.BindValueChanged(updateThreadStateChanged);
@ -448,19 +448,20 @@ namespace osu.Game
private void migrateDataToRealm()
{
using (var db = contextFactory.GetForWrite())
using (var usage = realmFactory.GetForWrite())
using (var realm = realmFactory.CreateContext())
using (var transaction = realm.BeginWrite())
{
// migrate ruleset settings. can be removed 20220315.
var existingSettings = db.Context.DatabasedSetting;
// only migrate data if the realm database is empty.
if (!usage.Realm.All<RealmRulesetSetting>().Any())
if (!realm.All<RealmRulesetSetting>().Any())
{
foreach (var dkb in existingSettings)
{
if (dkb.RulesetID == null) continue;
usage.Realm.Add(new RealmRulesetSetting
realm.Add(new RealmRulesetSetting
{
Key = dkb.Key,
Value = dkb.StringValue,
@ -472,7 +473,7 @@ namespace osu.Game
db.Context.RemoveRange(existingSettings);
usage.Commit();
transaction.Commit();
}
}

View File

@ -368,12 +368,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private void updateStoreFromButton(KeyButton button)
{
using (var usage = realmFactory.GetForWrite())
using (var realm = realmFactory.CreateContext())
{
var binding = usage.Realm.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
binding.KeyCombinationString = button.KeyBinding.KeyCombinationString;
usage.Commit();
var binding = realm.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString);
}
}

View File

@ -38,8 +38,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
List<RealmKeyBinding> bindings;
using (var usage = realmFactory.GetForRead())
bindings = usage.Realm.All<RealmKeyBinding>().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach();
using (var realm = realmFactory.CreateContext())
bindings = realm.All<RealmKeyBinding>().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach();
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
{