1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 16:12:57 +08:00

Update terminology to realm "instance" rather than "context"

This matches the terminology used by realm themselves, which feels
better.
This commit is contained in:
Dean Herbert 2022-01-24 20:11:36 +09:00
parent 6eb2c28e41
commit f30894840c
4 changed files with 67 additions and 76 deletions

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Database
{ {
RunTestWithRealm((realmAccess, storage) => RunTestWithRealm((realmAccess, storage) =>
{ {
var realm = realmAccess.Context; var realm = realmAccess.Realm;
var files = new RealmFileStore(realmAccess, storage); var files = new RealmFileStore(realmAccess, storage);
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
@ -38,7 +38,7 @@ namespace osu.Game.Tests.Database
{ {
RunTestWithRealm((realmAccess, storage) => RunTestWithRealm((realmAccess, storage) =>
{ {
var realm = realmAccess.Context; var realm = realmAccess.Realm;
var files = new RealmFileStore(realmAccess, storage); var files = new RealmFileStore(realmAccess, storage);
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
@ -55,7 +55,7 @@ namespace osu.Game.Tests.Database
{ {
RunTestWithRealm((realmAccess, storage) => RunTestWithRealm((realmAccess, storage) =>
{ {
var realm = realmAccess.Context; var realm = realmAccess.Realm;
var files = new RealmFileStore(realmAccess, storage); var files = new RealmFileStore(realmAccess, storage);
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
@ -94,7 +94,7 @@ namespace osu.Game.Tests.Database
{ {
RunTestWithRealm((realmAccess, storage) => RunTestWithRealm((realmAccess, storage) =>
{ {
var realm = realmAccess.Context; var realm = realmAccess.Realm;
var files = new RealmFileStore(realmAccess, storage); var files = new RealmFileStore(realmAccess, storage);
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));

View File

@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Navigation
.ChildrenOfType<KeyBindingPanel>().SingleOrDefault(); .ChildrenOfType<KeyBindingPanel>().SingleOrDefault();
private RealmKeyBinding firstOsuRulesetKeyBindings => Game.Dependencies private RealmKeyBinding firstOsuRulesetKeyBindings => Game.Dependencies
.Get<RealmAccess>().Context .Get<RealmAccess>().Realm
.All<RealmKeyBinding>() .All<RealmKeyBinding>()
.AsEnumerable() .AsEnumerable()
.First(k => k.RulesetName == "osu" && k.ActionInt == 0); .First(k => k.RulesetName == "osu" && k.ActionInt == 0);

View File

@ -59,9 +59,9 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods.
/// </summary> /// </summary>
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); private readonly SemaphoreSlim realmCreationLock = new SemaphoreSlim(1);
private readonly ThreadLocal<bool> currentThreadCanCreateContexts = new ThreadLocal<bool>(); private readonly ThreadLocal<bool> currentThreadCanCreateRealmInstances = new ThreadLocal<bool>();
/// <summary> /// <summary>
/// Holds a map of functions registered via <see cref="RegisterCustomSubscription"/> and <see cref="RegisterForNotifications{T}"/> and a coinciding action which when triggered, /// Holds a map of functions registered via <see cref="RegisterCustomSubscription"/> and <see cref="RegisterForNotifications{T}"/> and a coinciding action which when triggered,
@ -81,35 +81,35 @@ namespace osu.Game.Database
/// </summary> /// </summary>
private readonly Dictionary<Func<Realm, IDisposable?>, Action> notificationsResetMap = new Dictionary<Func<Realm, IDisposable?>, Action>(); private readonly Dictionary<Func<Realm, IDisposable?>, Action> notificationsResetMap = new Dictionary<Func<Realm, IDisposable?>, Action>();
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>(@"Realm", @"Contexts (Created)"); private static readonly GlobalStatistic<int> realm_instances_created = GlobalStatistics.Get<int>(@"Realm", @"Instances (Created)");
private readonly object contextLock = new object(); private readonly object realmLock = new object();
private Realm? context; private Realm? updateRealm;
public Realm Context => ensureUpdateContext(); public Realm Realm => ensureUpdateRealm();
private Realm ensureUpdateContext() private Realm ensureUpdateRealm()
{ {
if (!ThreadSafety.IsUpdateThread) if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"Use {nameof(createContext)} when performing realm operations from a non-update thread"); throw new InvalidOperationException(@$"Use {nameof(getRealmInstance)} when performing realm operations from a non-update thread");
lock (contextLock) lock (realmLock)
{ {
if (context == null) if (updateRealm == null)
{ {
context = createContext(); updateRealm = getRealmInstance();
Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}");
Logger.Log(@$"Opened realm ""{updateRealm.Config.DatabasePath}"" at version {updateRealm.Config.SchemaVersion}");
// Resubscribe any subscriptions // Resubscribe any subscriptions
foreach (var action in customSubscriptionsResetMap.Keys) foreach (var action in customSubscriptionsResetMap.Keys)
registerSubscription(action); registerSubscription(action);
} }
Debug.Assert(context != null); Debug.Assert(updateRealm != null);
// creating a context will ensure our schema is up-to-date and migrated. return updateRealm;
return context;
} }
} }
@ -118,7 +118,7 @@ namespace osu.Game.Database
private static readonly ThreadLocal<bool> current_thread_subscriptions_allowed = new ThreadLocal<bool>(); private static readonly ThreadLocal<bool> current_thread_subscriptions_allowed = new ThreadLocal<bool>();
/// <summary> /// <summary>
/// Construct a new instance of a realm context factory. /// Construct a new instance.
/// </summary> /// </summary>
/// <param name="storage">The game storage which will be used to create the realm backing file.</param> /// <param name="storage">The game storage which will be used to create the realm backing file.</param>
/// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param> /// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param>
@ -137,7 +137,7 @@ namespace osu.Game.Database
try try
{ {
// This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date. // This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
cleanupPendingDeletions(); cleanupPendingDeletions();
} }
catch (Exception e) catch (Exception e)
@ -153,7 +153,7 @@ namespace osu.Game.Database
private void cleanupPendingDeletions() private void cleanupPendingDeletions()
{ {
using (var realm = createContext()) using (var realm = getRealmInstance())
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
var pendingDeleteScores = realm.All<ScoreInfo>().Where(s => s.DeletePending); var pendingDeleteScores = realm.All<ScoreInfo>().Where(s => s.DeletePending);
@ -201,34 +201,28 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Run work on realm with a return value. /// Run work on realm with a return value.
/// </summary> /// </summary>
/// <remarks>
/// Handles correct context management automatically.
/// </remarks>
/// <param name="action">The work to run.</param> /// <param name="action">The work to run.</param>
/// <typeparam name="T">The return type.</typeparam> /// <typeparam name="T">The return type.</typeparam>
public T Run<T>(Func<Realm, T> action) public T Run<T>(Func<Realm, T> action)
{ {
if (ThreadSafety.IsUpdateThread) if (ThreadSafety.IsUpdateThread)
return action(Context); return action(Realm);
using (var realm = createContext()) using (var realm = getRealmInstance())
return action(realm); return action(realm);
} }
/// <summary> /// <summary>
/// Run work on realm. /// Run work on realm.
/// </summary> /// </summary>
/// <remarks>
/// Handles correct context management automatically.
/// </remarks>
/// <param name="action">The work to run.</param> /// <param name="action">The work to run.</param>
public void Run(Action<Realm> action) public void Run(Action<Realm> action)
{ {
if (ThreadSafety.IsUpdateThread) if (ThreadSafety.IsUpdateThread)
action(Context); action(Realm);
else else
{ {
using (var realm = createContext()) using (var realm = getRealmInstance())
action(realm); action(realm);
} }
} }
@ -236,17 +230,14 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Write changes to realm. /// Write changes to realm.
/// </summary> /// </summary>
/// <remarks>
/// Handles correct context management and transaction committing automatically.
/// </remarks>
/// <param name="action">The work to run.</param> /// <param name="action">The work to run.</param>
public void Write(Action<Realm> action) public void Write(Action<Realm> action)
{ {
if (ThreadSafety.IsUpdateThread) if (ThreadSafety.IsUpdateThread)
Context.Write(action); Realm.Write(action);
else else
{ {
using (var realm = createContext()) using (var realm = getRealmInstance())
realm.Write(action); realm.Write(action);
} }
} }
@ -257,10 +248,10 @@ namespace osu.Game.Database
/// <remarks> /// <remarks>
/// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>. /// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>.
/// ///
/// In addition to the documented realm behaviour, we have the additional requirement of handling subscriptions over potential context loss. /// In addition to the documented realm behaviour, we have the additional requirement of handling subscriptions over potential realm instance recycle.
/// When this happens, callback events will be automatically fired: /// When this happens, callback events will be automatically fired:
/// - On context loss, a callback with an empty collection and <c>null</c> <see cref="ChangeSet"/> will be invoked. /// - On recycle start, a callback with an empty collection and <c>null</c> <see cref="ChangeSet"/> will be invoked.
/// - On context revival, a standard initial realm callback will arrive, with <c>null</c> <see cref="ChangeSet"/> and an up-to-date collection. /// - On recycle end, a standard initial realm callback will arrive, with <c>null</c> <see cref="ChangeSet"/> and an up-to-date collection.
/// </remarks> /// </remarks>
/// <param name="query">The <see cref="IQueryable{T}"/> to observe for changes.</param> /// <param name="query">The <see cref="IQueryable{T}"/> to observe for changes.</param>
/// <typeparam name="T">Type of the elements in the list.</typeparam> /// <typeparam name="T">Type of the elements in the list.</typeparam>
@ -276,7 +267,7 @@ namespace osu.Game.Database
if (!ThreadSafety.IsUpdateThread) if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
lock (contextLock) lock (realmLock)
{ {
Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback); Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback);
@ -287,7 +278,7 @@ namespace osu.Game.Database
} }
/// <summary> /// <summary>
/// Run work on realm that will be run every time the update thread realm context gets recycled. /// Run work on realm that will be run every time the update thread realm instance gets recycled.
/// </summary> /// </summary>
/// <param name="action">The work to run. Return value should be an <see cref="IDisposable"/> from QueryAsyncWithNotifications, or an <see cref="InvokeOnDisposal"/> to clean up any bindings.</param> /// <param name="action">The work to run. Return value should be an <see cref="IDisposable"/> from QueryAsyncWithNotifications, or an <see cref="InvokeOnDisposal"/> to clean up any bindings.</param>
/// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns> /// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns>
@ -311,7 +302,7 @@ namespace osu.Game.Database
void unsubscribe() void unsubscribe()
{ {
lock (contextLock) lock (realmLock)
{ {
if (customSubscriptionsResetMap.TryGetValue(action, out var unsubscriptionAction)) if (customSubscriptionsResetMap.TryGetValue(action, out var unsubscriptionAction))
{ {
@ -328,12 +319,12 @@ namespace osu.Game.Database
{ {
Debug.Assert(ThreadSafety.IsUpdateThread); Debug.Assert(ThreadSafety.IsUpdateThread);
lock (contextLock) lock (realmLock)
{ {
// Retrieve context outside of flag update to ensure that the context is constructed, // Retrieve realm instance outside of flag update to ensure that the instance is retrieved,
// as attempting to access it inside the subscription if it's not constructed would lead to // as attempting to access it inside the subscription if it's not constructed would lead to
// cyclic invocations of the subscription callback. // cyclic invocations of the subscription callback.
var realm = Context; var realm = Realm;
Debug.Assert(!customSubscriptionsResetMap.TryGetValue(action, out var found) || found == null); Debug.Assert(!customSubscriptionsResetMap.TryGetValue(action, out var found) || found == null);
@ -344,12 +335,12 @@ namespace osu.Game.Database
} }
/// <summary> /// <summary>
/// Unregister all subscriptions when the realm context is to be recycled. /// Unregister all subscriptions when the realm instance is to be recycled.
/// Subscriptions will still remain and will be re-subscribed when the realm context returns. /// Subscriptions will still remain and will be re-subscribed when the realm instance returns.
/// </summary> /// </summary>
private void unregisterAllSubscriptions() private void unregisterAllSubscriptions()
{ {
lock (contextLock) lock (realmLock)
{ {
foreach (var action in notificationsResetMap.Values) foreach (var action in notificationsResetMap.Values)
action(); action();
@ -362,7 +353,7 @@ namespace osu.Game.Database
} }
} }
private Realm createContext() private Realm getRealmInstance()
{ {
if (isDisposed) if (isDisposed)
throw new ObjectDisposedException(nameof(RealmAccess)); throw new ObjectDisposedException(nameof(RealmAccess));
@ -371,20 +362,20 @@ namespace osu.Game.Database
try try
{ {
if (!currentThreadCanCreateContexts.Value) if (!currentThreadCanCreateRealmInstances.Value)
{ {
contextCreationLock.Wait(); realmCreationLock.Wait();
currentThreadCanCreateContexts.Value = true; currentThreadCanCreateRealmInstances.Value = true;
tookSemaphoreLock = true; tookSemaphoreLock = true;
} }
else else
{ {
// the semaphore is used to handle blocking of all context creation during certain periods. // the semaphore is used to handle blocking of all realm retrieval during certain periods.
// once the semaphore has been taken by this code section, it is safe to create further contexts on the same thread. // once the semaphore has been taken by this code section, it is safe to retrieve further realm instances on the same thread.
// this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`. // this can happen if a realm subscription is active and triggers a callback which has user code that calls `Run`.
} }
contexts_created.Value++; realm_instances_created.Value++;
return Realm.GetInstance(getConfiguration()); return Realm.GetInstance(getConfiguration());
} }
@ -392,8 +383,8 @@ namespace osu.Game.Database
{ {
if (tookSemaphoreLock) if (tookSemaphoreLock)
{ {
contextCreationLock.Release(); realmCreationLock.Release();
currentThreadCanCreateContexts.Value = false; currentThreadCanCreateRealmInstances.Value = false;
} }
} }
} }
@ -582,7 +573,7 @@ namespace osu.Game.Database
} }
/// <summary> /// <summary>
/// Flush any active contexts and block any further writes. /// Flush any active realm instances and block any further writes.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm. /// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm.
@ -598,14 +589,14 @@ namespace osu.Game.Database
try try
{ {
contextCreationLock.Wait(); realmCreationLock.Wait();
lock (contextLock) lock (realmLock)
{ {
if (context == null) if (updateRealm == null)
{ {
// null context means the update thread has not yet retrieved its context. // null realm means the update thread has not yet retrieved its instance.
// we don't need to worry about reviving the update context in this case, so don't bother with the SynchronizationContext. // we don't need to worry about reviving the update instance in this case, so don't bother with the SynchronizationContext.
Debug.Assert(!ThreadSafety.IsUpdateThread); Debug.Assert(!ThreadSafety.IsUpdateThread);
} }
else else
@ -620,8 +611,8 @@ namespace osu.Game.Database
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
context?.Dispose(); updateRealm?.Dispose();
context = null; updateRealm = null;
} }
const int sleep_length = 200; const int sleep_length = 200;
@ -648,17 +639,17 @@ namespace osu.Game.Database
} }
catch catch
{ {
contextCreationLock.Release(); realmCreationLock.Release();
throw; throw;
} }
return new InvokeOnDisposal<RealmAccess>(this, factory => return new InvokeOnDisposal<RealmAccess>(this, factory =>
{ {
factory.contextCreationLock.Release(); factory.realmCreationLock.Release();
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
// Post back to the update thread to revive any subscriptions. // Post back to the update thread to revive any subscriptions.
syncContext?.Post(_ => ensureUpdateContext(), null); syncContext?.Post(_ => ensureUpdateRealm(), null);
}); });
} }
@ -669,16 +660,16 @@ namespace osu.Game.Database
public void Dispose() public void Dispose()
{ {
lock (contextLock) lock (realmLock)
{ {
context?.Dispose(); updateRealm?.Dispose();
} }
if (!isDisposed) if (!isDisposed)
{ {
// intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal.
contextCreationLock.Wait(); realmCreationLock.Wait();
contextCreationLock.Dispose(); realmCreationLock.Dispose();
isDisposed = true; isDisposed = true;
} }

View File

@ -45,7 +45,7 @@ namespace osu.Game.Stores
// This method should be removed as soon as all the surrounding pieces support non-detached operations. // This method should be removed as soon as all the surrounding pieces support non-detached operations.
if (!item.IsManaged) if (!item.IsManaged)
{ {
var managed = Access.Context.Find<TModel>(item.ID); var managed = Access.Realm.Find<TModel>(item.ID);
managed.Realm.Write(() => operation(managed)); managed.Realm.Write(() => operation(managed));
item.Files.Clear(); item.Files.Clear();