diff --git a/osu.Game/Online/IStatefulUserHubClient.cs b/osu.Game/Online/IStatefulUserHubClient.cs
index e931e3c862..3b0959362f 100644
--- a/osu.Game/Online/IStatefulUserHubClient.cs
+++ b/osu.Game/Online/IStatefulUserHubClient.cs
@@ -26,5 +26,18 @@ namespace osu.Game.Online
///
///
Task DisconnectRequested();
+
+ ///
+ /// Invoked when server begins a shutdown sequence.
+ ///
+ ///
+ /// Server shutdowns are graceful.
+ ///
+ /// This will fire with hours of notice for clients to do what they need to and subsequently
+ /// disconnect. It's in the client's best interest to switch over to the new hubs as soon as
+ /// it can, so that the user can be on the same server as the majority of others (and avoid a
+ /// "server split" scenario where users are split across multiple shutting-down hubs).
+ ///
+ Task ServerShuttingDown();
}
}
diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs
index e70fe6d3bb..18e5c92b71 100644
--- a/osu.Game/Online/Metadata/MetadataClient.cs
+++ b/osu.Game/Online/Metadata/MetadataClient.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -19,6 +20,11 @@ namespace osu.Game.Online.Metadata
{
public abstract IBindable IsConnected { get; }
+ ///
+ /// A list of all watched multiplayer rooms (see ).
+ ///
+ protected readonly HashSet WatchedRooms = new HashSet();
+
[Resolved]
private IAPIProvider api { get; set; } = null!;
@@ -179,11 +185,28 @@ namespace osu.Game.Online.Metadata
#region Disconnection handling
+ ///
+ /// Invoked just prior to disconnection.
+ ///
public event Action? Disconnecting;
- public virtual Task DisconnectRequested()
+ public abstract Task Reconnect();
+
+ protected abstract Task DisconnectInternal();
+
+ Task IStatefulUserHubClient.DisconnectRequested()
{
- Schedule(() => Disconnecting?.Invoke());
+ Schedule(() =>
+ {
+ Disconnecting?.Invoke();
+ DisconnectInternal().FireAndForget();
+ });
+ return Task.CompletedTask;
+ }
+
+ Task IStatefulUserHubClient.ServerShuttingDown()
+ {
+ this.ReconnectWhenReady(IsConnected, () => WatchedRooms.Count == 0, Reconnect);
return Task.CompletedTask;
}
diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
index a30268d7bb..6e4f7e4d6d 100644
--- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs
+++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
@@ -69,7 +69,8 @@ namespace osu.Game.Online.Metadata
connection.On(nameof(IMetadataClient.FriendPresenceUpdated), ((IMetadataClient)this).FriendPresenceUpdated);
connection.On(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated);
connection.On(nameof(IMetadataClient.MultiplayerRoomScoreSet), ((IMetadataClient)this).MultiplayerRoomScoreSet);
- connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested);
+ connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
+ connection.On(nameof(IStatefulUserHubClient.ServerShuttingDown), ((IStatefulUserHubClient)this).ServerShuttingDown);
};
IsConnected.BindTo(connector.IsConnected);
@@ -109,6 +110,7 @@ namespace osu.Game.Online.Metadata
{
userPresences.Clear();
friendPresences.Clear();
+ WatchedRooms.Clear();
dailyChallengeInfo.Value = null;
localUserPresence = default;
});
@@ -272,6 +274,8 @@ namespace osu.Game.Online.Metadata
if (connector?.IsConnected.Value != true)
throw new OperationCanceledException();
+ WatchedRooms.Add(id);
+
Debug.Assert(connection != null);
var result = await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching multiplayer room with ID {id}", LoggingTarget.Network);
@@ -283,6 +287,8 @@ namespace osu.Game.Online.Metadata
if (connector?.IsConnected.Value != true)
throw new OperationCanceledException();
+ WatchedRooms.Remove(id);
+
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom), id).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching multiplayer room with ID {id}", LoggingTarget.Network);
@@ -297,17 +303,22 @@ namespace osu.Game.Online.Metadata
await connection.InvokeAsync(nameof(IMetadataServer.RefreshFriends)).ConfigureAwait(false);
}
- public override async Task DisconnectRequested()
- {
- await base.DisconnectRequested().ConfigureAwait(false);
- if (connector != null)
- await connector.Disconnect().ConfigureAwait(false);
- }
-
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
connector?.Dispose();
}
+
+ public override async Task Reconnect()
+ {
+ if (connector != null)
+ await connector.Reconnect().ConfigureAwait(false);
+ }
+
+ protected override async Task DisconnectInternal()
+ {
+ if (connector != null)
+ await connector.Disconnect().ConfigureAwait(false);
+ }
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index cc6d276c88..341739dbed 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -11,9 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
using osu.Game.Database;
-using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -117,11 +115,6 @@ namespace osu.Game.Online.Multiplayer
///
public event Action? ResultsReady;
- ///
- /// Invoked just prior to disconnection requested by the server via .
- ///
- public event Action? Disconnecting;
-
public event Action? CountdownStarted;
public event Action? CountdownStopped;
@@ -490,8 +483,6 @@ namespace osu.Game.Online.Multiplayer
public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);
- public abstract Task DisconnectInternal();
-
public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId);
///
@@ -1082,15 +1073,7 @@ namespace osu.Game.Online.Multiplayer
});
}
- Task IStatefulUserHubClient.DisconnectRequested()
- {
- Schedule(() =>
- {
- Disconnecting?.Invoke();
- DisconnectInternal();
- });
- return Task.CompletedTask;
- }
+ #region Matchmaking / Ranked Play
Task IMatchmakingClient.MatchmakingQueueJoined()
{
@@ -1240,14 +1223,35 @@ namespace osu.Game.Online.Multiplayer
public abstract Task MatchmakingSkipToNextStage();
- private partial class MultiplayerInvitationNotification : UserAvatarNotification
- {
- protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
+ #endregion
- public MultiplayerInvitationNotification(APIUser user, Room room)
- : base(user, NotificationsStrings.InvitedYouToTheMultiplayer(user.Username, room.Name))
+ #region Disconnection handling
+
+ ///
+ /// Invoked just prior to disconnection.
+ ///
+ public event Action? Disconnecting;
+
+ protected abstract Task DisconnectInternal();
+
+ public abstract Task Reconnect();
+
+ Task IStatefulUserHubClient.DisconnectRequested()
+ {
+ Schedule(() =>
{
- }
+ Disconnecting?.Invoke();
+ DisconnectInternal().FireAndForget();
+ });
+ return Task.CompletedTask;
}
+
+ Task IStatefulUserHubClient.ServerShuttingDown()
+ {
+ this.ReconnectWhenReady(IsConnected, () => room == null, Reconnect);
+ return Task.CompletedTask;
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
index 83d9e64a36..71dd527d00 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
@@ -5,7 +5,9 @@ using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
+using osu.Framework.Bindables;
using osu.Framework.Extensions.ExceptionExtensions;
+using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Logging;
using osu.Game.Utils;
@@ -43,6 +45,43 @@ namespace osu.Game.Online.Multiplayer
}
});
+ ///
+ /// Start a background process to disconnect/reconnect as soon as a specific condition is met.
+ ///
+ ///
+ /// If a reconnect happens via another means, this will abort attempts.
+ /// We only want to reconnect once.
+ ///
+ /// The client to operate on.
+ /// Connected state of client.
+ /// The condition which should be true to continue with the shutdown.
+ /// The method to run to perform the reconnect.
+ public static void ReconnectWhenReady(this IStatefulUserHubClient client, IBindable isConnected, Func readyFunction, Func reconnectFunction)
+ {
+ Task.Run(async () =>
+ {
+ bool didReconnect = false;
+ var connected = isConnected.GetBoundCopy();
+ connected.ValueChanged += _ => didReconnect = true;
+
+ string clientName = client.GetType().ReadableName();
+
+ Logger.Log($"{clientName} has signalled shutdown");
+
+ while (!readyFunction())
+ {
+ Logger.Log($"{clientName} shutdown waiting for idle conditions...");
+ await Task.Delay(10000).ConfigureAwait(false);
+ }
+
+ Logger.Log($"{clientName} disconnecting due to shutdown signal");
+ if (!didReconnect)
+ await reconnectFunction().ConfigureAwait(false);
+
+ connected.UnbindAll();
+ }).FireAndForget();
+ }
+
public static string? GetHubExceptionMessage(this Exception exception)
{
if (exception is HubException hubException)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerInvitationNotification.cs b/osu.Game/Online/Multiplayer/MultiplayerInvitationNotification.cs
new file mode 100644
index 0000000000..d8ec230ad2
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MultiplayerInvitationNotification.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Localisation;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays.Notifications;
+
+namespace osu.Game.Online.Multiplayer
+{
+ public partial class MultiplayerInvitationNotification : UserAvatarNotification
+ {
+ protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
+
+ public MultiplayerInvitationNotification(APIUser user, Room room)
+ : base(user, NotificationsStrings.InvitedYouToTheMultiplayer(user.Username, room.Name))
+ {
+ }
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 4fa8c15749..fedddce45b 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -10,15 +10,15 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Game.Online.API;
-using osu.Game.Online.Rooms;
-using osu.Game.Overlays.Notifications;
using osu.Game.Localisation;
+using osu.Game.Online.API;
using osu.Game.Online.Matchmaking;
using osu.Game.Online.Matchmaking.Requests;
using osu.Game.Online.Matchmaking.Responses;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.RankedPlay;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays.Notifications;
namespace osu.Game.Online.Multiplayer
{
@@ -92,7 +92,8 @@ namespace osu.Game.Online.Multiplayer
connection.On(nameof(IRankedPlayClient.RankedPlayCardRevealed), ((IRankedPlayClient)this).RankedPlayCardRevealed);
connection.On(nameof(IRankedPlayClient.RankedPlayCardPlayed), ((IRankedPlayClient)this).RankedPlayCardPlayed);
- connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested);
+ connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
+ connection.On(nameof(IStatefulUserHubClient.ServerShuttingDown), ((IStatefulUserHubClient)this).ServerShuttingDown);
};
IsConnected.BindTo(connector.IsConnected);
@@ -335,14 +336,6 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkipIntro));
}
- public override Task DisconnectInternal()
- {
- if (connector == null)
- return Task.CompletedTask;
-
- return connector.Disconnect();
- }
-
public override Task DiscardCards(RankedPlayCardItem[] cards)
{
if (!IsConnected.Value)
@@ -467,5 +460,17 @@ namespace osu.Game.Online.Multiplayer
base.Dispose(isDisposing);
connector?.Dispose();
}
+
+ public override async Task Reconnect()
+ {
+ if (connector != null)
+ await connector.Reconnect().ConfigureAwait(false);
+ }
+
+ protected override async Task DisconnectInternal()
+ {
+ if (connector != null)
+ await connector.Disconnect().ConfigureAwait(false);
+ }
}
}
diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
index 22f8cfbf2b..0ac77d6b35 100644
--- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
+++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
@@ -46,6 +46,7 @@ namespace osu.Game.Online.Spectator
connection.On(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching);
connection.On(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
+ connection.On(nameof(IStatefulUserHubClient.ServerShuttingDown), ((IStatefulUserHubClient)this).ServerShuttingDown);
};
IsConnected.BindTo(connector.IsConnected);
@@ -122,14 +123,16 @@ namespace osu.Game.Online.Spectator
return connection.InvokeAsync(nameof(ISpectatorServer.EndWatchingUser), userId);
}
+ public override async Task Reconnect()
+ {
+ if (connector != null)
+ await connector.Reconnect().ConfigureAwait(false);
+ }
+
protected override async Task DisconnectInternal()
{
- await base.DisconnectInternal().ConfigureAwait(false);
-
- if (connector == null)
- return;
-
- await connector.Disconnect().ConfigureAwait(false);
+ if (connector != null)
+ await connector.Disconnect().ConfigureAwait(false);
}
}
}
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index 5c99eb5208..7b62d64b41 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -75,11 +75,6 @@ namespace osu.Game.Online.Spectator
///
public event Action? OnUserScoreProcessed;
- ///
- /// Invoked just prior to disconnection requested by the server via .
- ///
- public event Action? Disconnecting;
-
///
/// A dictionary containing all users currently being watched, with the number of watching components for each user.
///
@@ -203,12 +198,6 @@ namespace osu.Game.Online.Spectator
return Task.CompletedTask;
}
- Task IStatefulUserHubClient.DisconnectRequested()
- {
- Schedule(() => DisconnectInternal().FireAndForget());
- return Task.CompletedTask;
- }
-
public void BeginPlaying(long? scoreToken, GameplayState state, Score score)
{
// This schedule is only here to match the one below in `EndPlaying`.
@@ -373,12 +362,6 @@ namespace osu.Game.Online.Spectator
protected abstract Task StopWatchingUserInternal(int userId);
- protected virtual Task DisconnectInternal()
- {
- Disconnecting?.Invoke();
- return Task.CompletedTask;
- }
-
protected override void Update()
{
base.Update();
@@ -446,5 +429,34 @@ namespace osu.Game.Online.Spectator
});
});
}
+
+ #region Disconnection handling
+
+ ///
+ /// Invoked just prior to disconnection.
+ ///
+ public event Action? Disconnecting;
+
+ protected abstract Task DisconnectInternal();
+
+ public abstract Task Reconnect();
+
+ Task IStatefulUserHubClient.DisconnectRequested()
+ {
+ Schedule(() =>
+ {
+ Disconnecting?.Invoke();
+ DisconnectInternal().FireAndForget();
+ });
+ return Task.CompletedTask;
+ }
+
+ Task IStatefulUserHubClient.ServerShuttingDown()
+ {
+ this.ReconnectWhenReady(IsConnected, () => watchedUsersRefCounts.Count == 0, Reconnect);
+ return Task.CompletedTask;
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs
index 5ded0601df..0aadd7dddc 100644
--- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs
+++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs
@@ -136,9 +136,17 @@ namespace osu.Game.Tests.Visual.Metadata
dailyChallengeInfo.Value = null;
}
- public void Reconnect()
+ public override Task Reconnect()
{
isConnected.Value = true;
+
+ return Task.CompletedTask;
+ }
+
+ protected override Task DisconnectInternal()
+ {
+ Disconnect();
+ return Task.CompletedTask;
}
}
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 14b308e905..17455c1b43 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -824,9 +824,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
return MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
}
- public override Task DisconnectInternal()
+ protected override Task DisconnectInternal()
+ {
+ Disconnect();
+ return Task.CompletedTask;
+ }
+
+ public override Task Reconnect()
{
- isConnected.Value = false;
return Task.CompletedTask;
}
diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
index 0eb1e7c285..22a9c4a5d6 100644
--- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
+++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
@@ -204,10 +204,16 @@ namespace osu.Game.Tests.Visual.Spectator
});
}
- protected override async Task DisconnectInternal()
+ protected override Task DisconnectInternal()
{
- await base.DisconnectInternal().ConfigureAwait(false);
isConnected.Value = false;
+ return Task.CompletedTask;
+ }
+
+ public override Task Reconnect()
+ {
+ isConnected.Value = true;
+ return Task.CompletedTask;
}
}
}