From c8b9c117cded884aaa4d7d97e5116300f07b81b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 15:47:41 +0900 Subject: [PATCH 1/3] Add failing test showing realm not sending through null `ChangeSet` --- .../RealmSubscriptionRegistrationTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 45842a952ab3..14864f7aa1a2 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -71,6 +71,35 @@ void onChanged(IRealmCollection sender, ChangeSet? changes) } } + [Test] + public void TestSubscriptionInitialChangeSetNull() + { + ChangeSet? firstChanges = null; + int receivedChangesCount = 0; + + RunTestWithRealm((realm, _) => + { + var registration = realm.RegisterForNotifications(r => r.All(), onChanged); + + realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely(); + + realm.Run(r => r.Refresh()); + + Assert.That(receivedChangesCount, Is.EqualTo(2)); + Assert.That(firstChanges, Is.Null); + + registration.Dispose(); + }); + + void onChanged(IRealmCollection sender, ChangeSet? changes) + { + if (receivedChangesCount == 0) + firstChanges = changes; + + receivedChangesCount++; + } + } + [Test] public void TestSubscriptionWithAsyncWrite() { From 2423bbb776bb0fd5042693684cd2e57ac7f3eff8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 15:28:35 +0900 Subject: [PATCH 2/3] Ensure realm subscriptions always fire initial callback with null `ChangeSet` We expect this to be the case, but it turns out that it [may be coalesced](https://www.mongodb.com/docs/realm-sdks/dotnet/latest/reference/Realms.IRealmCollection-1.html#Realms_IRealmCollection_1_SubscribeForNotifications_Realms_NotificationCallbackDelegate__0__Realms_KeyPathsCollection_): > Notifications are delivered via the standard event loop, and so can't > be delivered while the event loop is blocked by other activity. When > notifications can't be delivered instantly, multiple notifications may > be coalesced into a single notification. This can include the > notification with the initial collection. Rather than struggle with handling this locally every time, let's fix the callback at our end to ensure we receive the initial null case. I've raised concern for the API being a bit silly with realm (https://github.com/realm/realm-dotnet/issues/3641). --- osu.Game/Database/RealmObjectExtensions.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 72529ed9ff7c..bd8c52bb8549 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -65,7 +65,8 @@ public static class RealmObjectExtensions if (!d.Beatmaps.Contains(existingBeatmap)) { Debug.Fail("Beatmaps should never become detached under normal circumstances. If this ever triggers, it should be investigated further."); - Logger.Log("WARNING: One of the difficulties in a beatmap was detached from its set. Please save a copy of logs and report this to devs.", LoggingTarget.Database, LogLevel.Important); + Logger.Log("WARNING: One of the difficulties in a beatmap was detached from its set. Please save a copy of logs and report this to devs.", LoggingTarget.Database, + LogLevel.Important); d.Beatmaps.Add(existingBeatmap); } @@ -291,7 +292,21 @@ public static IDisposable QueryAsyncWithNotifications(this IRealmCollection + { + if (initial) + { + initial = false; + + // Realm might coalesce the initial callback, meaning we never receive a `ChangeSet` of `null` marking the first callback. + // Let's decouple it for simplicity in handling. + if (changes != null) + callback(sender, null); + } + + callback(sender, changes); + })); } /// From ee9e329db33c11cca18699e5d6396cedff3e07f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 16:05:58 +0900 Subject: [PATCH 3/3] Inhibit original callback from firing when sending initial changeset --- osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs | 2 +- osu.Game/Database/RealmObjectExtensions.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 14864f7aa1a2..e5be4d665b65 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -85,7 +85,7 @@ public void TestSubscriptionInitialChangeSetNull() realm.Run(r => r.Refresh()); - Assert.That(receivedChangesCount, Is.EqualTo(2)); + Assert.That(receivedChangesCount, Is.EqualTo(1)); Assert.That(firstChanges, Is.Null); registration.Dispose(); diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index bd8c52bb8549..2fa3b8a88072 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -302,7 +302,10 @@ public static IDisposable QueryAsyncWithNotifications(this IRealmCollection