From 865d63f7681b6b2eb10f4e8ef5234f576e86942c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Aug 2022 15:43:39 +0900 Subject: [PATCH] Refactor `APIAccess` main loop to read better --- osu.Game/Online/API/APIAccess.cs | 273 ++++++++++++++++--------------- 1 file changed, 143 insertions(+), 130 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 72c88b1e92..a0c8e0d555 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -104,127 +104,39 @@ namespace osu.Game.Online.API /// private int failureCount; + /// + /// The main API thread loop, which will continue to run until the game is shut down. + /// private void run() { while (!cancellationToken.IsCancellationRequested) { - switch (State.Value) + if (state.Value == APIState.Failing) { - case APIState.Failing: - //todo: replace this with a ping request. - log.Add(@"In a failing state, waiting a bit before we try again..."); - Thread.Sleep(5000); + // To recover from a failing state, falling through and running the full reconnection process seems safest for now. + // This could probably be replaced with a ping-style request if we want to avoid the reconnection overheads. + log.Add($@"{nameof(APIAccess)} is in a failing state, waiting a bit before we try again..."); + Thread.Sleep(5000); + } - if (!IsLoggedIn) goto case APIState.Connecting; + // Ensure that we have valid credentials. + // If not, setting the offline state will allow the game to prompt the user to provide new credentials. + if (!HasLogin) + { + state.Value = APIState.Offline; + Thread.Sleep(50); + continue; + } - if (queue.Count == 0) - { - log.Add(@"Queueing a ping request"); - Queue(new GetUserRequest()); - } + Debug.Assert(HasLogin); - break; + // Ensure that we are in an online state. If not, attempt a connect. + if (state.Value != APIState.Online) + { + attemptConnect(); - case APIState.Offline: - case APIState.Connecting: - // work to restore a connection... - if (!HasLogin) - { - state.Value = APIState.Offline; - Thread.Sleep(50); - continue; - } - - state.Value = APIState.Connecting; - - if (localUser.IsDefault) - { - // Show a placeholder user if saved credentials are available. - // This is useful for storing local scores and showing a placeholder username after starting the game, - // until a valid connection has been established. - setLocalUser(new APIUser - { - Username = ProvidedUsername, - }); - } - - // save the username at this point, if the user requested for it to be. - config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); - - if (!authentication.HasValidAccessToken) - { - LastLoginError = null; - - try - { - authentication.AuthenticateWithLogin(ProvidedUsername, password); - } - catch (Exception e) - { - //todo: this fails even on network-related issues. we should probably handle those differently. - LastLoginError = e; - log.Add(@"Login failed!"); - password = null; - authentication.Clear(); - continue; - } - } - - var userReq = new GetUserRequest(); - - userReq.Failure += ex => - { - if (ex is APIException) - { - LastLoginError = ex; - log.Add("Login failed on local user retrieval!"); - Logout(); - } - else if (ex is WebException webException && webException.Message == @"Unauthorized") - { - log.Add(@"Login no longer valid"); - Logout(); - } - else - failConnectionProcess(); - }; - userReq.Success += user => - { - // todo: save/pull from settings - user.Status.Value = new UserStatusOnline(); - - setLocalUser(user); - - //we're connected! - state.Value = APIState.Online; - failureCount = 0; - }; - - if (!handleRequest(userReq)) - { - failConnectionProcess(); - continue; - } - - // getting user's friends is considered part of the connection process. - var friendsReq = new GetFriendsRequest(); - - friendsReq.Failure += _ => failConnectionProcess(); - friendsReq.Success += res => friends.AddRange(res); - - if (!handleRequest(friendsReq)) - { - failConnectionProcess(); - continue; - } - - // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. - // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests - // before actually going online. - while (State.Value > APIState.Offline && State.Value < APIState.Online) - Thread.Sleep(500); - - break; + if (state.Value != APIState.Online) + continue; } // hard bail if we can't get a valid access token. @@ -234,31 +146,132 @@ namespace osu.Game.Online.API continue; } - while (true) - { - APIRequest req; - - lock (queue) - { - if (queue.Count == 0) break; - - req = queue.Dequeue(); - } - - handleRequest(req); - } - + processQueuedRequests(); Thread.Sleep(50); } + } - void failConnectionProcess() + /// + /// Dequeue from the queue and run each request synchronously until the queue is empty. + /// + private void processQueuedRequests() + { + while (true) { - // if something went wrong during the connection process, we want to reset the state (but only if still connecting). - if (State.Value == APIState.Connecting) - state.Value = APIState.Failing; + APIRequest req; + + lock (queue) + { + if (queue.Count == 0) return; + + req = queue.Dequeue(); + } + + handleRequest(req); } } + /// + /// From a non-connected state, perform a full connection flow, obtaining OAuth tokens and populating the local user and friends. + /// + /// + /// This method takes control of and transitions from to either + /// - (successful connection) + /// - (failed connection but retrying) + /// - (failed and can't retry, clear credentials and require user interaction) + /// + /// Whether the connection attempt was successful. + private void attemptConnect() + { + state.Value = APIState.Connecting; + + if (localUser.IsDefault) + { + // Show a placeholder user if saved credentials are available. + // This is useful for storing local scores and showing a placeholder username after starting the game, + // until a valid connection has been established. + setLocalUser(new APIUser + { + Username = ProvidedUsername, + }); + } + + // save the username at this point, if the user requested for it to be. + config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); + + if (!authentication.HasValidAccessToken) + { + LastLoginError = null; + + try + { + authentication.AuthenticateWithLogin(ProvidedUsername, password); + } + catch (Exception e) + { + //todo: this fails even on network-related issues. we should probably handle those differently. + LastLoginError = e; + log.Add($@"Login failed for username {ProvidedUsername} ({LastLoginError.Message})!"); + + Logout(); + return; + } + } + + var userReq = new GetUserRequest(); + userReq.Failure += ex => + { + if (ex is APIException) + { + LastLoginError = ex; + log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!"); + Logout(); + } + else if (ex is WebException webException && webException.Message == @"Unauthorized") + { + log.Add(@"Login no longer valid"); + Logout(); + } + else + { + state.Value = APIState.Failing; + } + }; + userReq.Success += user => + { + // todo: save/pull from settings + user.Status.Value = new UserStatusOnline(); + + setLocalUser(user); + + // we're connected! + state.Value = APIState.Online; + failureCount = 0; + }; + + if (!handleRequest(userReq)) + { + state.Value = APIState.Failing; + return; + } + + var friendsReq = new GetFriendsRequest(); + friendsReq.Failure += _ => state.Value = APIState.Failing; + friendsReq.Success += res => friends.AddRange(res); + + if (!handleRequest(friendsReq)) + { + state.Value = APIState.Failing; + return; + } + + // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. + // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests + // before actually going online. + while (State.Value == APIState.Connecting && !cancellationToken.IsCancellationRequested) + Thread.Sleep(500); + } + public void Perform(APIRequest request) { try