mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 14:32:55 +08:00
Fix API request potentially firing failed events after completion
Specifically, `Cancel()` calls were not thread safe. Due to a series of events, `ListPollingComponent` could call `Cancel` from a non-update thread, leading to a race condition where both a `Success` and `Fail` event can be fired. This is intended to be the simplest fix possible, locking and guarding specifically on the callbacks. Further work could be done in the future to improve the flow surrounding `pendingFailure`, potentially reducing redundant work and cleaning up the code, but that's not happening here. Closes https://github.com/ppy/osu/issues/13632.
This commit is contained in:
parent
23ef666f34
commit
80c2b1449b
15
osu.Game/Online/API/APIException.cs
Normal file
15
osu.Game/Online/API/APIException.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
{
|
||||
public class APIException : InvalidOperationException
|
||||
{
|
||||
public APIException(string messsage, Exception innerException)
|
||||
: base(messsage, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -79,7 +79,13 @@ namespace osu.Game.Online.API
|
||||
/// </summary>
|
||||
public event APIFailureHandler Failure;
|
||||
|
||||
private bool cancelled;
|
||||
private readonly object completionStateLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// The state of this request, from an outside perspective.
|
||||
/// This is used to ensure correct notification events are fired.
|
||||
/// </summary>
|
||||
private APIRequestCompletionState completionState;
|
||||
|
||||
private Action pendingFailure;
|
||||
|
||||
@ -116,12 +122,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
PostProcess();
|
||||
|
||||
API.Schedule(delegate
|
||||
{
|
||||
if (cancelled) return;
|
||||
|
||||
TriggerSuccess();
|
||||
});
|
||||
API.Schedule(TriggerSuccess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -131,16 +132,29 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
}
|
||||
|
||||
private bool succeeded;
|
||||
|
||||
internal virtual void TriggerSuccess()
|
||||
{
|
||||
succeeded = true;
|
||||
lock (completionStateLock)
|
||||
{
|
||||
if (completionState != APIRequestCompletionState.Waiting)
|
||||
return;
|
||||
|
||||
completionState = APIRequestCompletionState.Completed;
|
||||
}
|
||||
|
||||
Success?.Invoke();
|
||||
}
|
||||
|
||||
internal void TriggerFailure(Exception e)
|
||||
{
|
||||
lock (completionStateLock)
|
||||
{
|
||||
if (completionState != APIRequestCompletionState.Waiting)
|
||||
return;
|
||||
|
||||
completionState = APIRequestCompletionState.Failed;
|
||||
}
|
||||
|
||||
Failure?.Invoke(e);
|
||||
}
|
||||
|
||||
@ -148,10 +162,14 @@ namespace osu.Game.Online.API
|
||||
|
||||
public void Fail(Exception e)
|
||||
{
|
||||
if (succeeded || cancelled)
|
||||
return;
|
||||
lock (completionStateLock)
|
||||
{
|
||||
// while it doesn't matter if code following this check is run more than once,
|
||||
// this avoids unnecessarily performing work where we are already sure the user has been informed.
|
||||
if (completionState != APIRequestCompletionState.Waiting)
|
||||
return;
|
||||
}
|
||||
|
||||
cancelled = true;
|
||||
WebRequest?.Abort();
|
||||
|
||||
string responseString = WebRequest?.GetResponseString();
|
||||
@ -181,7 +199,11 @@ namespace osu.Game.Online.API
|
||||
/// <returns>Whether we are in a failed or cancelled state.</returns>
|
||||
private bool checkAndScheduleFailure()
|
||||
{
|
||||
if (pendingFailure == null) return cancelled;
|
||||
lock (completionStateLock)
|
||||
{
|
||||
if (pendingFailure == null)
|
||||
return completionState == APIRequestCompletionState.Failed;
|
||||
}
|
||||
|
||||
if (API == null)
|
||||
pendingFailure();
|
||||
@ -199,14 +221,6 @@ namespace osu.Game.Online.API
|
||||
}
|
||||
}
|
||||
|
||||
public class APIException : InvalidOperationException
|
||||
{
|
||||
public APIException(string messsage, Exception innerException)
|
||||
: base(messsage, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public delegate void APIFailureHandler(Exception e);
|
||||
|
||||
public delegate void APISuccessHandler();
|
||||
|
23
osu.Game/Online/API/APIRequestCompletionState.cs
Normal file
23
osu.Game/Online/API/APIRequestCompletionState.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
{
|
||||
public enum APIRequestCompletionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Not yet run or currently waiting on response.
|
||||
/// </summary>
|
||||
Waiting,
|
||||
|
||||
/// <summary>
|
||||
/// Ran to completion.
|
||||
/// </summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>
|
||||
/// Cancelled or failed due to error.
|
||||
/// </summary>
|
||||
Failed
|
||||
}
|
||||
}
|
@ -70,7 +70,7 @@ namespace osu.Game.Online
|
||||
return true;
|
||||
}
|
||||
|
||||
// not ennough time has passed since the last poll. we do want to schedule a poll to happen, though.
|
||||
// not enough time has passed since the last poll. we do want to schedule a poll to happen, though.
|
||||
scheduleNextPoll();
|
||||
return false;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user