Skip to content

Commit

Permalink
Merge pull request #135 from Sebastian-Webster/own-filelock-implement…
Browse files Browse the repository at this point in the history
…ation

Use homemade lock file system
  • Loading branch information
Sebastian-Webster authored Nov 15, 2024
2 parents df1231e + ce5e4f0 commit bc0e2d9
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 67 deletions.
41 changes: 2 additions & 39 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"@babel/preset-typescript": "^7.25.9",
"@types/adm-zip": "^0.5.5",
"@types/node": "^22.7.9",
"@types/proper-lockfile": "^4.1.4",
"@types/semver": "^7.5.8",
"babel-jest": "^29.7.0",
"jest": "^29.7.0",
Expand All @@ -41,7 +40,6 @@
},
"dependencies": {
"adm-zip": "^0.5.16",
"proper-lockfile": "^4.1.2",
"semver": "^7.6.3"
},
"repository": {
Expand Down
11 changes: 5 additions & 6 deletions src/libraries/Downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import AdmZip from 'adm-zip'
import { normalize as normalizePath } from 'path';
import { randomUUID } from 'crypto';
import { execFile } from 'child_process';
import { lockSync } from 'proper-lockfile';
import { BinaryInfo, InternalServerOptions } from '../../types';
import { waitForLock } from './FileLock';
import { lockFile, waitForLock } from './FileLock';

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

let releaseFunction: () => void;
let releaseFunction: () => Promise<void>;

while (true) {
try {
releaseFunction = lockSync(extractedPath, {realpath: false})
releaseFunction = await lockFile(extractedPath)
break
} catch (e) {
if (e.code === 'ELOCKED') {
if (e === 'LOCKED') {
logger.log('Waiting for lock for MySQL version', version)
await waitForLock(extractedPath, options)
logger.log('Lock is gone for version', version)
Expand Down Expand Up @@ -249,7 +248,7 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp
if (downloadTries >= options.downloadRetries) {
//Only reject if we have met the downloadRetries limit
try {
releaseFunction()
await releaseFunction()
} catch (e) {
logger.error('An error occurred while releasing lock after downloadRetries exhaustion. The error was:', e)
}
Expand Down
11 changes: 5 additions & 6 deletions src/libraries/Executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { GenerateRandomPort } from "./Port";
import DBDestroySignal from "./AbortSignal";
import { ExecuteFileReturn, InstalledMySQLVersion, InternalServerOptions, MySQLDB } from "../../types";
import {normalize as normalizePath, resolve as resolvePath} from 'path'
import { lockSync } from 'proper-lockfile';
import { waitForLock } from "./FileLock";
import { lockFile, waitForLock } from "./FileLock";

class Executor {
logger: Logger;
Expand Down Expand Up @@ -312,14 +311,14 @@ class Executor {

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

let lockRelease: () => void;
let lockRelease: () => Promise<void>;

while(true) {
try {
lockRelease = lockSync(copyPath, {realpath: false})
lockRelease = await lockFile(copyPath)
break
} catch (e) {
if (e.code === 'ELOCKED') {
if (e === 'LOCKED') {
this.logger.log('Waiting for lock for libaio copy')
await waitForLock(copyPath, options)
this.logger.log('Lock is gone for libaio copy')
Expand Down Expand Up @@ -359,7 +358,7 @@ class Executor {
} finally {

try {
lockRelease()
await lockRelease()
} catch (e) {
this.logger.error('Error unlocking libaio file:', e)
}
Expand Down
77 changes: 64 additions & 13 deletions src/libraries/FileLock.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,73 @@
import { checkSync } from "proper-lockfile";
import fsPromises from 'fs/promises';
import { InternalServerOptions } from "../../types";

export function waitForLock(path: string, options: InternalServerOptions): Promise<void> {
return new Promise(async (resolve, reject) => {
let retries = 0;
while (retries <= options.lockRetries) {
retries++
const mtimeUpdateIntervalTime = 2_000
const mtimeLimit = 10_000

export async function waitForLock(path: string, options: InternalServerOptions): Promise<void> {
const lockPath = `${path}.lock`
let retries = 0;

do {
retries++;
try {
const stat = await fsPromises.stat(lockPath)
if (performance.now() - stat.mtime.getTime() > mtimeLimit) {
return
} else {
await new Promise(resolve => setTimeout(resolve, options.lockRetryWait))
}
} catch (e) {
if (e.code === 'ENOENT') {
return
} else {
throw e
}
}
} while(retries <= options.lockRetries)

throw `lockRetries has been exceeded. Lock had not been released after ${options.lockRetryWait} * ${options.lockRetries} (${options.lockRetryWait * options.lockRetries}) milliseconds.`
}

function setupMTimeEditor(lockPath: string): () => Promise<void> {
const interval = setInterval(async () => {
try {
const time = new Date();
await fsPromises.utimes(lockPath, time, time)
} catch {}
}, mtimeUpdateIntervalTime)

return async () => {
clearInterval(interval)
await fsPromises.rmdir(lockPath)
}
}

export async function lockFile(path: string): Promise<() => Promise<void>> {
const lockPath = `${path}.lock`
try {
await fsPromises.mkdir(lockPath)
return setupMTimeEditor(lockPath)
} catch (e) {
if (e.code === 'EEXIST') {
try {
const locked = checkSync(path, {realpath: false});
if (!locked) {
return resolve()
const stat = await fsPromises.stat(lockPath)
if (performance.now() - stat.mtime.getTime() > mtimeLimit) {
return setupMTimeEditor(lockPath)
} else {
await new Promise(resolve => setTimeout(resolve, options.lockRetryWait))
throw 'LOCKED'
}
} catch (e) {
return reject(e)
if (e.code === 'ENOENT') {
//This will run if the lock gets released after the EEXIST error is thrown but before the stat is checked.
//If this is the case, the lock acquisition should be retried.
return await lockFile(path)
} else {
throw e
}
}
} else {
throw e
}
reject(`lockRetries has been exceeded. Lock had not been released after ${options.lockRetryWait} * ${options.lockRetries} milliseconds.`)
})
}
}
2 changes: 1 addition & 1 deletion tests/versions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jest.setTimeout(500_000);

for (const version of versions) {
for (const username of usernames) {
test(`running on version ${version} with username ${username}`, async () => {
test.concurrent(`running on version ${version} with username ${username}`, async () => {
Error.stackTraceLimit = Infinity
const options: ServerOptions = {
version,
Expand Down

0 comments on commit bc0e2d9

Please sign in to comment.