Skip to content

Commit 08d49b3

Browse files
committed
Add safer profile saving handling by locking with an async mutex
1 parent c6eade7 commit 08d49b3

File tree

2 files changed

+47
-30
lines changed

2 files changed

+47
-30
lines changed

project/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"gen:productionquests": "tsx ./src/tools/ProductionQuestsGen/ProductionQuestsGenProgram.ts"
3535
},
3636
"dependencies": {
37+
"async-mutex": "^0.5.0",
3738
"atomically": "2.0.3",
3839
"fs-extra": "11.2.0",
3940
"i18n": "0.15.1",

project/src/servers/SaveServer.ts

+46-30
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import { FileSystem } from "@spt/utils/FileSystem";
99
import { HashUtil } from "@spt/utils/HashUtil";
1010
import { JsonUtil } from "@spt/utils/JsonUtil";
1111
import { Timer } from "@spt/utils/Timer";
12+
import { Mutex } from "async-mutex";
1213
import { inject, injectAll, injectable } from "tsyringe";
1314

1415
@injectable()
1516
export class SaveServer {
1617
protected profileFilepath = "user/profiles/";
1718
protected profiles: Map<string, ISptProfile> = new Map();
18-
protected profilesBeingSaved: Set<string> = new Set();
19+
protected profilesBeingSavedMutex: Map<string, Mutex> = new Map();
1920
protected onBeforeSaveCallbacks: Map<string, (profile: ISptProfile) => Promise<ISptProfile>> = new Map();
2021
protected saveSHA1: { [key: string]: string } = {};
2122

@@ -188,43 +189,48 @@ export class SaveServer {
188189
* @returns A promise that resolves when saving is completed.
189190
*/
190191
public async saveProfile(sessionID: string): Promise<void> {
191-
if (!this.profiles.get(sessionID)) {
192-
throw new Error(`Profile ${sessionID} does not exist! Unable to save this profile!`);
193-
}
194-
195-
if (this.profilesBeingSaved.has(sessionID)) {
196-
throw new Error(`Profile ${sessionID} is already being saved!`);
197-
}
192+
// Get the current mutex if it exists, create a new one if it doesn't for this profile
193+
const mutex = this.profilesBeingSavedMutex.get(sessionID) || new Mutex();
194+
this.profilesBeingSavedMutex.set(sessionID, mutex);
198195

199-
this.profilesBeingSaved.add(sessionID);
196+
const release = await mutex.acquire();
200197

201-
const filePath = `${this.profileFilepath}${sessionID}.json`;
198+
try {
199+
if (!this.profiles.get(sessionID)) {
200+
throw new Error(`Profile ${sessionID} does not exist! Unable to save this profile!`);
201+
}
202202

203-
// Run pre-save callbacks before we save into json
204-
for (const [id, callback] of this.onBeforeSaveCallbacks) {
205-
const previous = this.profiles.get(sessionID) as ISptProfile; // Cast as ISptProfile here since there should be no reason we're getting an undefined profile
206-
try {
207-
this.profiles.set(sessionID, await callback(this.profiles.get(sessionID) as ISptProfile)); // Cast as ISptProfile here since there should be no reason we're getting an undefined profile
208-
} catch (error) {
209-
this.logger.error(this.localisationService.getText("profile_save_callback_error", { callback, error }));
210-
this.profiles.set(sessionID, previous);
203+
const filePath = `${this.profileFilepath}${sessionID}.json`;
204+
205+
// Run pre-save callbacks before we save into json
206+
for (const [id, callback] of this.onBeforeSaveCallbacks) {
207+
const previous = this.profiles.get(sessionID) as ISptProfile; // Cast as ISptProfile here since there should be no reason we're getting an undefined profile
208+
try {
209+
this.profiles.set(sessionID, await callback(this.profiles.get(sessionID) as ISptProfile)); // Cast as ISptProfile here since there should be no reason we're getting an undefined profile
210+
} catch (error) {
211+
this.logger.error(
212+
this.localisationService.getText("profile_save_callback_error", { callback, error }),
213+
);
214+
this.profiles.set(sessionID, previous);
215+
}
211216
}
212-
}
213217

214-
const jsonProfile = this.jsonUtil.serialize(
215-
this.profiles.get(sessionID),
216-
!this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE).features.compressProfile,
217-
);
218+
const jsonProfile = this.jsonUtil.serialize(
219+
this.profiles.get(sessionID),
220+
!this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE).features.compressProfile,
221+
);
218222

219-
const sha1 = await this.hashUtil.generateSha1ForDataAsync(jsonProfile);
223+
const sha1 = await this.hashUtil.generateSha1ForDataAsync(jsonProfile);
220224

221-
if (typeof this.saveSHA1[sessionID] !== "string" || this.saveSHA1[sessionID] !== sha1) {
222-
this.saveSHA1[sessionID] = sha1;
223-
// save profile to disk
224-
await this.fileSystem.write(filePath, jsonProfile);
225+
if (typeof this.saveSHA1[sessionID] !== "string" || this.saveSHA1[sessionID] !== sha1) {
226+
this.saveSHA1[sessionID] = sha1;
227+
// save profile to disk
228+
await this.fileSystem.write(filePath, jsonProfile);
229+
}
230+
} finally {
231+
// Release the current lock
232+
release();
225233
}
226-
227-
this.profilesBeingSaved.delete(sessionID);
228234
}
229235

230236
/**
@@ -237,6 +243,16 @@ export class SaveServer {
237243

238244
this.profiles.delete(sessionID);
239245

246+
const pendingMutex = this.profilesBeingSavedMutex.get(sessionID);
247+
248+
if (pendingMutex) {
249+
// If the profile already has a mutex assigned, cancel all pending locks and release.
250+
pendingMutex.cancel();
251+
pendingMutex.release();
252+
253+
this.profilesBeingSavedMutex.delete(sessionID);
254+
}
255+
240256
await this.fileSystem.remove(file);
241257

242258
return !(await this.fileSystem.exists(file));

0 commit comments

Comments
 (0)