UserDbDataManager.cs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. using System.Threading;
  2. using System.Threading.Tasks;
  3. using Content.Server.Preferences.Managers;
  4. using Robust.Shared.Network;
  5. using Robust.Shared.Player;
  6. using Robust.Shared.Utility;
  7. namespace Content.Server.Database;
  8. /// <summary>
  9. /// Manages per-user data that comes from the database. Ensures it is loaded efficiently on client connect,
  10. /// and ensures data is loaded before allowing players to spawn or such.
  11. /// </summary>
  12. /// <remarks>
  13. /// Actual loading code is handled by separate managers such as <see cref="IServerPreferencesManager"/>.
  14. /// This manager is simply a centralized "is loading done" controller for other code to rely on.
  15. /// </remarks>
  16. public sealed class UserDbDataManager : IPostInjectInit
  17. {
  18. [Dependency] private readonly ILogManager _logManager = default!;
  19. private readonly Dictionary<NetUserId, UserData> _users = new();
  20. private readonly List<OnLoadPlayer> _onLoadPlayer = [];
  21. private readonly List<OnFinishLoad> _onFinishLoad = [];
  22. private readonly List<OnPlayerDisconnect> _onPlayerDisconnect = [];
  23. private ISawmill _sawmill = default!;
  24. // TODO: Ideally connected/disconnected would be subscribed to IPlayerManager directly,
  25. // but this runs into ordering issues with game ticker.
  26. public void ClientConnected(ICommonSession session)
  27. {
  28. _sawmill.Verbose($"Initiating load for user {session}");
  29. DebugTools.Assert(!_users.ContainsKey(session.UserId), "We should not have any cached data on client connect.");
  30. var cts = new CancellationTokenSource();
  31. var task = Load(session, cts.Token);
  32. var data = new UserData(cts, task);
  33. _users.Add(session.UserId, data);
  34. }
  35. public void ClientDisconnected(ICommonSession session)
  36. {
  37. _users.Remove(session.UserId, out var data);
  38. if (data == null)
  39. throw new InvalidOperationException("Did not have cached data in ClientDisconnect!");
  40. data.Cancel.Cancel();
  41. data.Cancel.Dispose();
  42. foreach (var onDisconnect in _onPlayerDisconnect)
  43. {
  44. onDisconnect(session);
  45. }
  46. }
  47. private async Task Load(ICommonSession session, CancellationToken cancel)
  48. {
  49. // The task returned by this function is only ever observed by callers of WaitLoadComplete,
  50. // which doesn't even happen currently if the lobby is enabled.
  51. // As such, this task must NOT throw a non-cancellation error!
  52. try
  53. {
  54. var tasks = new List<Task>();
  55. foreach (var action in _onLoadPlayer)
  56. {
  57. tasks.Add(action(session, cancel));
  58. }
  59. await Task.WhenAll(tasks);
  60. cancel.ThrowIfCancellationRequested();
  61. foreach (var action in _onFinishLoad)
  62. {
  63. action(session);
  64. }
  65. _sawmill.Verbose($"Load complete for user {session}");
  66. }
  67. catch (OperationCanceledException)
  68. {
  69. _sawmill.Debug($"Load cancelled for user {session}");
  70. // We can rethrow the cancellation.
  71. // This will make the task returned by WaitLoadComplete() also return a cancellation.
  72. throw;
  73. }
  74. catch (Exception e)
  75. {
  76. // Must catch all exceptions here, otherwise task may go unobserved.
  77. _sawmill.Error($"Load of user data failed: {e}");
  78. // Kick them from server, since something is hosed. Let them try again I guess.
  79. session.Channel.Disconnect("Loading of server user data failed, this is a bug.");
  80. // We throw a OperationCanceledException so users of WaitLoadComplete() always see cancellation here.
  81. throw new OperationCanceledException("Load of user data cancelled due to unknown error");
  82. }
  83. }
  84. /// <summary>
  85. /// Wait for all on-database data for a user to be loaded.
  86. /// </summary>
  87. /// <remarks>
  88. /// The task returned by this function may end up in a cancelled state
  89. /// (throwing <see cref="OperationCanceledException"/>) if the user disconnects while loading or an error occurs.
  90. /// </remarks>
  91. /// <param name="session"></param>
  92. /// <returns>
  93. /// A task that completes when all on-database data for a user has finished loading.
  94. /// </returns>
  95. public Task WaitLoadComplete(ICommonSession session)
  96. {
  97. return _users[session.UserId].Task;
  98. }
  99. public bool IsLoadComplete(ICommonSession session)
  100. {
  101. return GetLoadTask(session).IsCompletedSuccessfully;
  102. }
  103. public Task GetLoadTask(ICommonSession session)
  104. {
  105. return _users[session.UserId].Task;
  106. }
  107. public void AddOnLoadPlayer(OnLoadPlayer action)
  108. {
  109. _onLoadPlayer.Add(action);
  110. }
  111. public void AddOnFinishLoad(OnFinishLoad action)
  112. {
  113. _onFinishLoad.Add(action);
  114. }
  115. public void AddOnPlayerDisconnect(OnPlayerDisconnect action)
  116. {
  117. _onPlayerDisconnect.Add(action);
  118. }
  119. void IPostInjectInit.PostInject()
  120. {
  121. _sawmill = _logManager.GetSawmill("userdb");
  122. }
  123. private sealed record UserData(CancellationTokenSource Cancel, Task Task);
  124. public delegate Task OnLoadPlayer(ICommonSession player, CancellationToken cancel);
  125. public delegate void OnFinishLoad(ICommonSession player);
  126. public delegate void OnPlayerDisconnect(ICommonSession player);
  127. }