-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Conversation
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Test failures need resolution here. |
if (notification.CancellationToken.IsCancellationRequested) | ||
return; | ||
cancellation.ThrowIfCancellationRequested(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
osu/osu.Game/Database/ModelDownloader.cs
Lines 67 to 84 in 5276f77
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 OperationCanceledException
s 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...
There was a problem hiding this comment.
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.
Mostly to see if it breaks anything.
Addresses #30388.
osu.2024-10-23.at.08.50.25.mp4