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 @@ - +