Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send notifications about score processing completion to clients #157

Merged
merged 11 commits into from
Dec 26, 2022

Conversation

bdach
Copy link
Collaborator

@bdach bdach commented Dec 20, 2022

Note: This is an RFC. It has been tested very lightly. The happy path seems to work, but I'm not sure about the multitude of sad paths. I'm PRing this now to get it out for comments and see how many WTFs I get, as I'm not confident about almost everything in this diff, so I wouldn't necessarily advise to hurry this one up too much.

Testing procedure, for whoever is inclined:

  • Begin with a working full-stack osu + osu-web + osu-server-spectator setup
  • In osu, check out pull mentioned above
  • In osu-server-spectator, switch to local checkout
  • Set up & launch osu-queue-score-statistics so that it can pick up scores placed in redis by osu-web
  • At this point messages will flow, but there will be no visible effect game-side. To make one happen, the following crude patch can be applied:
patch to apply to game
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index a0a45e18a8..45c94b73d2 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -990,6 +990,11 @@ protected override void LoadComplete()
 
             // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
             handleStartupImport();
+
+            SpectatorClient.OnUserScoreProcessed += (userId, scoreId) =>
+            {
+                Notifications.Post(new SimpleNotification { Text = $"OnUserScoreProcessed (userId: {userId}, scoreId: {scoreId})" });
+            };
         }
 
         private void handleStartupImport()
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 7586dc6407..468bc61696 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -188,7 +188,7 @@ public virtual string Version
 
         private RulesetConfigCache rulesetConfigCache;
 
-        private SpectatorClient spectatorClient;
+        protected SpectatorClient SpectatorClient { get; private set; }
 
         protected MultiplayerClient MultiplayerClient { get; private set; }
 
@@ -298,7 +298,7 @@ private void load(ReadableKeyCombinationProvider keyCombinationProvider)
 
             // TODO: OsuGame or OsuGameBase?
             dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
-            dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
+            dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints));
             dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
             dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
 
@@ -343,7 +343,7 @@ private void load(ReadableKeyCombinationProvider keyCombinationProvider)
             if (API is APIAccess apiAccess)
                 AddInternal(apiAccess);
 
-            AddInternal(spectatorClient);
+            AddInternal(SpectatorClient);
             AddInternal(MultiplayerClient);
             AddInternal(metadataClient);
 

At this point, every completed play should result in a notification being fired. Aborted plays won't always fire the notification. I'm not sure why yet, I'm going to look into that a bit more still tomorrow if this approach doesn't just get dumpstered immediately.

I'll be adding some self-commentary review comments shortly.

}
}

private void purgeTimedOutSubscriptions()
Copy link
Collaborator Author

@bdach bdach Dec 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this entire flow to avoid a situation where the subscriber gets backlogged due to the score processor not being able to catch up to incoming scores and accumulates too many subscriptions, causing a cascading failure. I'm not sure how realistic of a scenario this is in practice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to have to see how this plays out post deploy really.

if (subscriptions.TryRemove(scoreProcessed.ScoreId, out var subscription))
{
using (subscription)
subscription.InvokeAsync().Wait();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .Wait() is unfortunate, but I'm not sure how to avoid it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably fine.

@bdach
Copy link
Collaborator Author

bdach commented Dec 21, 2022

Aborted plays won't always fire the notification. I'm not sure why yet

I've figured out what this was; see ppy/osu#21753 for explanation and proposed fix.

/// <summary>
/// The maximum amount of time to wait for a <see cref="ScoreProcessed"/> message for a given score in milliseconds.
/// </summary>
private const int timeout_interval_ms = 30_000;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll see how this plays out. I have a feeling going higher than this will be what we want eventually (minutes at least) to allow for potential server-side issues/outages.

@peppy peppy merged commit 162ac6a into ppy:master Dec 26, 2022

foreach (var scoreId in scoreIds)
{
if (subscriptions.TryGetValue(scoreId, out var subscription) && subscription.TimedOut)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to watch out for is that CancellationTokenSource.Token throws an exception if the CTS has been disposed. There could theoretically be a small moment where this TryGetValue() retrieves the value on this thread, but then the using (subscription) above runs and only after that does the subscription.TimedOut check happen.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was aware of this, but when I checked the implementation, then I didn't expect an IsCancellationRequested check to throw after disposal. Maybe bad form relying on implementation details.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No you're correct, I didn't know there was such a discrepancy between Token and IsCancellationRequested

@bdach bdach deleted the score-processed-notification branch December 26, 2022 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants