diff --git a/osu.Android.props b/osu.Android.props
index 4e5b9fdbb1..f85a96f819 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs
index dc0d42595b..8262ef18d4 100644
--- a/osu.Game.Tests/Database/GeneralUsageTests.cs
+++ b/osu.Game.Tests/Database/GeneralUsageTests.cs
@@ -87,11 +87,6 @@ namespace osu.Game.Tests.Database
hasThreadedUsage.Wait();
- // Usually the host would run the synchronization context work per frame.
- // For the sake of keeping this test simple (there's only one update invocation),
- // let's replace it so we can ensure work is run immediately.
- SynchronizationContext.SetSynchronizationContext(new ImmediateExecuteSynchronizationContext());
-
Assert.Throws(() =>
{
using (realm.BlockAllOperations())
@@ -107,10 +102,5 @@ namespace osu.Game.Tests.Database
}
});
}
-
- private class ImmediateExecuteSynchronizationContext : SynchronizationContext
- {
- public override void Post(SendOrPostCallback d, object? state) => d(state);
- }
}
}
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
index 3f81b36378..4bc1f5078a 100644
--- a/osu.Game.Tests/Database/RealmLiveTests.cs
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -47,16 +47,14 @@ namespace osu.Game.Tests.Database
liveBeatmap = beatmap.ToLive(realm);
});
- using (realm.BlockAllOperations())
- {
- // recycle realm before migrating
- }
-
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
{
migratedStorage.DeleteDirectory(string.Empty);
- storage.Migrate(migratedStorage);
+ using (realm.BlockAllOperations())
+ {
+ storage.Migrate(migratedStorage);
+ }
Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden));
}
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 4fdfaa804c..9bdbebfe89 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -89,10 +89,15 @@ namespace osu.Game.Database
private Realm? updateRealm;
+ private bool isSendingNotificationResetEvents;
+
public Realm Realm => ensureUpdateRealm();
private Realm ensureUpdateRealm()
{
+ if (isSendingNotificationResetEvents)
+ throw new InvalidOperationException("Cannot retrieve a realm context from a notification callback during a blocking operation.");
+
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"Use {nameof(getRealmInstance)} when performing realm operations from a non-update thread");
@@ -300,7 +305,7 @@ namespace osu.Game.Database
return new InvokeOnDisposal(() =>
{
if (ThreadSafety.IsUpdateThread)
- unsubscribe();
+ syncContext.Send(_ => unsubscribe(), null);
else
syncContext.Post(_ => unsubscribe(), null);
@@ -339,25 +344,6 @@ namespace osu.Game.Database
}
}
- ///
- /// Unregister all subscriptions when the realm instance is to be recycled.
- /// Subscriptions will still remain and will be re-subscribed when the realm instance returns.
- ///
- private void unregisterAllSubscriptions()
- {
- lock (realmLock)
- {
- foreach (var action in notificationsResetMap.Values)
- action();
-
- foreach (var action in customSubscriptionsResetMap)
- {
- action.Value?.Dispose();
- customSubscriptionsResetMap[action.Key] = null;
- }
- }
- }
-
private Realm getRealmInstance()
{
if (isDisposed)
@@ -610,9 +596,16 @@ namespace osu.Game.Database
throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
syncContext = SynchronizationContext.Current;
- }
- unregisterAllSubscriptions();
+ // Before disposing the update context, clean up all subscriptions.
+ // Note that in the case of realm notification subscriptions, this is not really required (they will be cleaned up by disposal).
+ // In the case of custom subscriptions, we want them to fire before the update realm is disposed in case they do any follow-up work.
+ foreach (var action in customSubscriptionsResetMap)
+ {
+ action.Value?.Dispose();
+ customSubscriptionsResetMap[action.Key] = null;
+ }
+ }
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
@@ -641,6 +634,28 @@ namespace osu.Game.Database
// We still want to continue with the blocking operation, though.
Logger.Log($"Realm compact failed with error {e}", LoggingTarget.Database);
}
+
+ // In order to ensure events arrive in the correct order, these *must* be fired post disposal of the update realm,
+ // and must be posted to the synchronization context.
+ // This is because realm may fire event callbacks between the `unregisterAllSubscriptions` and `updateRealm.Dispose`
+ // calls above.
+ syncContext?.Send(_ =>
+ {
+ // Flag ensures that we don't get in a deadlocked scenario due to a callback attempting to access `RealmAccess.Realm` or `RealmAccess.Run`
+ // and hitting `realmRetrievalLock` a second time. Generally such usages should not exist, and as such we throw when an attempt is made
+ // to use in this fashion.
+ isSendingNotificationResetEvents = true;
+
+ try
+ {
+ foreach (var action in notificationsResetMap.Values)
+ action();
+ }
+ finally
+ {
+ isSendingNotificationResetEvents = false;
+ }
+ }, null);
}
catch
{
@@ -654,8 +669,14 @@ namespace osu.Game.Database
{
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
realmRetrievalLock.Release();
+
// Post back to the update thread to revive any subscriptions.
- syncContext?.Post(_ => ensureUpdateRealm(), null);
+ // In the case we are on the update thread, let's also require this to run synchronously.
+ // This requirement is mostly due to test coverage, but shouldn't cause any harm.
+ if (ThreadSafety.IsUpdateThread)
+ syncContext?.Send(_ => ensureUpdateRealm(), null);
+ else
+ syncContext?.Post(_ => ensureUpdateRealm(), null);
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index af5d8a5920..aa6fb93aa0 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 2bcdea61b3..fbb4688588 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -60,7 +60,7 @@
-
+
@@ -83,7 +83,7 @@
-
+