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

Update notification text when import is paused due to gameplay #30402

Merged
merged 6 commits into from
Nov 20, 2024

Conversation

peppy
Copy link
Member

@peppy peppy commented Oct 23, 2024

Addresses #30388.

osu.2024-10-23.at.08.50.25.mp4

@peppy peppy added the area:import dealing with importing of data from stable or file formats label Oct 23, 2024
@tovc
Copy link

tovc commented Oct 23, 2024

more of a point regarding design, but the spinning circle doesn't change, which might, at a glance, imply that the import isn't paused. something like a yellow pause icon might be more appropriate?

actually, for that matter, there is already the yellow spinning circle design, might as well just reuse that


try
{
pauseIfNecessary(parameters, notification.CancellationToken);
Copy link
Contributor

@smoogipoo smoogipoo Oct 24, 2024

Choose a reason for hiding this comment

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

It's a little bit scary that this is the first entrypoint to the async context (there's no await prior to this line). For example if Import is ever called from the update/bdl thread, then this is going to potentially deadlock the game.

I suggest either await Task.Yield() before the pause, or wrapping the entire thing in a Task.Run().

Or.... if it can, use await Task.Delay() instead of Thread.Sleep(). Doesn't look like it can though.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a fair point. I started by duplicating the method and making an async version, but then ended up refactoring things a bit to not require this.

Of note, the Parallel.ForEachAsync change was required because without it, the previous Select loop would seem to cause all imports to get past the pause check and run up to the Import part which would mean they never actually pause.

The whole import async logic is a bit of a mess but as far as I can tell it's working as expected.

@pull-request-size pull-request-size bot added size/M and removed size/S labels Oct 24, 2024
@smoogipoo
Copy link
Contributor

Test failures need resolution here.

Comment on lines 118 to 122
if (notification.CancellationToken.IsCancellationRequested)
return;
cancellation.ThrowIfCancellationRequested();
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure about this? Won't this be logged (to the user also) as an unobserved exception? I mean... see the try-catch below which even swallows the OperationCanceledException

Copy link
Contributor

Choose a reason for hiding this comment

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

The pauseIfNecessaryAsync method also throws and will likely do the same thing too outside of the ForEachAsync here (maybe a reproduction for that is enter gameplay, start import from stable, then cancel the notification?).

Copy link
Member Author

@peppy peppy Nov 14, 2024

Choose a reason for hiding this comment

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

It definitely doesn't come up as an unobserved else I would have noticed.

You can test using:

diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs
index c4eb93b754..addba3f338 100644
--- a/osu.Game/Database/RealmArchiveModelImporter.cs
+++ b/osu.Game/Database/RealmArchiveModelImporter.cs
@@ -119,6 +119,9 @@ public async Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notific
 
             await Parallel.ForEachAsync(tasks, notification.CancellationToken, async (task, cancellation) =>
             {
+                while (!notification.CancellationToken.IsCancellationRequested)
+                    Thread.Sleep(1000);
+
                 cancellation.ThrowIfCancellationRequested();
 
                 try
@@ -139,6 +142,7 @@ await Parallel.ForEachAsync(tasks, notification.CancellationToken, async (task,
                 }
                 catch (OperationCanceledException)
                 {
+                    throw;
                 }
                 catch (Exception e)
                 {
@@ -531,7 +535,8 @@ protected virtual void PostImport(TModel model, Realm realm, ImportParameters pa
         /// <param name="model">The new model proposed for import.</param>
         /// <param name="realm">The current realm context.</param>
         /// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
-        protected TModel? CheckForExisting(TModel model, Realm realm) => string.IsNullOrEmpty(model.Hash) ? null : realm.All<TModel>().OrderBy(b => b.DeletePending).FirstOrDefault(b => b.Hash == model.Hash);
+        protected TModel? CheckForExisting(TModel model, Realm realm) =>
+            string.IsNullOrEmpty(model.Hash) ? null : realm.All<TModel>().OrderBy(b => b.DeletePending).FirstOrDefault(b => b.Hash == model.Hash);
 
         /// <summary>
         /// Whether import can be skipped after finding an existing import early in the process.

Debugging through I see a TaskCanceledException but it gracefully handles somewhere (debugger doesn't let me see). But maybe there's a scenario where that's not the case? I couldn't find one.

Using ThrowIfCancellationRequested is recommended way to handle this, so I'd be inclined to add a catch somewhere if it's required, rather than change the calls.

Copy link
Contributor

@smoogipoo smoogipoo Nov 18, 2024

Choose a reason for hiding this comment

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

It is causing an issue with your diff. When you cancel the notification the download/download buttons (in beatmap listing) remain active in an "importing" state, with no way to restore.

request.Success += filename =>
{
Task.Factory.StartNew(async () =>
{
bool importSuccessful;
if (originalModel != null)
importSuccessful = (await importer.ImportAsUpdate(notification, new ImportTask(filename), originalModel).ConfigureAwait(false)) != null;
else
importSuccessful = (await importer.Import(notification, new[] { new ImportTask(filename) }).ConfigureAwait(false)).Any();
// for now a failed import will be marked as a failed download for simplicity.
if (!importSuccessful)
DownloadFailed?.Invoke(request);
CurrentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning);
};

I'm seeing nothing special about what's happening here, it just looks like OperationCanceledExceptions are never unobserved / don't reach that handler. Throwing InvalidOperationException works, throwing OperationCanceledException doesn't 😅

Also, it's actually even caused by just passing into the Parallel.ForEachAsync() itself without throwing from inside the callback delegate...

Copy link
Member Author

Choose a reason for hiding this comment

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

I've moved all the "clean up" actions in this flow to use try-finally for safety. Seems to work as expected now.

@pull-request-size pull-request-size bot added size/L and removed size/M labels Nov 18, 2024
@smoogipoo smoogipoo merged commit 53f0bef into ppy:master Nov 20, 2024
10 checks passed
@peppy peppy deleted the import-paused-visibility branch November 22, 2024 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:import dealing with importing of data from stable or file formats size/L
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants