Clean up cancellation handling in `WorkingBeatmap`

After the recent changes introducing cancellation support to
`WorkingBeatmap`, it turned out that if the cancellation support was
used, `GetPlayableBeatmap()` would raise timeout exceptions rather than
the expected `OperationCanceledException`.

To that end, split off a separate overload for the typical usage, that
catches `OperationCanceledException` and converts them to beatmap load
timeout exceptions, and use normal `OperationCanceledException`s in the
overload that requires a cancellation token to work.
This commit is contained in:
Bartłomiej Dach 2021-11-20 17:23:55 +01:00
parent 15feb17da8
commit 6100bf66a6
No known key found for this signature in database
GPG Key ID: BCECCD4FA41F6497
4 changed files with 48 additions and 35 deletions

View File

@ -9,6 +9,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
@ -40,7 +41,7 @@ public void TestGetPlayableCancellationToken()
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
loadStarted.Set(); loadStarted.Set();
Assert.Throws<OperationCanceledException>(() => working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, cancellationToken: cts.Token)); Assert.Throws<OperationCanceledException>(() => working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>(), cts.Token));
loadCompleted.Set(); loadCompleted.Set();
}, TaskCreationOptions.LongRunning); }, TaskCreationOptions.LongRunning);
@ -58,7 +59,7 @@ public void TestGetPlayableDefaultTimeout()
{ {
var working = new TestNeverLoadsWorkingBeatmap(); var working = new TestNeverLoadsWorkingBeatmap();
Assert.Throws<OperationCanceledException>(() => working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo)); Assert.Throws(Is.InstanceOf<TimeoutException>(), () => working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
working.ResetEvent.Set(); working.ResetEvent.Set();
} }

View File

@ -90,12 +90,31 @@ public interface IWorkingBeatmap
/// have been applied, and <see cref="HitObject"/>s have been fully constructed. /// have been applied, and <see cref="HitObject"/>s have been fully constructed.
/// </para> /// </para>
/// </summary> /// </summary>
/// <remarks>
/// By default, the beatmap load process will be interrupted after 10 seconds.
/// For finer-grained control over the load process, use the
/// <see cref="GetPlayableBeatmap(osu.Game.Rulesets.IRulesetInfo,System.Collections.Generic.IReadOnlyList{osu.Game.Rulesets.Mods.Mod},System.Threading.CancellationToken)"/>
/// overload instead.
/// </remarks>
/// <param name="ruleset">The <see cref="RulesetInfo"/> to create a playable <see cref="IBeatmap"/> for.</param> /// <param name="ruleset">The <see cref="RulesetInfo"/> to create a playable <see cref="IBeatmap"/> for.</param>
/// <param name="mods">The <see cref="Mod"/>s to apply to the <see cref="IBeatmap"/>.</param> /// <param name="mods">The <see cref="Mod"/>s to apply to the <see cref="IBeatmap"/>.</param>
/// <param name="cancellationToken">Cancellation token that cancels the beatmap loading process. If not provided, a default timeout of 10,000ms will be applied to the load process.</param>
/// <returns>The converted <see cref="IBeatmap"/>.</returns> /// <returns>The converted <see cref="IBeatmap"/>.</returns>
/// <exception cref="BeatmapInvalidForRulesetException">If <see cref="Beatmap"/> could not be converted to <paramref name="ruleset"/>.</exception> /// <exception cref="BeatmapInvalidForRulesetException">If <see cref="Beatmap"/> could not be converted to <paramref name="ruleset"/>.</exception>
IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods = null, CancellationToken? cancellationToken = null); IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, params Mod[] mods);
/// <summary>
/// Constructs a playable <see cref="IBeatmap"/> from <see cref="Beatmap"/> using the applicable converters for a specific <see cref="RulesetInfo"/>.
/// <para>
/// The returned <see cref="IBeatmap"/> is in a playable state - all <see cref="HitObject"/> and <see cref="BeatmapDifficulty"/> <see cref="Mod"/>s
/// have been applied, and <see cref="HitObject"/>s have been fully constructed.
/// </para>
/// </summary>
/// <param name="ruleset">The <see cref="RulesetInfo"/> to create a playable <see cref="IBeatmap"/> for.</param>
/// <param name="mods">The <see cref="Mod"/>s to apply to the <see cref="IBeatmap"/>.</param>
/// <param name="cancellationToken">Cancellation token that cancels the beatmap loading process.</param>
/// <returns>The converted <see cref="IBeatmap"/>.</returns>
/// <exception cref="BeatmapInvalidForRulesetException">If <see cref="Beatmap"/> could not be converted to <paramref name="ruleset"/>.</exception>
IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Load a new audio track instance for this beatmap. This should be called once before accessing <see cref="Track"/>. /// Load a new audio track instance for this beatmap. This should be called once before accessing <see cref="Track"/>.

View File

@ -79,14 +79,24 @@ protected virtual Track GetVirtualTrack(double emptyLength = 0)
/// <returns>The applicable <see cref="IBeatmapConverter"/>.</returns> /// <returns>The applicable <see cref="IBeatmapConverter"/>.</returns>
protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap); protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap);
public virtual IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods = null, CancellationToken? cancellationToken = null) public IBeatmap GetPlayableBeatmap([NotNull] IRulesetInfo ruleset, params Mod[] mods)
{ {
var token = cancellationToken ?? try
// don't apply the default timeout when debugger is attached (may be breakpointing / debugging). {
(Debugger.IsAttached ? new CancellationToken() : new CancellationTokenSource(10000).Token); using (var cancellationTokenSource = new CancellationTokenSource(10_000))
{
mods ??= Array.Empty<Mod>(); // don't apply the default timeout when debugger is attached (may be breakpointing / debugging).
return GetPlayableBeatmap(ruleset, mods, Debugger.IsAttached ? new CancellationToken() : cancellationTokenSource.Token);
}
}
catch (OperationCanceledException)
{
throw new BeatmapLoadTimeoutException(BeatmapInfo);
}
}
public virtual IBeatmap GetPlayableBeatmap([NotNull] IRulesetInfo ruleset, [NotNull] IReadOnlyList<Mod> mods, CancellationToken token)
{
var rulesetInstance = ruleset.CreateInstance(); var rulesetInstance = ruleset.CreateInstance();
if (rulesetInstance == null) if (rulesetInstance == null)
@ -101,9 +111,7 @@ public virtual IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<M
// Apply conversion mods // Apply conversion mods
foreach (var mod in mods.OfType<IApplicableToBeatmapConverter>()) foreach (var mod in mods.OfType<IApplicableToBeatmapConverter>())
{ {
if (token.IsCancellationRequested) token.ThrowIfCancellationRequested();
throw new BeatmapLoadTimeoutException(BeatmapInfo);
mod.ApplyToBeatmapConverter(converter); mod.ApplyToBeatmapConverter(converter);
} }
@ -113,9 +121,7 @@ public virtual IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<M
// Apply conversion mods to the result // Apply conversion mods to the result
foreach (var mod in mods.OfType<IApplicableAfterBeatmapConversion>()) foreach (var mod in mods.OfType<IApplicableAfterBeatmapConversion>())
{ {
if (token.IsCancellationRequested) token.ThrowIfCancellationRequested();
throw new BeatmapLoadTimeoutException(BeatmapInfo);
mod.ApplyToBeatmap(converted); mod.ApplyToBeatmap(converted);
} }
@ -124,9 +130,7 @@ public virtual IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<M
{ {
foreach (var mod in mods.OfType<IApplicableToDifficulty>()) foreach (var mod in mods.OfType<IApplicableToDifficulty>())
{ {
if (token.IsCancellationRequested) token.ThrowIfCancellationRequested();
throw new BeatmapLoadTimeoutException(BeatmapInfo);
mod.ApplyToDifficulty(converted.Difficulty); mod.ApplyToDifficulty(converted.Difficulty);
} }
} }
@ -139,28 +143,17 @@ public virtual IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<M
processor?.PreProcess(); processor?.PreProcess();
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
try foreach (var obj in converted.HitObjects)
{ {
foreach (var obj in converted.HitObjects) token.ThrowIfCancellationRequested();
{ obj.ApplyDefaults(converted.ControlPointInfo, converted.Difficulty, token);
if (token.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
obj.ApplyDefaults(converted.ControlPointInfo, converted.Difficulty, token);
}
}
catch (OperationCanceledException)
{
throw new BeatmapLoadTimeoutException(BeatmapInfo);
} }
foreach (var mod in mods.OfType<IApplicableToHitObject>()) foreach (var mod in mods.OfType<IApplicableToHitObject>())
{ {
foreach (var obj in converted.HitObjects) foreach (var obj in converted.HitObjects)
{ {
if (token.IsCancellationRequested) token.ThrowIfCancellationRequested();
throw new BeatmapLoadTimeoutException(BeatmapInfo);
mod.ApplyToHitObject(obj); mod.ApplyToHitObject(obj);
} }
} }

View File

@ -216,7 +216,7 @@ public GameplayWorkingBeatmap(IBeatmap gameplayBeatmap)
this.gameplayBeatmap = gameplayBeatmap; this.gameplayBeatmap = gameplayBeatmap;
} }
public override IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods = null, CancellationToken? cancellationToken = null) public override IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods, CancellationToken cancellationToken)
=> gameplayBeatmap; => gameplayBeatmap;
protected override IBeatmap GetBeatmap() => gameplayBeatmap; protected override IBeatmap GetBeatmap() => gameplayBeatmap;