diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/core.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/core.json index ef076341..173c758a 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/core.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/core.json @@ -2,7 +2,7 @@ "projectName": "SPT", "compatibleTarkovVersion": "0.16.9.40087", "serverName": "SPT Server", - "profileSaveIntervalSeconds": 15, + "profileSaveIntervalSeconds": 60, "sptFriendNickname": "SPT", "allowProfileWipe": true, "enableNoGCRegions": true, diff --git a/Libraries/SPTarkov.Server.Core/Servers/SaveServer.cs b/Libraries/SPTarkov.Server.Core/Servers/SaveServer.cs index ff8fb212..4f504f2a 100644 --- a/Libraries/SPTarkov.Server.Core/Servers/SaveServer.cs +++ b/Libraries/SPTarkov.Server.Core/Servers/SaveServer.cs @@ -35,6 +35,7 @@ public class SaveServer( protected readonly ConcurrentDictionary profiles = new(); protected readonly ConcurrentDictionary saveMd5 = new(); + protected readonly ConcurrentDictionary saveLocks = new(); /// /// Add callback to occur prior to saving profile changes @@ -208,8 +209,7 @@ public class SaveServer( /// ID of profile to store in memory public async Task LoadProfileAsync(MongoId sessionID) { - var filename = $"{sessionID.ToString()}.json"; - var filePath = $"{profileFilepath}{filename}"; + var filePath = Path.Combine(profileFilepath, $"{sessionID}.json"); if (fileUtil.FileExists(filePath)) // File found, store in profiles[] { @@ -225,7 +225,7 @@ public class SaveServer( 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"; + var corruptBackupPath = Path.Combine(profileFilepath, $"{sessionID}-corrupt.json"); File.Copy(filePath, corruptBackupPath, true); if (backupService.RestoreProfile(sessionID)) @@ -280,34 +280,48 @@ public class SaveServer( return 0; } - var filePath = $"{profileFilepath}{sessionID.ToString()}.json"; + // Lock based on sessionID so we don't attempt to write to the same save file + // multiple times at the same time, leading to file access contention + SemaphoreSlim saveLock = saveLocks.GetOrAdd(sessionID, _ => new SemaphoreSlim(1, 1)); + await saveLock.WaitAsync(); - // Run pre-save callbacks before we save into json - foreach (var callback in onBeforeSaveCallbacks) + Stopwatch start; + try { - var previous = profiles[sessionID]; - try + var filePath = Path.Combine(profileFilepath, $"{sessionID}.json"); + + // Run pre-save callbacks before we save into json + foreach (var callback in onBeforeSaveCallbacks) { - profiles[sessionID] = onBeforeSaveCallbacks[callback.Key](profiles[sessionID]); + var previous = profiles[sessionID]; + try + { + profiles[sessionID] = onBeforeSaveCallbacks[callback.Key](profiles[sessionID]); + } + catch (Exception e) + { + logger.Error(serverLocalisationService.GetText("profile_save_callback_error", new { callback, error = e })); + profiles[sessionID] = previous; + } } - catch (Exception e) + + start = Stopwatch.StartNew(); + var jsonProfile = jsonUtil.Serialize(profiles[sessionID], !configServer.GetConfig().Features.CompressProfile); + var fmd5 = await hashUtil.GenerateHashForDataAsync(HashingAlgorithm.MD5, jsonProfile); + if (!saveMd5.TryGetValue(sessionID, out var currentMd5) || currentMd5 != fmd5) { - logger.Error(serverLocalisationService.GetText("profile_save_callback_error", new { callback, error = e })); - profiles[sessionID] = previous; + saveMd5[sessionID] = fmd5; + // save profile to disk + await fileUtil.WriteFileAsync(filePath, jsonProfile); } + + start.Stop(); + } + finally + { + saveLock.Release(); } - var start = Stopwatch.StartNew(); - var jsonProfile = jsonUtil.Serialize(profiles[sessionID], !configServer.GetConfig().Features.CompressProfile); - var fmd5 = await hashUtil.GenerateHashForDataAsync(HashingAlgorithm.MD5, jsonProfile); - if (!saveMd5.TryGetValue(sessionID, out var currentMd5) || currentMd5 != fmd5) - { - saveMd5[sessionID] = fmd5; - // save profile to disk - await fileUtil.WriteFileAsync(filePath, jsonProfile); - } - - start.Stop(); return start.ElapsedMilliseconds; } @@ -318,7 +332,7 @@ public class SaveServer( /// True if successful public bool RemoveProfile(MongoId sessionID) { - var file = $"{profileFilepath}{sessionID}.json"; + var file = Path.Combine(profileFilepath, $"{sessionID}.json"); if (profiles.ContainsKey(sessionID)) { profiles.TryRemove(sessionID, out _); diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 0f8ecda4..2714f71f 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -630,7 +630,7 @@ public class LocationLifecycleService( pmcProfile.Info.LastTimePlayedAsSavage = timeUtil.GetTimeStamp(); // Force a profile save - saveServer.SaveProfileAsync(sessionId); + saveServer.SaveProfileAsync(sessionId).GetAwaiter().GetResult(); } ///