@@ -368,49 +368,57 @@ public async Task ServerSave(List<Aisling> players, CancellationToken ct = defau
368368
369369 private async Task PlayerSaveRoutine ( Aisling player , CancellationToken ct )
370370 {
371- await using var conn = new SqlConnection ( ConnectionString ) ;
372- await conn . OpenAsync ( ct ) . ConfigureAwait ( false ) ;
373- await using var tx = ( SqlTransaction ) await conn . BeginTransactionAsync ( IsolationLevel . ReadCommitted , ct ) . ConfigureAwait ( false ) ;
374-
375- try
371+ var ok = await ExecuteWithRetryAsync ( async token =>
376372 {
377- player . Client . LastSave = DateTime . UtcNow ;
378- var dt = PlayerDataTable ( ) ;
379- var qDt = QuestDataTable ( ) ;
380- var cDt = ComboScrollDataTable ( ) ;
381- var iDt = ItemsDataTable ( ) ;
382- var skillDt = SkillDataTable ( ) ;
383- var spellDt = SpellDataTable ( ) ;
384- var buffDt = BuffsDataTable ( ) ;
385- var debuffDt = DeBuffsDataTable ( ) ;
386- dt = PlayerStatSave ( player , dt ) ;
387- qDt = PlayerQuestSave ( player , qDt ) ;
388- cDt = PlayerComboSave ( player , cDt ) ;
389- iDt = PlayerItemSave ( player , iDt ) ;
390- skillDt = PlayerSkillSave ( player , skillDt ) ;
391- spellDt = PlayerSpellSave ( player , spellDt ) ;
392- buffDt = PlayerBuffSave ( player , buffDt ) ;
393- debuffDt = PlayerDebuffSave ( player , debuffDt ) ;
394-
395- await ExecTvpAsync ( conn , tx , "PlayerSave" , "@Players" , "dbo.PlayerType" , dt , ct ) . ConfigureAwait ( false ) ;
396- await ExecTvpAsync ( conn , tx , "PlayerQuestSave" , "@Quests" , "dbo.QuestType" , qDt , ct ) . ConfigureAwait ( false ) ;
397- await ExecTvpAsync ( conn , tx , "PlayerComboSave" , "@Combos" , "dbo.ComboType" , cDt , ct ) . ConfigureAwait ( false ) ;
398- await ExecTvpAsync ( conn , tx , "ItemUpsert" , "@Items" , "dbo.ItemType" , iDt , ct ) . ConfigureAwait ( false ) ;
399- await ExecTvpAsync ( conn , tx , "PlayerSaveSkills" , "@Skills" , "dbo.SkillType" , skillDt , ct ) . ConfigureAwait ( false ) ;
400- await ExecTvpAsync ( conn , tx , "PlayerSaveSpells" , "@Spells" , "dbo.SpellType" , spellDt , ct ) . ConfigureAwait ( false ) ;
401- await ExecTvpAsync ( conn , tx , "PlayerBuffSync" , "@Buffs" , "dbo.BuffType" , buffDt , ct , new SqlParameter ( "@Serial" , SqlDbType . BigInt ) { Value = ( long ) player . Serial } ) . ConfigureAwait ( false ) ;
402- await ExecTvpAsync ( conn , tx , "PlayerDeBuffSync" , "@Debuffs" , "dbo.DebuffType" , debuffDt , ct , new SqlParameter ( "@Serial" , SqlDbType . BigInt ) { Value = ( long ) player . Serial } ) . ConfigureAwait ( false ) ;
403-
404- await tx . CommitAsync ( ct ) . ConfigureAwait ( false ) ;
405-
406- // Player has a synced database state
407- player . PlayerSaveDirty = false ;
408- }
409- catch ( Exception e )
373+ await using var conn = new SqlConnection ( ConnectionString ) ;
374+ await conn . OpenAsync ( token ) . ConfigureAwait ( false ) ;
375+ await using var tx = ( SqlTransaction ) await conn . BeginTransactionAsync ( IsolationLevel . ReadCommitted , token ) . ConfigureAwait ( false ) ;
376+
377+ try
378+ {
379+ player . Client . LastSave = DateTime . UtcNow ;
380+
381+ var dt = PlayerStatSave ( player , PlayerDataTable ( ) ) ;
382+ var qDt = PlayerQuestSave ( player , QuestDataTable ( ) ) ;
383+ var cDt = PlayerComboSave ( player , ComboScrollDataTable ( ) ) ;
384+ var iDt = PlayerItemSave ( player , ItemsDataTable ( ) ) ;
385+ var skillDt = PlayerSkillSave ( player , SkillDataTable ( ) ) ;
386+ var spellDt = PlayerSpellSave ( player , SpellDataTable ( ) ) ;
387+ var buffDt = PlayerBuffSave ( player , BuffsDataTable ( ) ) ;
388+ var debuffDt = PlayerDebuffSave ( player , DeBuffsDataTable ( ) ) ;
389+
390+ await ExecTvpAsync ( conn , tx , "PlayerSave" , "@Players" , "dbo.PlayerType" , dt , token ) . ConfigureAwait ( false ) ;
391+ await ExecTvpAsync ( conn , tx , "PlayerQuestSave" , "@Quests" , "dbo.QuestType" , qDt , token ) . ConfigureAwait ( false ) ;
392+ await ExecTvpAsync ( conn , tx , "PlayerComboSave" , "@Combos" , "dbo.ComboType" , cDt , token ) . ConfigureAwait ( false ) ;
393+ await ExecTvpAsync ( conn , tx , "ItemUpsert" , "@Items" , "dbo.ItemType" , iDt , token ) . ConfigureAwait ( false ) ;
394+ await ExecTvpAsync ( conn , tx , "PlayerSaveSkills" , "@Skills" , "dbo.SkillType" , skillDt , token ) . ConfigureAwait ( false ) ;
395+ await ExecTvpAsync ( conn , tx , "PlayerSaveSpells" , "@Spells" , "dbo.SpellType" , spellDt , token ) . ConfigureAwait ( false ) ;
396+ await ExecTvpAsync (
397+ conn , tx , "PlayerBuffSync" , "@Buffs" , "dbo.BuffType" , buffDt , token ,
398+ new SqlParameter ( "@Serial" , SqlDbType . BigInt ) { Value = ( long ) player . Serial } ) . ConfigureAwait ( false ) ;
399+ await ExecTvpAsync (
400+ conn , tx , "PlayerDeBuffSync" , "@Debuffs" , "dbo.DebuffType" , debuffDt , token ,
401+ new SqlParameter ( "@Serial" , SqlDbType . BigInt ) { Value = ( long ) player . Serial } ) . ConfigureAwait ( false ) ;
402+
403+ await tx . CommitAsync ( token ) . ConfigureAwait ( false ) ;
404+ player . PlayerSaveDirty = false ;
405+ }
406+ catch
407+ {
408+ try
409+ {
410+ await tx . RollbackAsync ( token ) . ConfigureAwait ( false ) ;
411+ }
412+ catch { }
413+
414+ // Throw and let ExecuteWithRetryAsync see the SqlException
415+ throw ;
416+ }
417+ } , ct ) . ConfigureAwait ( false ) ;
418+
419+ if ( ! ok )
410420 {
411- await tx . RollbackAsync ( ct ) . ConfigureAwait ( false ) ;
412- ServerSetup . EventsLogger ( $ "PlayerSave performed rollback!", LogLevel . Error ) ;
413- SentrySdk . CaptureException ( e ) ;
421+ ServerSetup . EventsLogger ( $ "PlayerSave failed after retries for Serial={ player . Serial } ", LogLevel . Error ) ;
414422 }
415423 }
416424
@@ -419,10 +427,10 @@ private static async Task ExecTvpAsync(SqlConnection conn, SqlTransaction tx, st
419427 {
420428 await using var cmd = new SqlCommand ( procName , conn , tx )
421429 {
422- CommandType = CommandType . StoredProcedure
430+ CommandType = CommandType . StoredProcedure ,
431+ CommandTimeout = 5
423432 } ;
424433
425- // Add scalar/extra parameters first
426434 if ( extraParams is { Length : > 0 } )
427435 {
428436 foreach ( var ep in extraParams )
@@ -432,10 +440,9 @@ private static async Task ExecTvpAsync(SqlConnection conn, SqlTransaction tx, st
432440 }
433441 }
434442
435- // Add TVP parameter
436- var p = cmd . Parameters . AddWithValue ( tvpParamName , tvp ) ;
437- p . SqlDbType = SqlDbType . Structured ;
438- p . TypeName = typeName ;
443+ var tvpParam = cmd . Parameters . Add ( tvpParamName , SqlDbType . Structured ) ;
444+ tvpParam . TypeName = typeName ;
445+ tvpParam . Value = tvp ;
439446
440447 await cmd . ExecuteNonQueryAsync ( ct ) . ConfigureAwait ( false ) ;
441448 }
0 commit comments