Skip to content

Commit 185cf2b

Browse files
committed
Added proper file locking to FileSaltStore
1 parent 69b81bd commit 185cf2b

File tree

5 files changed

+78
-4
lines changed

5 files changed

+78
-4
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"fastify": "5.6.1",
8585
"fastify-plugin": "5.1.0",
8686
"pino": "9.7.0",
87+
"proper-lockfile": "4.1.2",
8788
"ua-parser-js": "1.0.41"
8889
}
8990
}

src/services/salt-store/FileSaltStore.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {promises as fs} from 'fs';
22
import * as path from 'path';
3+
import * as Lockfile from 'proper-lockfile';
34
import {ISaltStore, SaltRecord} from './ISaltStore';
45
import logger from '../../utils/logger';
56

@@ -112,10 +113,33 @@ export class FileSaltStore implements ISaltStore {
112113

113114
/**
114115
* Executes a file operation with queuing to prevent concurrent writes.
116+
* Uses both per-instance queuing and cross-instance file locking.
115117
*/
116118
private async executeFileOperation<T>(operation: () => Promise<T>): Promise<T> {
117-
// Queue operations to prevent concurrent file access issues
118-
const currentOperation = this.fileOperationPromise.then(operation);
119+
// Queue operations to prevent concurrent file access issues within this instance
120+
const currentOperation = this.fileOperationPromise.then(async () => {
121+
// Acquire cross-instance lock before accessing file
122+
let release: (() => Promise<void>) | undefined;
123+
try {
124+
release = await Lockfile.lock(this.filePath, {
125+
retries: {
126+
retries: 10,
127+
minTimeout: 50,
128+
maxTimeout: 500
129+
},
130+
stale: 10000, // Consider lock stale after 10s
131+
realpath: false // Don't resolve symlinks
132+
});
133+
134+
// Execute operation while holding lock
135+
return await operation();
136+
} finally {
137+
// Always release lock
138+
if (release) {
139+
await release();
140+
}
141+
}
142+
});
119143
this.fileOperationPromise = currentOperation.then(() => {}, () => {});
120144
return currentOperation;
121145
}

src/types/proper-lockfile.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
declare module 'proper-lockfile' {
2+
export interface LockOptions {
3+
retries?: {
4+
retries?: number;
5+
minTimeout?: number;
6+
maxTimeout?: number;
7+
factor?: number;
8+
randomize?: boolean;
9+
};
10+
stale?: number;
11+
realpath?: boolean;
12+
fs?: Record<string, unknown>;
13+
onCompromised?: (err: Error) => void;
14+
}
15+
16+
export interface UnlockOptions {
17+
realpath?: boolean;
18+
fs?: Record<string, unknown>;
19+
}
20+
21+
export function lock(file: string, options?: LockOptions): Promise<() => Promise<void>>;
22+
export function unlock(file: string, options?: UnlockOptions): Promise<void>;
23+
export function check(file: string, options?: LockOptions): Promise<boolean>;
24+
export function lockSync(file: string, options?: LockOptions): () => void;
25+
export function unlockSync(file: string, options?: UnlockOptions): void;
26+
export function checkSync(file: string, options?: LockOptions): boolean;
27+
}

test/unit/services/salt-store/FileSaltStore.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ describe('FileSaltStore', () => {
1515
});
1616

1717
afterEach(async () => {
18-
// Clean up test file
18+
// Clean up test file and lock directory
1919
try {
2020
await fs.unlink(testFilePath);
2121
} catch {
2222
// Ignore errors if file doesn't exist
2323
}
24+
try {
25+
await fs.rm(`${testFilePath}.lock`, {recursive: true, force: true});
26+
} catch {
27+
// Ignore errors if lock directory doesn't exist
28+
}
2429
});
2530

2631
describe('set', () => {
@@ -542,6 +547,7 @@ describe('FileSaltStore', () => {
542547

543548
// Clean up
544549
await fs.rm(path.dirname(nestedPath), {recursive: true, force: true});
550+
await fs.rm(`${nestedPath}.lock`, {recursive: true, force: true}).catch(() => {});
545551
});
546552

547553
it('should handle concurrent writes correctly', async () => {
@@ -630,6 +636,8 @@ describe('FileSaltStore', () => {
630636
// Clean up
631637
await fs.unlink(store1Path);
632638
await fs.unlink(store2Path);
639+
await fs.rm(`${store1Path}.lock`, {recursive: true, force: true}).catch(() => {});
640+
await fs.rm(`${store2Path}.lock`, {recursive: true, force: true}).catch(() => {});
633641
});
634642

635643
it('should work correctly with multiple instances using same file', async () => {

yarn.lock

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5148,7 +5148,7 @@ gopd@^1.0.1, gopd@^1.2.0:
51485148
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
51495149
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
51505150

5151-
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
5151+
graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
51525152
version "4.2.11"
51535153
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
51545154
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
@@ -6489,6 +6489,15 @@ promise-map-series@^0.2.1:
64896489
dependencies:
64906490
rsvp "^3.0.14"
64916491

6492+
6493+
version "4.1.2"
6494+
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f"
6495+
integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==
6496+
dependencies:
6497+
graceful-fs "^4.2.4"
6498+
retry "^0.12.0"
6499+
signal-exit "^3.0.2"
6500+
64926501
proto3-json-serializer@^2.0.2:
64936502
version "2.0.2"
64946503
resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz#5b705203b4d58f3880596c95fad64902617529dd"
@@ -6728,6 +6737,11 @@ retry-request@^8.0.0:
67286737
extend "^3.0.2"
67296738
teeny-request "^10.0.0"
67306739

6740+
retry@^0.12.0:
6741+
version "0.12.0"
6742+
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
6743+
integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==
6744+
67316745
reusify@^1.0.4:
67326746
version "1.1.0"
67336747
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"

0 commit comments

Comments
 (0)