Skip to content

Commit 5eaab9b

Browse files
authored
fix: prevent crash when going to character select from ingame (AscensionGameDev#2067)
* partial fix for race condition can still cause it by logging in with no pv, setting the pv and quickly exiting to char select, logging out, deleting the pv instance from the player db, logging back in, setting the pv, and quickly exiting to char select (and maybe even just normal logout triggers it in this specific scenario?) * fix: actually complete the logout when logging out from character select * cleanup cruft from working on dbce
1 parent 6ad11f0 commit 5eaab9b

File tree

12 files changed

+163
-44
lines changed

12 files changed

+163
-44
lines changed

Intersect.Client/Core/Main.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,10 +330,17 @@ public static void JoinGame()
330330
Audio.StopMusic(ClientConfiguration.Instance.MusicFadeTimer);
331331
}
332332

333-
public static void Logout(bool characterSelect)
333+
public static void Logout(bool characterSelect, bool skipFade = false)
334334
{
335335
Audio.PlayMusic(ClientConfiguration.Instance.MenuMusic, ClientConfiguration.Instance.MusicFadeTimer, ClientConfiguration.Instance.MusicFadeTimer, true);
336-
Fade.FadeOut();
336+
if (skipFade)
337+
{
338+
Fade.Cancel();
339+
}
340+
else
341+
{
342+
Fade.FadeOut();
343+
}
337344

338345
if (!ClientContext.IsSinglePlayer)
339346
{
@@ -387,7 +394,14 @@ public static void Logout(bool characterSelect)
387394
PacketSender.SendLogout(characterSelect);
388395
}
389396

390-
Fade.FadeIn();
397+
if (skipFade)
398+
{
399+
Fade.Cancel();
400+
}
401+
else
402+
{
403+
Fade.FadeIn();
404+
}
391405
}
392406

393407
}

Intersect.Client/Interface/Menu/SelectCharacterWindow.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ public void Hide()
344344

345345
private void mLogoutButton_Clicked(Base sender, ClickedEventArgs arguments)
346346
{
347+
Main.Logout(false, skipFade: true);
347348
mMainMenu.Reset();
348349
}
349350

Intersect.Network/LiteNetLib/LiteNetLibInterface.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,13 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead
446446

447447
public void OnNetworkLatencyUpdate(NetPeer peer, int latency)
448448
{
449+
#if !DIAGNOSTIC
450+
if (latency < 1)
451+
{
452+
return;
453+
}
454+
#endif
455+
449456
Log.Verbose($"LTNC {peer.EndPoint} {latency}ms");
450457
}
451458

Intersect.Server.Core/Core/ServerContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ protected override void Dispose(bool disposing)
118118
var savingTasks = new List<Task>();
119119
foreach (var user in Database.PlayerData.User.OnlineList.ToArray())
120120
{
121-
savingTasks.Add(Task.Run(() => user.Save()));
121+
savingTasks.Add(Task.Run(() => user.SaveWithDebounce()));
122122
}
123123

124124
Task.WaitAll(savingTasks.ToArray());

Intersect.Server.Core/Database/IntersectDbContext.cs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.EntityFrameworkCore;
1010
using Microsoft.EntityFrameworkCore.ChangeTracking;
1111
using Microsoft.EntityFrameworkCore.Infrastructure;
12+
using Microsoft.Extensions.Primitives;
1213

1314
namespace Intersect.Server.Database;
1415

@@ -204,40 +205,46 @@ public override int SaveChanges(bool acceptAllChangesOnSuccess)
204205

205206
return rowsChanged;
206207
}
207-
catch (DbUpdateConcurrencyException ex)
208+
catch (DbUpdateConcurrencyException concurrencyException)
208209
{
209210
var concurrencyErrors = new StringBuilder();
210-
foreach (var entry in ex.Entries)
211+
foreach (var entry in concurrencyException.Entries)
211212
{
212-
var type = entry.GetType().FullName;
213-
concurrencyErrors.AppendLine($"Entry Type [{type} / {entry.State}]");
214-
concurrencyErrors.AppendLine("--------------------");
215-
concurrencyErrors.AppendLine($"Type: {entry.Entity.GetFullishName()}");
213+
concurrencyErrors.AppendLine(
214+
$"[{entry.State}] {entry.Entity.GetFullishName()} ({entry.GetFullishName()})"
215+
);
216216

217217
var proposedValues = entry.CurrentValues;
218218
var databaseValues = entry.GetDatabaseValues();
219219

220220
foreach (var property in proposedValues.Properties)
221221
{
222222
concurrencyErrors.AppendLine(
223-
$"\t{property.Name:propertyNameColumnSize} (Token: {property.IsConcurrencyToken}): Proposed: {proposedValues[property]} Original Value: {entry.OriginalValues[property]} Database Value: {(databaseValues != null ? databaseValues[property] : "null")}"
223+
string.Join(
224+
' ',
225+
$"\t{property.Name:propertyNameColumnSize} (Token: {property.IsConcurrencyToken}):",
226+
$"Proposed: {proposedValues[property]}",
227+
$"Original Value: {entry.OriginalValues[property]}",
228+
$"Database Value: {(databaseValues?[property] ?? "null")}"
229+
)
224230
);
225231
}
226232

227-
concurrencyErrors.AppendLine("");
228-
concurrencyErrors.AppendLine("");
233+
concurrencyErrors.AppendLine();
229234
}
230235

231-
var suffix = string.Empty;
232236
#if DEBUG
233-
suffix = $"#{currentExecutionId}";
237+
var suffix = $"#{currentExecutionId}";
238+
#else
239+
var suffix = string.Empty;
234240
#endif
235241
var entityTypeNames = ChangeTracker.Entries()
236242
.Select(entry => entry.Entity.GetType().Name)
237243
.Distinct()
238244
.ToArray();
239-
Log.Error(ex, $"Jackpot! Concurrency Bug For {string.Join(", ", entityTypeNames)} {suffix}");
245+
Log.Error(concurrencyException, $"Jackpot! Concurrency Bug For {string.Join(", ", entityTypeNames)} {suffix}");
240246
Log.Error(concurrencyErrors.ToString());
247+
Log.Error(Environment.StackTrace);
241248

242249
#if DEBUG
243250
Log.Debug($"DBOP-C SaveChanges({acceptAllChangesOnSuccess}) #{currentExecutionId}");

Intersect.Server.Core/Database/PlayerData/Players/Variable.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,19 @@ namespace Intersect.Server.Database.PlayerData.Players
66
{
77
public partial class Variable
88
{
9+
private Guid _id;
10+
911
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
1012
[JsonIgnore]
11-
public Guid Id { get; protected set; }
13+
public Guid Id
14+
{
15+
get => _id;
16+
set
17+
{
18+
Console.WriteLine("HERE IS THE ISSUE?", Environment.StackTrace);
19+
_id = value;
20+
}
21+
}
1222

1323
[DatabaseGenerated(DatabaseGeneratedOption.None)]
1424
public Guid VariableId { get; protected set; }

Intersect.Server.Core/Database/PlayerData/User.cs

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ namespace Intersect.Server.Database.PlayerData
3232
[ApiVisibility(ApiVisibility.Restricted | ApiVisibility.Private)]
3333
public partial class User
3434
{
35+
private long _lastSave;
36+
private readonly object _lastSaveLock = new();
37+
3538
private static readonly ConcurrentDictionary<Guid, User> OnlineUsers = new();
3639

3740
[JsonIgnore][NotMapped] private readonly object mSavingLock = new();
@@ -225,20 +228,19 @@ public void DeleteCharacter(Player deleteCharacter)
225228
{
226229
lock (mSavingLock)
227230
{
228-
using (var context = DbInterface.CreatePlayerContext(false))
229-
{
230-
context.Users.Update(this);
231+
using var context = DbInterface.CreatePlayerContext(false);
231232

232-
context.ChangeTracker.DetectChanges();
233+
context.Users.Update(this);
233234

234-
context.StopTrackingUsersExcept(this);
235+
Players.Remove(deleteCharacter);
235236

236-
context.Entry(deleteCharacter).State = EntityState.Deleted;
237+
context.ChangeTracker.DetectChanges();
237238

238-
Players.Remove(deleteCharacter);
239+
context.StopTrackingUsersExcept(this);
239240

240-
context.SaveChanges();
241-
}
241+
context.Entry(deleteCharacter).State = EntityState.Deleted;
242+
243+
context.SaveChanges();
242244
}
243245
}
244246
catch (Exception ex)
@@ -294,16 +296,35 @@ public async Task SaveAsync(
294296
CancellationToken cancellationToken = default
295297
) => Save(playerContext, force);
296298

297-
public void Save(bool force) => Save(force: force, create: false);
299+
public void SaveWithDebounce(long debounceMs = 5000)
300+
{
301+
lock (_lastSaveLock)
302+
{
303+
if (_lastSave < debounceMs + Timing.Global.MillisecondsUtc)
304+
{
305+
Log.Debug("Skipping save due to debounce");
306+
return;
307+
}
308+
}
309+
310+
Save();
311+
}
298312

299-
public void Save(bool force = false, bool create = false) => Save(default, force, create);
313+
public UserSaveResult Save(bool force) => Save(force: force, create: false);
314+
315+
public UserSaveResult Save(bool force = false, bool create = false) => Save(default, force, create);
300316

301317
#if DIAGNOSTIC
302318
private int _saveCounter = 0;
303319
#endif
304320

305-
private void Save(PlayerContext? playerContext, bool force = false, bool create = false)
321+
private UserSaveResult Save(PlayerContext? playerContext, bool force = false, bool create = false)
306322
{
323+
lock (_lastSaveLock)
324+
{
325+
_lastSave = Timing.Global.MillisecondsUtc;
326+
}
327+
307328
#if DIAGNOSTIC
308329
var currentExecutionId = _saveCounter++;
309330
#endif
@@ -326,7 +347,10 @@ private void Save(PlayerContext? playerContext, bool force = false, bool create
326347

327348
if (!lockTaken)
328349
{
329-
return;
350+
#if DIAGNOSTIC
351+
Log.Debug($"Failed to take lock {Environment.StackTrace}");
352+
#endif
353+
return UserSaveResult.SkippedCouldNotTakeLock;
330354
}
331355

332356
#if DIAGNOSTIC
@@ -345,6 +369,7 @@ private void Save(PlayerContext? playerContext, bool force = false, bool create
345369
}
346370
else
347371
{
372+
// playerContext.Attach(this);
348373
playerContext.Users.Update(this);
349374
}
350375

@@ -367,6 +392,8 @@ private void Save(PlayerContext? playerContext, bool force = false, bool create
367392
#if DIAGNOSTIC
368393
Log.Debug($"DBOP-B Save({playerContext}, {force}, {create}) #{currentExecutionId} {Name} ({Id})");
369394
#endif
395+
396+
return UserSaveResult.Completed;
370397
}
371398
catch (DbUpdateConcurrencyException ex)
372399
{
@@ -428,6 +455,8 @@ private void Save(PlayerContext? playerContext, bool force = false, bool create
428455
Monitor.Exit(mSavingLock);
429456
}
430457
}
458+
459+
return UserSaveResult.Failed;
431460
}
432461

433462
[return: NotNullIfNotNull(nameof(user))]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Intersect.Server.Database.PlayerData;
2+
3+
public enum UserSaveResult
4+
{
5+
Completed,
6+
SkippedCouldNotTakeLock,
7+
Failed,
8+
}

Intersect.Server.Core/Entities/Player.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Intersect.Network.Packets.Server;
1414
using Intersect.Server.Database;
1515
using Intersect.Server.Database.Logging.Entities;
16+
using Intersect.Server.Database.PlayerData;
1617
using Intersect.Server.Database.PlayerData.Players;
1718
using Intersect.Server.Database.PlayerData.Security;
1819
using Intersect.Server.Entities.Combat;
@@ -466,7 +467,7 @@ private void Logout(bool softLogout = false)
466467
BankInterface = default;
467468
InShop = default;
468469

469-
//Clear cooldowns that have expired
470+
// Clear cooldowns that have expired
470471
RemoveStaleItemCooldowns();
471472
RemoveStaleSpellCooldowns();
472473

@@ -477,10 +478,10 @@ private void Logout(bool softLogout = false)
477478
PacketSender.SendGlobalMsg(Strings.Player.left.ToString(Name, Options.Instance.GameName));
478479
}
479480

480-
//Remvoe this player from the online list
481+
// Remove this player from the online list
481482
if (OnlinePlayers?.ContainsKey(Id) ?? false)
482483
{
483-
OnlinePlayers.TryRemove(Id, out Player me);
484+
OnlinePlayers.TryRemove(Id, out Player _);
484485
OnlineList = OnlinePlayers.Values.ToArray();
485486
}
486487

@@ -495,21 +496,63 @@ private void Logout(bool softLogout = false)
495496
User?.TryLogout(softLogout);
496497
}
497498

498-
DbInterface.Pool.QueueWorkItem(CompleteLogout);
499+
#if DIAGNOSTIC
500+
var stackTrace = Environment.StackTrace;
501+
#else
502+
var stackTrace = default(string);
503+
#endif
504+
var logoutOperationId = Guid.NewGuid();
505+
DbInterface.Pool.QueueWorkItem(CompleteLogout, logoutOperationId, stackTrace);
499506
}
500507

501508
#if DIAGNOSTIC
502509
private int _logoutCounter = 0;
503510
#endif
504511

505-
public void CompleteLogout()
512+
public void CompleteLogout(Guid logoutOperationId, string? stackTrace = default)
506513
{
514+
if (logoutOperationId != default)
515+
{
516+
Log.Debug($"Completing logout {logoutOperationId}");
517+
}
518+
519+
if (stackTrace != default)
520+
{
521+
Log.Debug(stackTrace);
522+
}
523+
507524
#if DIAGNOSTIC
508525
var currentExecutionId = _logoutCounter++;
509526
Log.Debug($"Started {nameof(CompleteLogout)}() #{currentExecutionId} on {Name} ({User?.Name})");
510527
#endif
511528

512-
User?.Save();
529+
try
530+
{
531+
Log.Diagnostic($"Starting save for logout {logoutOperationId}");
532+
var saveResult = User?.Save();
533+
switch (saveResult)
534+
{
535+
case UserSaveResult.Completed:
536+
Log.Diagnostic($"Completed save for logout {logoutOperationId}");
537+
break;
538+
case UserSaveResult.SkippedCouldNotTakeLock:
539+
Log.Debug($"Skipped save for logout {logoutOperationId}");
540+
break;
541+
case UserSaveResult.Failed:
542+
Log.Warn($"Save failed for logout {logoutOperationId}");
543+
break;
544+
case null:
545+
Log.Warn($"Skipped save because {nameof(User)} is null.");
546+
break;
547+
default:
548+
throw new UnreachableException();
549+
}
550+
}
551+
catch
552+
{
553+
Log.Warn($"Crashed while saving for logout {logoutOperationId}");
554+
throw;
555+
}
513556

514557
lock (_savingLock)
515558
{
@@ -541,8 +584,8 @@ public override void Update(long timeMs)
541584
{
542585
if (CombatTimer < Timing.Global.Milliseconds)
543586
{
587+
Log.Debug($"Combat timer expired for player {Id}, logging out.");
544588
Logout();
545-
546589
return;
547590
}
548591
}

0 commit comments

Comments
 (0)