Skip to content

Commit bc0e2d9

Browse files
Merge pull request #135 from Sebastian-Webster/own-filelock-implementation
Use homemade lock file system
2 parents df1231e + ce5e4f0 commit bc0e2d9

File tree

6 files changed

+77
-67
lines changed

6 files changed

+77
-67
lines changed

package-lock.json

Lines changed: 2 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"@babel/preset-typescript": "^7.25.9",
3333
"@types/adm-zip": "^0.5.5",
3434
"@types/node": "^22.7.9",
35-
"@types/proper-lockfile": "^4.1.4",
3635
"@types/semver": "^7.5.8",
3736
"babel-jest": "^29.7.0",
3837
"jest": "^29.7.0",
@@ -41,7 +40,6 @@
4140
},
4241
"dependencies": {
4342
"adm-zip": "^0.5.16",
44-
"proper-lockfile": "^4.1.2",
4543
"semver": "^7.6.3"
4644
},
4745
"repository": {

src/libraries/Downloader.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import AdmZip from 'adm-zip'
66
import { normalize as normalizePath } from 'path';
77
import { randomUUID } from 'crypto';
88
import { execFile } from 'child_process';
9-
import { lockSync } from 'proper-lockfile';
109
import { BinaryInfo, InternalServerOptions } from '../../types';
11-
import { waitForLock } from './FileLock';
10+
import { lockFile, waitForLock } from './FileLock';
1211

1312
function getZipData(entry: AdmZip.IZipEntry): Promise<Buffer> {
1413
return new Promise((resolve, reject) => {
@@ -201,14 +200,14 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp
201200
return resolve(binaryPath)
202201
}
203202

204-
let releaseFunction: () => void;
203+
let releaseFunction: () => Promise<void>;
205204

206205
while (true) {
207206
try {
208-
releaseFunction = lockSync(extractedPath, {realpath: false})
207+
releaseFunction = await lockFile(extractedPath)
209208
break
210209
} catch (e) {
211-
if (e.code === 'ELOCKED') {
210+
if (e === 'LOCKED') {
212211
logger.log('Waiting for lock for MySQL version', version)
213212
await waitForLock(extractedPath, options)
214213
logger.log('Lock is gone for version', version)
@@ -249,7 +248,7 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp
249248
if (downloadTries >= options.downloadRetries) {
250249
//Only reject if we have met the downloadRetries limit
251250
try {
252-
releaseFunction()
251+
await releaseFunction()
253252
} catch (e) {
254253
logger.error('An error occurred while releasing lock after downloadRetries exhaustion. The error was:', e)
255254
}

src/libraries/Executor.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import { GenerateRandomPort } from "./Port";
88
import DBDestroySignal from "./AbortSignal";
99
import { ExecuteFileReturn, InstalledMySQLVersion, InternalServerOptions, MySQLDB } from "../../types";
1010
import {normalize as normalizePath, resolve as resolvePath} from 'path'
11-
import { lockSync } from 'proper-lockfile';
12-
import { waitForLock } from "./FileLock";
11+
import { lockFile, waitForLock } from "./FileLock";
1312

1413
class Executor {
1514
logger: Logger;
@@ -312,14 +311,14 @@ class Executor {
312311

313312
const copyPath = resolvePath(`${binaryFilepath}/../../lib/private/libaio.so.1`)
314313

315-
let lockRelease: () => void;
314+
let lockRelease: () => Promise<void>;
316315

317316
while(true) {
318317
try {
319-
lockRelease = lockSync(copyPath, {realpath: false})
318+
lockRelease = await lockFile(copyPath)
320319
break
321320
} catch (e) {
322-
if (e.code === 'ELOCKED') {
321+
if (e === 'LOCKED') {
323322
this.logger.log('Waiting for lock for libaio copy')
324323
await waitForLock(copyPath, options)
325324
this.logger.log('Lock is gone for libaio copy')
@@ -359,7 +358,7 @@ class Executor {
359358
} finally {
360359

361360
try {
362-
lockRelease()
361+
await lockRelease()
363362
} catch (e) {
364363
this.logger.error('Error unlocking libaio file:', e)
365364
}

src/libraries/FileLock.ts

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,73 @@
1-
import { checkSync } from "proper-lockfile";
1+
import fsPromises from 'fs/promises';
22
import { InternalServerOptions } from "../../types";
33

4-
export function waitForLock(path: string, options: InternalServerOptions): Promise<void> {
5-
return new Promise(async (resolve, reject) => {
6-
let retries = 0;
7-
while (retries <= options.lockRetries) {
8-
retries++
4+
const mtimeUpdateIntervalTime = 2_000
5+
const mtimeLimit = 10_000
6+
7+
export async function waitForLock(path: string, options: InternalServerOptions): Promise<void> {
8+
const lockPath = `${path}.lock`
9+
let retries = 0;
10+
11+
do {
12+
retries++;
13+
try {
14+
const stat = await fsPromises.stat(lockPath)
15+
if (performance.now() - stat.mtime.getTime() > mtimeLimit) {
16+
return
17+
} else {
18+
await new Promise(resolve => setTimeout(resolve, options.lockRetryWait))
19+
}
20+
} catch (e) {
21+
if (e.code === 'ENOENT') {
22+
return
23+
} else {
24+
throw e
25+
}
26+
}
27+
} while(retries <= options.lockRetries)
28+
29+
throw `lockRetries has been exceeded. Lock had not been released after ${options.lockRetryWait} * ${options.lockRetries} (${options.lockRetryWait * options.lockRetries}) milliseconds.`
30+
}
31+
32+
function setupMTimeEditor(lockPath: string): () => Promise<void> {
33+
const interval = setInterval(async () => {
34+
try {
35+
const time = new Date();
36+
await fsPromises.utimes(lockPath, time, time)
37+
} catch {}
38+
}, mtimeUpdateIntervalTime)
39+
40+
return async () => {
41+
clearInterval(interval)
42+
await fsPromises.rmdir(lockPath)
43+
}
44+
}
45+
46+
export async function lockFile(path: string): Promise<() => Promise<void>> {
47+
const lockPath = `${path}.lock`
48+
try {
49+
await fsPromises.mkdir(lockPath)
50+
return setupMTimeEditor(lockPath)
51+
} catch (e) {
52+
if (e.code === 'EEXIST') {
953
try {
10-
const locked = checkSync(path, {realpath: false});
11-
if (!locked) {
12-
return resolve()
54+
const stat = await fsPromises.stat(lockPath)
55+
if (performance.now() - stat.mtime.getTime() > mtimeLimit) {
56+
return setupMTimeEditor(lockPath)
1357
} else {
14-
await new Promise(resolve => setTimeout(resolve, options.lockRetryWait))
58+
throw 'LOCKED'
1559
}
1660
} catch (e) {
17-
return reject(e)
61+
if (e.code === 'ENOENT') {
62+
//This will run if the lock gets released after the EEXIST error is thrown but before the stat is checked.
63+
//If this is the case, the lock acquisition should be retried.
64+
return await lockFile(path)
65+
} else {
66+
throw e
67+
}
1868
}
69+
} else {
70+
throw e
1971
}
20-
reject(`lockRetries has been exceeded. Lock had not been released after ${options.lockRetryWait} * ${options.lockRetries} milliseconds.`)
21-
})
72+
}
2273
}

tests/versions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jest.setTimeout(500_000);
1717

1818
for (const version of versions) {
1919
for (const username of usernames) {
20-
test(`running on version ${version} with username ${username}`, async () => {
20+
test.concurrent(`running on version ${version} with username ${username}`, async () => {
2121
Error.stackTraceLimit = Infinity
2222
const options: ServerOptions = {
2323
version,

0 commit comments

Comments
 (0)