1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 15:47:26 +08:00

Merge pull request #16606 from peppy/fix-out-of-order-events-on-block-fail

Fix notification reset events potentially arriving out of order if a block operation times out
This commit is contained in:
Dan Balasescu 2022-01-26 20:28:21 +09:00 committed by GitHub
commit a07fce55d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 52 additions and 43 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.125.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.126.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -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<TimeoutException>(() =>
{
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);
}
}
}

View File

@ -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));
}

View File

@ -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
}
}
/// <summary>
/// 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.
/// </summary>
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);
}
}

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.8.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.125.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.126.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="Sentry" Version="3.13.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" />

View File

@ -60,7 +60,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.125.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.126.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -83,7 +83,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.125.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.126.0" />
<PackageReference Include="SharpCompress" Version="0.30.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />