Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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