Further attempt to resolve profile corruption issues (#650)
* Further attempt to resolve profile corruption issues - FileUtil now uses File.Replace and does a sync flush - Add restore capabilities to BackupService - If loading a profile fails, attempt to restore from the most recent backup - Trigger a backup creation on raid start, raid end, and game close - Load profiles before starting the backupService to avoid backing up corrupt profiles * - Switch async calls to .GetAwaiter().GetResult() for better exception handling --------- Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@ public class GameCallbacks(
|
|||||||
HttpResponseUtil httpResponseUtil,
|
HttpResponseUtil httpResponseUtil,
|
||||||
Watermark watermark,
|
Watermark watermark,
|
||||||
SaveServer saveServer,
|
SaveServer saveServer,
|
||||||
|
BackupService backupService,
|
||||||
GameController gameController,
|
GameController gameController,
|
||||||
ProfileActivityService profileActivityService,
|
ProfileActivityService profileActivityService,
|
||||||
TimeUtil timeUtil
|
TimeUtil timeUtil
|
||||||
@@ -66,6 +67,10 @@ public class GameCallbacks(
|
|||||||
public async ValueTask<string> GameLogout(string url, EmptyRequestData _, MongoId sessionID)
|
public async ValueTask<string> GameLogout(string url, EmptyRequestData _, MongoId sessionID)
|
||||||
{
|
{
|
||||||
await saveServer.SaveProfileAsync(sessionID);
|
await saveServer.SaveProfileAsync(sessionID);
|
||||||
|
|
||||||
|
// Backup profiles on exit
|
||||||
|
await backupService.Init();
|
||||||
|
|
||||||
return httpResponseUtil.GetBody(new GameLogoutResponseData { Status = "ok" });
|
return httpResponseUtil.GetBody(new GameLogoutResponseData { Status = "ok" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ public class SaveCallbacks(SaveServer saveServer, ConfigServer configServer, Bac
|
|||||||
|
|
||||||
public async Task OnLoad()
|
public async Task OnLoad()
|
||||||
{
|
{
|
||||||
await backupService.StartBackupSystem();
|
|
||||||
await saveServer.LoadAsync();
|
await saveServer.LoadAsync();
|
||||||
|
|
||||||
|
// Note: This has to happen after loading the saveServer so we don't backup corrupted profiles
|
||||||
|
await backupService.StartBackupSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> OnUpdate(long secondsSinceLastRun)
|
public async Task<bool> OnUpdate(long secondsSinceLastRun)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using SPTarkov.DI.Annotations;
|
using SPTarkov.DI.Annotations;
|
||||||
using SPTarkov.Server.Core.DI;
|
using SPTarkov.Server.Core.DI;
|
||||||
@@ -22,6 +23,7 @@ public class SaveServer(
|
|||||||
HashUtil hashUtil,
|
HashUtil hashUtil,
|
||||||
ServerLocalisationService serverLocalisationService,
|
ServerLocalisationService serverLocalisationService,
|
||||||
ProfileValidatorService profileValidatorService,
|
ProfileValidatorService profileValidatorService,
|
||||||
|
BackupService backupService,
|
||||||
ISptLogger<SaveServer> logger,
|
ISptLogger<SaveServer> logger,
|
||||||
ConfigServer configServer
|
ConfigServer configServer
|
||||||
)
|
)
|
||||||
@@ -211,7 +213,31 @@ public class SaveServer(
|
|||||||
if (fileUtil.FileExists(filePath))
|
if (fileUtil.FileExists(filePath))
|
||||||
// File found, store in profiles[]
|
// File found, store in profiles[]
|
||||||
{
|
{
|
||||||
var profile = await jsonUtil.DeserializeFromFileAsync<JsonObject>(filePath);
|
JsonObject? profile = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
profile = await jsonUtil.DeserializeFromFileAsync<JsonObject>(filePath);
|
||||||
|
}
|
||||||
|
catch (JsonException e)
|
||||||
|
{
|
||||||
|
// If the profile fails to deserialize, it may have corrupted, try to restore from a backup
|
||||||
|
logger.Warning($"Failed loading profile for {sessionID.ToString()}. Attempting to load backup");
|
||||||
|
|
||||||
|
// We make a copy of the profile before overwriting it, just incase
|
||||||
|
var corruptBackupPath = $"{profileFilepath}{sessionID.ToString()}-corrupt.json";
|
||||||
|
File.Copy(filePath, corruptBackupPath, true);
|
||||||
|
|
||||||
|
if (backupService.RestoreProfile(sessionID))
|
||||||
|
{
|
||||||
|
profile = await jsonUtil.DeserializeFromFileAsync<JsonObject>(filePath);
|
||||||
|
logger.Success("Profile restored from backup!");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to restore profile backup", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (profile is not null)
|
if (profile is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace SPTarkov.Server.Core.Services;
|
|||||||
public class BackupService
|
public class BackupService
|
||||||
{
|
{
|
||||||
protected const string ProfileDir = "./user/profiles";
|
protected const string ProfileDir = "./user/profiles";
|
||||||
|
protected const string activeModsFilename = "activeMods.json";
|
||||||
|
|
||||||
protected readonly List<string> ActiveServerMods;
|
protected readonly List<string> ActiveServerMods;
|
||||||
protected readonly BackupConfig BackupConfig;
|
protected readonly BackupConfig BackupConfig;
|
||||||
@@ -131,7 +132,7 @@ public class BackupService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write a copy of active mods.
|
// Write a copy of active mods.
|
||||||
await FileUtil.WriteFileAsync(Path.Combine(targetDir, "activeMods.json"), JsonUtil.Serialize(ActiveServerMods));
|
await FileUtil.WriteFileAsync(Path.Combine(targetDir, activeModsFilename), JsonUtil.Serialize(ActiveServerMods));
|
||||||
|
|
||||||
if (Logger.IsLogEnabled(LogLevel.Debug))
|
if (Logger.IsLogEnabled(LogLevel.Debug))
|
||||||
{
|
{
|
||||||
@@ -223,6 +224,24 @@ public class BackupService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected string? GetMostRecentProfileBackup(IEnumerable<string> backupPaths, string profileId)
|
||||||
|
{
|
||||||
|
var profileFilename = $"{profileId}.json";
|
||||||
|
var backupPathsWithCreationDateTime = GetBackupPathsWithCreationTimestamp(backupPaths);
|
||||||
|
|
||||||
|
foreach (var (backupTimestamp, backupPath) in backupPathsWithCreationDateTime.Reverse())
|
||||||
|
{
|
||||||
|
var profileBackups = FileUtil.GetFiles(backupPath);
|
||||||
|
var profileBackup = profileBackups.FirstOrDefault(path => path.EndsWith(profileFilename));
|
||||||
|
if (profileBackup != null)
|
||||||
|
{
|
||||||
|
return profileBackup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves and sorts the backup file paths from the specified directory.
|
/// Retrieves and sorts the backup file paths from the specified directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -308,4 +327,29 @@ public class BackupService
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the most recent profile backup for the given profile Id
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profileId">The profile ID of the backup to restore</param>
|
||||||
|
/// <returns>True on success. False on failure</returns>
|
||||||
|
public bool RestoreProfile(string profileId)
|
||||||
|
{
|
||||||
|
var backupDir = BackupConfig.Directory;
|
||||||
|
var backupPaths = GetBackupPaths(backupDir);
|
||||||
|
var mostRecentBackupForProfile = GetMostRecentProfileBackup(backupPaths, profileId);
|
||||||
|
|
||||||
|
// Verify we have a backup for this profile
|
||||||
|
if (mostRecentBackupForProfile == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the most recent profile backup
|
||||||
|
var profileFileName = FileUtil.GetFileNameAndExtension(mostRecentBackupForProfile);
|
||||||
|
var targetProfilePath = Path.Combine(ProfileDir, profileFileName);
|
||||||
|
|
||||||
|
File.Copy(mostRecentBackupForProfile, targetProfilePath, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class LocationLifecycleService(
|
|||||||
TimeUtil timeUtil,
|
TimeUtil timeUtil,
|
||||||
DatabaseService databaseService,
|
DatabaseService databaseService,
|
||||||
ProfileHelper profileHelper,
|
ProfileHelper profileHelper,
|
||||||
|
BackupService backupService,
|
||||||
ProfileActivityService profileActivityService,
|
ProfileActivityService profileActivityService,
|
||||||
BotNameService botNameService,
|
BotNameService botNameService,
|
||||||
ICloner cloner,
|
ICloner cloner,
|
||||||
@@ -77,6 +78,9 @@ public class LocationLifecycleService(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual StartLocalRaidResponseData StartLocalRaid(MongoId sessionId, StartLocalRaidRequestData request)
|
public virtual StartLocalRaidResponseData StartLocalRaid(MongoId sessionId, StartLocalRaidRequestData request)
|
||||||
{
|
{
|
||||||
|
// Backup the profile on raid start
|
||||||
|
backupService.Init().GetAwaiter().GetResult();
|
||||||
|
|
||||||
logger.Debug($"Starting: {request.Location}");
|
logger.Debug($"Starting: {request.Location}");
|
||||||
|
|
||||||
var playerProfile = profileHelper.GetFullProfile(sessionId);
|
var playerProfile = profileHelper.GetFullProfile(sessionId);
|
||||||
@@ -409,6 +413,10 @@ public class LocationLifecycleService(
|
|||||||
HandleCoopExtract(sessionId, pmcProfile, request.Results.ExitName);
|
HandleCoopExtract(sessionId, pmcProfile, request.Results.ExitName);
|
||||||
SendCoopTakenFenceMessage(sessionId);
|
SendCoopTakenFenceMessage(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save and backup the profile on raid end
|
||||||
|
saveServer.SaveProfileAsync(sessionId).GetAwaiter().GetResult();
|
||||||
|
backupService.Init().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -131,10 +131,18 @@ public class FileUtil
|
|||||||
|
|
||||||
// We flush here so we can be sure it's immediately committed to disk
|
// We flush here so we can be sure it's immediately committed to disk
|
||||||
await fs.FlushAsync();
|
await fs.FlushAsync();
|
||||||
|
fs.Flush(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite over the old file
|
// Overwrite over the old file
|
||||||
File.Move(tempFilePath, filePath, overwrite: true);
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
File.Replace(tempFilePath, filePath, null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Move(tempFilePath, filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user