From be714552769adda4a4007477d385fd658b89b73d Mon Sep 17 00:00:00 2001 From: ci010 Date: Wed, 15 Jan 2025 18:26:28 +0800 Subject: [PATCH] refactor: Use async-mutext to lock --- .vscode/settings.json | 1 - pnpm-lock.yaml | 33 +-- xmcl-keystone-ui/package.json | 5 +- xmcl-keystone-ui/src/composables/ftb.ts | 2 +- .../src/composables/instanceVersionInstall.ts | 11 +- .../src/composables/modpackInstaller.ts | 2 +- xmcl-runtime-api/index.ts | 3 +- .../src/services/InstanceService.ts | 3 - xmcl-runtime-api/src/util/LockKey.ts | 11 + xmcl-runtime-api/src/util/mutex.ts | 146 ----------- xmcl-runtime-api/src/util/semaphore.ts | 27 -- xmcl-runtime/app/LauncherApp.ts | 6 +- xmcl-runtime/app/MutexManager.ts | 23 ++ xmcl-runtime/app/SemaphoreManager.ts | 89 ------- .../instance/InstanceServerInfoService.ts | 1 - xmcl-runtime/instance/InstanceService.ts | 10 +- xmcl-runtime/instanceIO/InstanceIOService.ts | 6 +- .../instanceIO/InstanceInstallService.ts | 4 +- xmcl-runtime/mod/InstanceModsService.ts | 4 +- xmcl-runtime/moddb/ModMetadataService.ts | 2 +- xmcl-runtime/moddb/ProjectMappingService.ts | 2 +- xmcl-runtime/package.json | 1 + xmcl-runtime/resource/core/helper.ts | 231 ------------------ xmcl-runtime/resource/core/parseMetadata.ts | 2 +- .../resource/core/watchResourcesDirectory.ts | 2 +- .../InstanceResourcePacksService.ts | 7 +- .../ResourcePackPreviewService.ts | 10 +- xmcl-runtime/save/InstanceSavesService.ts | 8 +- xmcl-runtime/service/Service.ts | 63 +---- xmcl-runtime/{util => sql}/sqlHelper.ts | 0 xmcl-runtime/util/mutex.ts | 47 ---- xmcl-runtime/util/trafficAgent.ts | 19 -- 32 files changed, 102 insertions(+), 679 deletions(-) create mode 100644 xmcl-runtime-api/src/util/LockKey.ts delete mode 100644 xmcl-runtime-api/src/util/mutex.ts delete mode 100644 xmcl-runtime-api/src/util/semaphore.ts create mode 100644 xmcl-runtime/app/MutexManager.ts delete mode 100644 xmcl-runtime/app/SemaphoreManager.ts delete mode 100644 xmcl-runtime/resource/core/helper.ts rename xmcl-runtime/{util => sql}/sqlHelper.ts (100%) delete mode 100644 xmcl-runtime/util/mutex.ts delete mode 100644 xmcl-runtime/util/trafficAgent.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 78c5dcb09..62cf29aea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,7 +32,6 @@ "optifine", "resourcepack", "vite", - "vuex", "windicss", "xmcl" ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 327de4c0b..66b71af0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,6 +219,9 @@ importers: '@xmcl/user': specifier: workspace:* version: link:../xmcl/packages/user + async-mutex: + specifier: ^0.5.0 + version: 0.5.0 driver.js: specifier: ^1.3.1 version: 1.3.1 @@ -234,9 +237,6 @@ importers: markdown-it-link-attributes: specifier: ^4.0.1 version: 4.0.1 - rfc6902: - specifier: ^5.0.1 - version: 5.0.1 semver: specifier: ^7.5.8 version: 7.6.0 @@ -273,9 +273,6 @@ importers: vuetify: specifier: ^2.6.13 version: 2.6.15(vue@2.7.16) - vuex: - specifier: ^3.6.2 - version: 3.6.2(vue@2.7.16) devDependencies: '@intlify/unplugin-vue-i18n': specifier: ^0.13.0 @@ -415,6 +412,9 @@ importers: applicationinsights: specifier: ^2.9.1 version: 2.9.5 + async-mutex: + specifier: ^0.5.0 + version: 0.5.0 atomically: specifier: ^2.0.3 version: 2.0.3 @@ -2570,6 +2570,9 @@ packages: resolution: {integrity: sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==} engines: {node: <=0.11.8 || >0.11.10} + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} @@ -4231,9 +4234,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfc6902@5.0.1: - resolution: {integrity: sha512-tYGfLpKIq9X7lrt4o3IkD9w9bpeAtsejfAqWNR98AoxfTsZqCepKa8eDlRiX8QMiCOD9vMx0/YbKLx0G1nPi5w==} - rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -4761,11 +4761,6 @@ packages: peerDependencies: vue: ^2.6.4 - vuex@3.6.2: - resolution: {integrity: sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==} - peerDependencies: - vue: ^2.0.0 - wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -6494,6 +6489,10 @@ snapshots: semver: 5.7.1 shimmer: 1.2.1 + async-mutex@0.5.0: + dependencies: + tslib: 2.6.3 + async@3.2.4: {} asynckit@0.4.0: {} @@ -8275,8 +8274,6 @@ snapshots: reusify@1.0.4: {} - rfc6902@5.0.1: {} - rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -8850,10 +8847,6 @@ snapshots: dependencies: vue: 2.7.16 - vuex@3.6.2(vue@2.7.16): - dependencies: - vue: 2.7.16 - wcwidth@1.0.1: dependencies: defaults: 1.0.4 diff --git a/xmcl-keystone-ui/package.json b/xmcl-keystone-ui/package.json index e7c3c7f91..a234496d5 100644 --- a/xmcl-keystone-ui/package.json +++ b/xmcl-keystone-ui/package.json @@ -37,12 +37,12 @@ "@xmcl/runtime-api": "workspace:*", "@xmcl/text-component": "workspace:*", "@xmcl/user": "workspace:*", + "async-mutex": "^0.5.0", "driver.js": "^1.3.1", "fuzzy": "^0.1.3", "lodash.debounce": "^4.0.8", "markdown-it": "^13.0.1", "markdown-it-link-attributes": "^4.0.1", - "rfc6902": "^5.0.1", "semver": "^7.5.8", "skinview3d": "^3.0.1", "swrv": "v2-latest", @@ -54,8 +54,7 @@ "vue-i18n": "^8.28.2", "vue-i18n-bridge": "9.13.1", "vue-router": "^3.6.5", - "vuetify": "^2.6.13", - "vuex": "^3.6.2" + "vuetify": "^2.6.13" }, "devDependencies": { "@intlify/unplugin-vue-i18n": "^0.13.0", diff --git a/xmcl-keystone-ui/src/composables/ftb.ts b/xmcl-keystone-ui/src/composables/ftb.ts index 032d2cc38..8cb0b6d05 100644 --- a/xmcl-keystone-ui/src/composables/ftb.ts +++ b/xmcl-keystone-ui/src/composables/ftb.ts @@ -138,7 +138,7 @@ export function useFeedTheBeastModpackInstall() { installFiles(path, files) const lock = getInstanceLock(path) - lock.write(async () => { + lock.runExclusive(async () => { const resolved = existed ? await getResolvedVersion(existed) : undefined const instruction = await getInstallInstruction(path, config.runtime, options.version || '', resolved, all.value) await handleInstallInstruction(instruction) diff --git a/xmcl-keystone-ui/src/composables/instanceVersionInstall.ts b/xmcl-keystone-ui/src/composables/instanceVersionInstall.ts index 65dbcb5a5..d7dff654b 100644 --- a/xmcl-keystone-ui/src/composables/instanceVersionInstall.ts +++ b/xmcl-keystone-ui/src/composables/instanceVersionInstall.ts @@ -1,7 +1,8 @@ import { getSWRV } from '@/util/swrvGet' import type { AssetIndexIssue, AssetIssue, JavaVersion, LibraryIssue, MinecraftJarIssue, ResolvedVersion } from '@xmcl/core' import type { InstallProfileIssueReport } from '@xmcl/installer' -import { DiagnoseServiceKey, InstallServiceKey, Instance, InstanceServiceKey, JavaRecord, JavaServiceKey, ReadWriteLock, RuntimeVersions, ServerVersionHeader, VersionHeader, VersionServiceKey, parseOptifineVersion } from '@xmcl/runtime-api' +import { Mutex } from 'async-mutex' +import { DiagnoseServiceKey, InstallServiceKey, Instance, InstanceServiceKey, JavaRecord, JavaServiceKey, RuntimeVersions, ServerVersionHeader, VersionHeader, VersionServiceKey, parseOptifineVersion } from '@xmcl/runtime-api' import { InjectionKey, Ref, ShallowRef } from 'vue' import { InstanceResolveVersion } from './instanceVersion' import { useService } from './service' @@ -245,7 +246,7 @@ export function useInstanceVersionInstallInstruction(path: Ref, instance const loading = ref(0) const config = inject(kSWRVConfig) - const instanceLock: Record = {} + const instanceLock: Record = {} async function update(version: InstanceResolveVersion | undefined) { if (!version) return @@ -255,7 +256,7 @@ export function useInstanceVersionInstallInstruction(path: Ref, instance loading.value += 1 const lock = getInstanceLock(path.value) console.time('[getInstallInstruction]') - await lock.write(async () => { + await lock.runExclusive(async () => { try { const _path = version.instance const _selectedVersion = version.version @@ -287,7 +288,7 @@ export function useInstanceVersionInstallInstruction(path: Ref, instance if (lock) { return lock } - const newLock = new ReadWriteLock() + const newLock = new Mutex() instanceLock[path] = newLock return newLock } @@ -512,7 +513,7 @@ export function useInstanceVersionInstallInstruction(path: Ref, instance } const last = resolvedVersion.value const lock = getInstanceLock(path.value) - await lock.write(() => handleInstallInstruction(inst)) + await lock.runExclusive(() => handleInstallInstruction(inst)) if (last === resolvedVersion.value) { await update(last) } diff --git a/xmcl-keystone-ui/src/composables/modpackInstaller.ts b/xmcl-keystone-ui/src/composables/modpackInstaller.ts index 0c0fe81a4..bbb82513c 100644 --- a/xmcl-keystone-ui/src/composables/modpackInstaller.ts +++ b/xmcl-keystone-ui/src/composables/modpackInstaller.ts @@ -57,7 +57,7 @@ export function useModpackInstaller() { } const lock = getInstanceLock(instancePath) - lock.write(async () => { + lock.runExclusive(async () => { const resolved = version ? await resolveLocalVersion(version) : undefined const instruction = await getInstallInstruction(instancePath, runtime, '', resolved, all.value) await handleInstallInstruction(instruction) diff --git a/xmcl-runtime-api/index.ts b/xmcl-runtime-api/index.ts index b9e6e8fd0..9a9b7013f 100644 --- a/xmcl-runtime-api/index.ts +++ b/xmcl-runtime-api/index.ts @@ -79,10 +79,9 @@ export * from './src/task' export * from './src/util/authority' export * from './src/util/mavenVersion' export * from './src/util/SharedState' -export * from './src/util/mutex' export { default as packFormatVersionRange } from './src/util/packFormatVersionRange' export * from './src/util/promiseSignal' export { default as protocolToMinecraft } from './src/util/protocolToMinecraft' export * from './src/util/sdp' -export * from './src/util/semaphore' +export * from './src/util/LockKey' export * from './src/util/versionRange' diff --git a/xmcl-runtime-api/src/services/InstanceService.ts b/xmcl-runtime-api/src/services/InstanceService.ts index a9102fdb3..d7ec2e19c 100644 --- a/xmcl-runtime-api/src/services/InstanceService.ts +++ b/xmcl-runtime-api/src/services/InstanceService.ts @@ -40,9 +40,6 @@ export class InstanceState { instances: Instance[] = [] instanceAdd(instance: Instance) { - /** - * Prevent the case that hot reload keep the vuex state - */ if (!this.all[instance.path]) { // TODO: remove in vue3 // set(this.all, instance.path, { ...instance, serverStatus: UNKNOWN_STATUS }) diff --git a/xmcl-runtime-api/src/util/LockKey.ts b/xmcl-runtime-api/src/util/LockKey.ts new file mode 100644 index 000000000..e3026a792 --- /dev/null +++ b/xmcl-runtime-api/src/util/LockKey.ts @@ -0,0 +1,11 @@ + +export const LockKey = { + versions: 'versions', + libraries: 'libraries', + assets: 'assets', + version: (v: string) => `versions/${v}`, + instance: (p: string) => `instances/${p}`, + instanceRemove: (p: string) => `instances/${p}/remove`, + shaderpacks: (p: string) => `shaderpacks/${p}`, + resourcepacks: (p: string) => `resourcepacks/${p}`, +} diff --git a/xmcl-runtime-api/src/util/mutex.ts b/xmcl-runtime-api/src/util/mutex.ts deleted file mode 100644 index a1d306fc0..000000000 --- a/xmcl-runtime-api/src/util/mutex.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { PromiseSignal, createPromiseSignal } from './promiseSignal' - -export enum LockStatus { - Idle, - Reading, - Writing, -} - -export interface SemaphoreListener { - (delta: number, semaphore: number): void -} - -/** - * A simple implementation of read write mutex on a resource. It provide api to acquire the read/write operation on a resource. - * - * This ensure all operations accessing the resource by this lock will not violate the mutual exclusion. - */ -export class ReadWriteLock { - #queue: Array<[() => Promise, PromiseSignal, boolean]> = [] - #free: PromiseSignal[] = [] - private status: LockStatus = LockStatus.Idle - /** - * The integer representing number of worker is occuping the lock. - * - For the read operation, it can be as many as possible. - * - For the write operation, it can only be 1. - */ - private semaphore = 0 - - constructor(private listener?: SemaphoreListener) { } - - async #processOperation(operation: () => Promise, signal: PromiseSignal, isRead: boolean) { - try { - this.status = isRead ? LockStatus.Reading : LockStatus.Writing - this.#up() - const result = await operation() - signal.resolve(result) - } catch (e) { - signal.reject(e) - } finally { - this.#down() - if (this.semaphore === 0) { - this.status = LockStatus.Idle - this.#processIfIdle() - } - } - } - - async #processIfIdle() { - if (this.status === LockStatus.Idle) { - while (this.#queue.length > 0) { - const [operation, signal, isRead] = this.#queue.shift()! - this.#processOperation(operation, signal, isRead) - const thisIsWrite = !isRead - const isNextWrite = !(this.#queue[0]?.[2] ?? true) - if (thisIsWrite || isNextWrite) { - // Wait write if next is write or current is writing - await signal.promise.catch(() => { }) - while (this.#free.length > 0) { - // Wait all read operation to finish - const freeRead = this.#free.shift()! - await freeRead.promise.catch(() => { }) - } - } - } - } - } - - #up() { - this.semaphore += 1 - this.listener?.(1, this.semaphore) - } - - #down() { - this.semaphore -= 1 - this.listener?.(-1, this.semaphore) - } - - getStatus() { - return this.status - } - - /** - * Submit a read operation to the resource. Once the resource is reading, all read operations can get the lock. - * - * The write operation cannot execute if the status is Reading. - * - * @param operation The read operation - */ - async read(operation: () => Promise): Promise { - const signal = createPromiseSignal() - if (this.status === LockStatus.Reading) { - this.#free.push(signal) - this.#processOperation(operation, signal, true) - return signal.promise - } - - this.#queue.push([operation, signal, true]) - this.#processIfIdle() - return signal.promise - } - - async acquireRead(): Promise<() => void> { - let start = () => { } - let end = () => { } - const startPromise = new Promise((resolve) => { - start = resolve - }) - const endPromise = new Promise((resolve) => { - end = resolve - }) - this.read(async () => { - start() - return endPromise - }) - await startPromise - return end - } - - /** - * Submit a write operation to the resource. A write operation can only execute if the status is idel. - * @param operation The write operation. - */ - write(operation: () => Promise): Promise { - const signal = createPromiseSignal() - this.#queue.push([operation, signal, false]) - this.#processIfIdle() - return signal.promise - } - - async acquireWrite() { - let start = () => { } - let end = () => { } - const startPromise = new Promise((resolve) => { - start = resolve - }) - const endPromise = new Promise((resolve) => { - end = resolve - }) - this.write(async () => { - start() - return endPromise - }) - await startPromise - return end - } -} diff --git a/xmcl-runtime-api/src/util/semaphore.ts b/xmcl-runtime-api/src/util/semaphore.ts deleted file mode 100644 index 61c3f35a0..000000000 --- a/xmcl-runtime-api/src/util/semaphore.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ServiceKey } from '../services/Service' - -export const LockKey = { - versions: 'versions', - libraries: 'libraries', - assets: 'assets', - version: (v: string) => `versions/${v}`, - instance: (p: string) => `instances/${p}`, - shaderpacks: (p: string) => `shaderpacks/${p}`, - resourcepacks: (p: string) => `resourcepacks/${p}`, -} - -export function resolveLocks(lock: string): string[] { - const [type, body] = lock.indexOf(':') === -1 ? ['', lock] : lock.split(':') - const locks = [] - for (let pos = body.indexOf('/'); pos !== -1; pos = body.indexOf('/', pos + 1)) { - locks.push(body.substring(0, pos)) - } - locks.push(body) - return type ? locks.map(v => `${type}:${v}`) : locks -} - -export type ParamSerializer = (...params: any[]) => string | undefined - -export function getServiceSemaphoreKey(key: ServiceKey, method: keyof T, params?: string) { - return params ? `${key}.${method as string}(${params})` : `${key}.${method as string}()` -} diff --git a/xmcl-runtime/app/LauncherApp.ts b/xmcl-runtime/app/LauncherApp.ts index 31b8d23ec..b7f1e40ee 100644 --- a/xmcl-runtime/app/LauncherApp.ts +++ b/xmcl-runtime/app/LauncherApp.ts @@ -19,7 +19,7 @@ import { LauncherAppPlugin } from './LauncherAppPlugin' import { LauncherAppUpdater } from './LauncherAppUpdater' import { LauncherProtocolHandler } from './LauncherProtocolHandler' import { SecretStorage } from './SecretStorage' -import SemaphoreManager from './SemaphoreManager' +import MutexManager from './MutexManager' import { Shell } from './Shell' import { kGameDataPath, kTempDataPath } from './gameDataPath' import { InjectionKey, ObjectFactory } from './objectRegistry' @@ -70,7 +70,7 @@ export class LauncherApp extends EventEmitter { */ readonly minecraftDataPath: string - readonly semaphoreManager: SemaphoreManager + readonly mutex: MutexManager readonly launcherAppManager: LauncherAppManager /** * The log event emitter. This should only be used for log consumer like telemetry or log file writer. @@ -184,7 +184,7 @@ export class LauncherApp extends EventEmitter { this.controller = getController(this) this.updater = getUpdater(this) - this.semaphoreManager = new SemaphoreManager(this) + this.mutex = new MutexManager(this) this.launcherAppManager = new LauncherAppManager(this) for (const plugin of plugins) { diff --git a/xmcl-runtime/app/MutexManager.ts b/xmcl-runtime/app/MutexManager.ts new file mode 100644 index 000000000..99bba3d03 --- /dev/null +++ b/xmcl-runtime/app/MutexManager.ts @@ -0,0 +1,23 @@ +import { Mutex } from 'async-mutex' +import { LauncherApp } from './LauncherApp' + +/** + * Just a simple mutex manager. + */ +export default class MutexManager { + private all: Record = {} + + constructor(private app: LauncherApp) { + } + + /** + * Get the mutex for the resource with the path. + * @param resourcePath The resource path + */ + of(resourcePath: string) { + if (!this.all[resourcePath]) { + this.all[resourcePath] = new Mutex() + } + return this.all[resourcePath] + } +} diff --git a/xmcl-runtime/app/SemaphoreManager.ts b/xmcl-runtime/app/SemaphoreManager.ts deleted file mode 100644 index 311427c8d..000000000 --- a/xmcl-runtime/app/SemaphoreManager.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { nextTick } from 'process' -import { LauncherApp } from '../app/LauncherApp' -import { ReadWriteLock } from '@xmcl/runtime-api' -import { Logger } from '~/logger' - -export default class SemaphoreManager { - private locks: Record = {} - - private semaphore: Record = {} - /** - * Total order is used to check if client/server is sync - */ - private order = 0 - private semaphoreWaiter: Record void>> = {} - private logger: Logger - - constructor(private app: LauncherApp) { - this.logger = app.getLogger('SemaphoreManager') - app.controller.handle('semaphore', () => { - return [this.semaphore, this.order] - }) - app.controller.handle('semaphoreAbort', (_, key) => { - this.logger.log(`Force release the semaphore: ${key}`) - this.release(key) - }) - } - - getLock(resourcePath: string) { - if (!this.locks[resourcePath]) { - this.locks[resourcePath] = new ReadWriteLock((delta) => { - if (delta > 0) { - this.acquire(resourcePath) - } else { - this.release(resourcePath) - } - }) - } - return this.locks[resourcePath] - } - - /** - * Acquire and broadcast the key is in used. - * @param key The key or keys to acquire - */ - acquire(key: string) { - if (key in this.semaphore) { - this.semaphore[key] += 1 - } else { - this.semaphore[key] = 1 - } - this.order += 1 - this.app.controller.broadcast('acquire', [key, this.order]) - } - - wait(key: string) { - if (this.semaphore[key] === 0) return Promise.resolve() - return new Promise((resolve) => { - if (!this.semaphoreWaiter[key]) { - this.semaphoreWaiter[key] = [] - } - this.semaphoreWaiter[key].push(resolve) - }) - } - - /** - * Release and broadcast the key is not used. - * @param key The key or keys to release - */ - release(key: string) { - if (key in this.semaphore) { - this.semaphore[key] -= 1 - } else { - this.semaphore[key] = 0 - } - this.order += 1 - if (this.semaphore[key] === 0) { - nextTick(() => { - if (this.semaphore[key] === 0) { - const all = this.semaphoreWaiter[key] - if (all) { - for (const w of all) w() - this.semaphoreWaiter[key] = [] - } - } - }) - } - this.app.controller.broadcast('release', [key, this.order]) - } -} diff --git a/xmcl-runtime/instance/InstanceServerInfoService.ts b/xmcl-runtime/instance/InstanceServerInfoService.ts index 1cc120dfb..d42e0b132 100644 --- a/xmcl-runtime/instance/InstanceServerInfoService.ts +++ b/xmcl-runtime/instance/InstanceServerInfoService.ts @@ -5,7 +5,6 @@ import { Inject, kGameDataPath, LauncherAppKey, PathResolver } from '~/app' import { AbstractService, ExposeServiceKey, ServiceStateManager } from '~/service' import { LauncherApp } from '../app/LauncherApp' import { exists, hardLinkFiles, isHardLinked, unHardLinkFiles } from '../util/fs' -import { watch } from 'chokidar' @ExposeServiceKey(InstanceServerInfoServiceKey) export class InstanceServerInfoService extends AbstractService implements IInstanceServerInfoService { diff --git a/xmcl-runtime/instance/InstanceService.ts b/xmcl-runtime/instance/InstanceService.ts index 7f5de46af..513c759ee 100644 --- a/xmcl-runtime/instance/InstanceService.ts +++ b/xmcl-runtime/instance/InstanceService.ts @@ -1,4 +1,4 @@ -import { CreateInstanceOption, EditInstanceOptions, InstanceService as IInstanceService, InstanceSchema, InstanceServiceKey, InstanceState, InstancesSchema, SharedState, RuntimeVersions, createTemplate } from '@xmcl/runtime-api' +import { CreateInstanceOption, EditInstanceOptions, InstanceService as IInstanceService, InstanceSchema, InstanceServiceKey, InstanceState, InstancesSchema, SharedState, RuntimeVersions, createTemplate, LockKey } from '@xmcl/runtime-api' import filenamify from 'filenamify' import { existsSync } from 'fs' import { copy, ensureDir, readdir, readlink, rename, rm, stat } from 'fs-extra' @@ -330,15 +330,17 @@ export class InstanceService extends StatefulService implements I requireString(path) const isManaged = this.isUnderManaged(path) - const lock = this.semaphoreManager.getLock(`remove://${path}`) + const lock = this.mutex.of(LockKey.instanceRemove(path)) + const instanceLock = this.mutex.of(LockKey.instance(path)) if (isManaged && await exists(path)) { - await lock.write(async () => { + await lock.runExclusive(async () => { + instanceLock.cancel() const oldHandlers = this.#removeHandlers[path] for (const handlerRef of oldHandlers || []) { handlerRef.deref()?.() } try { - await rm(path, { recursive: true, force: true, maxRetries: 3 }) + await rm(path, { recursive: true, force: true, maxRetries: 1 }) } catch (e) { if (isSystemError(e) && e.code === ENOENT_ERROR) { this.warn(`Fail to remove instance ${path}`) diff --git a/xmcl-runtime/instanceIO/InstanceIOService.ts b/xmcl-runtime/instanceIO/InstanceIOService.ts index f6068158c..6a9934ba2 100644 --- a/xmcl-runtime/instanceIO/InstanceIOService.ts +++ b/xmcl-runtime/instanceIO/InstanceIOService.ts @@ -223,7 +223,7 @@ export class InstanceIOService extends AbstractService implements IInstanceIOSer // add assets if (includeAssets) { - releases.push(await this.semaphoreManager.getLock(LockKey.assets).acquireRead()) + releases.push(await this.mutex.of(LockKey.assets).acquire()) const assetsJson = resolve(root, 'assets', 'indexes', `${version.assets}.json`) zipTask.addFile(assetsJson, `assets/indexes/${version.assets}.json`) const objects = await readFile(assetsJson, 'utf8').then(JSON.parse).then(manifest => manifest.objects) @@ -236,7 +236,7 @@ export class InstanceIOService extends AbstractService implements IInstanceIOSer const versionsChain = version.pathChain for (const versionPath of versionsChain) { const versionId = basename(versionPath) - releases.push(await this.semaphoreManager.getLock(LockKey.version(versionId)).acquireRead()) + releases.push(await this.mutex.of(LockKey.version(versionId)).acquire()) if (includeVersionJar && await exists(join(versionPath, `${versionId}.jar`))) { zipTask.addFile(join(versionPath, `${versionId}.jar`), `versions/${versionId}/${versionId}.jar`) } @@ -245,7 +245,7 @@ export class InstanceIOService extends AbstractService implements IInstanceIOSer // add libraries if (includeLibraries) { - releases.push(await this.semaphoreManager.getLock(LockKey.libraries).acquireRead()) + releases.push(await this.mutex.of(LockKey.libraries).acquire()) for (const lib of version.libraries) { zipTask.addFile(resolve(root, 'libraries', lib.download.path), `libraries/${lib.download.path}`) diff --git a/xmcl-runtime/instanceIO/InstanceInstallService.ts b/xmcl-runtime/instanceIO/InstanceInstallService.ts index 507a56e06..8f3aa2ba8 100644 --- a/xmcl-runtime/instanceIO/InstanceInstallService.ts +++ b/xmcl-runtime/instanceIO/InstanceInstallService.ts @@ -50,10 +50,10 @@ export class InstanceInstallService extends AbstractService implements IInstance instancePath, ) - const lock = this.semaphoreManager.getLock(LockKey.instance(instancePath)) + const lock = this.mutex.of(LockKey.instance(instancePath)) const updateInstanceTask = task('installInstance', async function () { - await lock.write(async () => { + await lock.runExclusive(async () => { try { const newAddedFiles = files.filter(f => f.operation === 'add' || f.operation === 'backup-add') await this.yield(new ResolveInstanceFileTask(newAddedFiles, curseforgeClient, modrinthClient)) diff --git a/xmcl-runtime/mod/InstanceModsService.ts b/xmcl-runtime/mod/InstanceModsService.ts index 9e229605d..ba9b62294 100644 --- a/xmcl-runtime/mod/InstanceModsService.ts +++ b/xmcl-runtime/mod/InstanceModsService.ts @@ -114,13 +114,13 @@ export class InstanceModsService extends AbstractService implements IInstanceMod async watch(instancePath: string): Promise> { if (!instancePath) throw new AnyError('WatchModError', 'Cannot watch instance mods on empty path') - const lock = this.semaphoreManager.getLock(LockKey.instance(instancePath)) + const lock = this.mutex.of(LockKey.instance(instancePath)) const stateManager = await this.app.registry.get(ServiceStateManager) return stateManager.registerOrGet(getInstanceModStateKey(instancePath), async ({ doAsyncOperation }) => { const basePath = join(instancePath, 'mods') await ensureDir(basePath) - const { dispose, revalidate, state } = this.resourceManager.watch(basePath, ResourceDomain.Mods, (func) => doAsyncOperation(lock.read(func))) + const { dispose, revalidate, state } = this.resourceManager.watch(basePath, ResourceDomain.Mods, (func) => doAsyncOperation(lock.waitForUnlock().then(func))) const instanceService = await this.app.registry.get(InstanceService) instanceService.registerRemoveHandler(instancePath, dispose) diff --git a/xmcl-runtime/moddb/ModMetadataService.ts b/xmcl-runtime/moddb/ModMetadataService.ts index 3be49be5d..77ee362f3 100644 --- a/xmcl-runtime/moddb/ModMetadataService.ts +++ b/xmcl-runtime/moddb/ModMetadataService.ts @@ -11,7 +11,7 @@ import { SqliteWASMDialect } from '~/sql' import { TaskFn, kTaskExecutor } from '~/task' import { checksumFromStream } from '~/util/fs' import { isNonnull } from '~/util/object' -import { jsonObjectFrom } from '~/util/sqlHelper' +import { jsonObjectFrom } from '~/sql/sqlHelper' interface Database { file: { diff --git a/xmcl-runtime/moddb/ProjectMappingService.ts b/xmcl-runtime/moddb/ProjectMappingService.ts index 84b068c4a..be7ee155d 100644 --- a/xmcl-runtime/moddb/ProjectMappingService.ts +++ b/xmcl-runtime/moddb/ProjectMappingService.ts @@ -48,7 +48,7 @@ export class ProjectMappingService extends AbstractService implements IProjectMa if (this.#db?.locale === locale) return this.#db.db let filePath = join(this.app.appDataPath, `project-mapping-${locale}.sqlite`) - await this.semaphoreManager.getLock('project-mapping').write(async () => { + await this.mutex.of('project-mapping').runExclusive(async () => { let original = `https://xmcl.blob.core.windows.net/project-mapping/${locale}.sqlite` async function exists() { diff --git a/xmcl-runtime/package.json b/xmcl-runtime/package.json index 256a8786d..0e96d3d44 100644 --- a/xmcl-runtime/package.json +++ b/xmcl-runtime/package.json @@ -22,6 +22,7 @@ "@azure/msal-common": "^14.14.0", "@azure/msal-node": "^2.12.0", "xxhash-wasm": "^1.0.2", + "async-mutex": "^0.5.0", "@node-rs/crc32-wasm32-wasi": "^1.10.3", "@xmcl/client": "workspace:*", "@xmcl/core": "workspace:*", diff --git a/xmcl-runtime/resource/core/helper.ts b/xmcl-runtime/resource/core/helper.ts deleted file mode 100644 index ac4fb3898..000000000 --- a/xmcl-runtime/resource/core/helper.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { SelectQueryBuilder, RawBuilder, Simplify, sql, Expression, SelectQueryNode, AliasNode, ColumnNode, ExpressionWrapper, IdentifierNode, ReferenceNode, TableNode, ValueNode } from 'kysely' - -function getJsonObjectArgs( - node: SelectQueryNode, - table: string, -): Expression[] { - const args: Expression[] = [] - - for (const { selection: s } of node.selections ?? []) { - if (ReferenceNode.is(s) && ColumnNode.is(s.column)) { - args.push( - colName(s.column.column.name), - colRef(table, s.column.column.name), - ) - } else if (ColumnNode.is(s)) { - args.push(colName(s.column.name), colRef(table, s.column.name)) - } else if (AliasNode.is(s) && IdentifierNode.is(s.alias)) { - args.push(colName(s.alias.name), colRef(table, s.alias.name)) - } else { - throw new Error('can\'t extract column names from the select query node') - } - } - - return args -} - -function colName(col: string): Expression { - return new ExpressionWrapper(ValueNode.createImmediate(col)) -} - -function colRef(table: string, col: string): Expression { - return new ExpressionWrapper( - ReferenceNode.create(ColumnNode.create(col), TableNode.create(table)), - ) -} - -/** - * A SQLite helper for aggregating a subquery into a JSON array. - * - * NOTE: This helper only works correctly if you've installed the `ParseJSONResultsPlugin`. - * Otherwise the nested selections will be returned as JSON strings. - * - * The plugin can be installed like this: - * - * ```ts - * const db = new Kysely({ - * dialect: new SqliteDialect(config), - * plugins: [new ParseJSONResultsPlugin()] - * }) - * ``` - * - * ### Examples - * - * ```ts - * const result = await db - * .selectFrom('person') - * .select((eb) => [ - * 'id', - * jsonArrayFrom( - * eb.selectFrom('pet') - * .select(['pet.id as pet_id', 'pet.name']) - * .whereRef('pet.owner_id', '=', 'person.id') - * .orderBy('pet.name') - * ).as('pets') - * ]) - * .execute() - * - * result[0].id - * result[0].pets[0].pet_id - * result[0].pets[0].name - * ``` - * - * The generated SQL (SQLite): - * - * ```sql - * select "id", ( - * select coalesce(json_group_array(json_object( - * 'pet_id', "agg"."pet_id", - * 'name', "agg"."name" - * )), '[]') from ( - * select "pet"."id" as "pet_id", "pet"."name" - * from "pet" - * where "pet"."owner_id" = "person"."id" - * order by "pet"."name" - * ) as "agg" - * ) as "pets" - * from "person" - * ``` - */ -export function jsonArrayFrom( - expr: SelectQueryBuilder, -): RawBuilder[]> { - return sql`(select coalesce(json_group_array(json_object(${sql.join( - getSqliteJsonObjectArgs(expr.toOperationNode(), 'agg'), - )})), '[]') from ${expr} as agg)` -} - -/** - * A SQLite helper for turning a subquery into a JSON object. - * - * The subquery must only return one row. - * - * NOTE: This helper only works correctly if you've installed the `ParseJSONResultsPlugin`. - * Otherwise the nested selections will be returned as JSON strings. - * - * The plugin can be installed like this: - * - * ```ts - * const db = new Kysely({ - * dialect: new SqliteDialect(config), - * plugins: [new ParseJSONResultsPlugin()] - * }) - * ``` - * - * ### Examples - * - * ```ts - * const result = await db - * .selectFrom('person') - * .select((eb) => [ - * 'id', - * jsonObjectFrom( - * eb.selectFrom('pet') - * .select(['pet.id as pet_id', 'pet.name']) - * .whereRef('pet.owner_id', '=', 'person.id') - * .where('pet.is_favorite', '=', true) - * ).as('favorite_pet') - * ]) - * .execute() - * - * result[0].id - * result[0].favorite_pet.pet_id - * result[0].favorite_pet.name - * ``` - * - * The generated SQL (SQLite): - * - * ```sql - * select "id", ( - * select json_object( - * 'pet_id', "obj"."pet_id", - * 'name', "obj"."name" - * ) from ( - * select "pet"."id" as "pet_id", "pet"."name" - * from "pet" - * where "pet"."owner_id" = "person"."id" - * and "pet"."is_favorite" = ? - * ) as obj - * ) as "favorite_pet" - * from "person"; - * ``` - */ -export function jsonObjectFrom( - expr: SelectQueryBuilder, -): RawBuilder | null> { - return sql`(select json_object(${sql.join( - getSqliteJsonObjectArgs(expr.toOperationNode(), 'obj'), - )}) from ${expr} as obj)` -} - -/** - * The SQLite `json_object` function. - * - * NOTE: This helper only works correctly if you've installed the `ParseJSONResultsPlugin`. - * Otherwise the nested selections will be returned as JSON strings. - * - * The plugin can be installed like this: - * - * ```ts - * const db = new Kysely({ - * dialect: new SqliteDialect(config), - * plugins: [new ParseJSONResultsPlugin()] - * }) - * ``` - * - * ### Examples - * - * ```ts - * const result = await db - * .selectFrom('person') - * .select((eb) => [ - * 'id', - * jsonBuildObject({ - * first: eb.ref('first_name'), - * last: eb.ref('last_name'), - * full: sql`first_name || ' ' || last_name` - * }).as('name') - * ]) - * .execute() - * - * result[0].id - * result[0].name.first - * result[0].name.last - * result[0].name.full - * ``` - * - * The generated SQL (SQLite): - * - * ```sql - * select "id", json_object( - * 'first', first_name, - * 'last', last_name, - * 'full', "first_name" || ' ' || "last_name" - * ) as "name" - * from "person" - * ``` - */ -export function jsonBuildObject>>( - obj: O, -): RawBuilder< - Simplify<{ - [K in keyof O]: O[K] extends Expression ? V : never - }> -> { - return sql`json_object(${sql.join( - Object.keys(obj).flatMap((k) => [sql.lit(k), obj[k]]), - )})` -} - -function getSqliteJsonObjectArgs( - node: SelectQueryNode, - table: string, -): Expression[] { - try { - return getJsonObjectArgs(node, table) - } catch { - throw new Error( - 'SQLite jsonArrayFrom and jsonObjectFrom functions can only handle explicit selections due to limitations of the json_object function. selectAll() is not allowed in the subquery.', - ) - } -} diff --git a/xmcl-runtime/resource/core/parseMetadata.ts b/xmcl-runtime/resource/core/parseMetadata.ts index a6e225a75..9e85646d6 100644 --- a/xmcl-runtime/resource/core/parseMetadata.ts +++ b/xmcl-runtime/resource/core/parseMetadata.ts @@ -1,10 +1,10 @@ import { Exception, File, ResourceDomain, ResourceMetadata } from '@xmcl/runtime-api' import { pickMetadata } from './generateResource' -import { jsonArrayFrom } from './helper' import { ResourceContext } from './ResourceContext' import { ResourceWorkerQueuePayload } from './ResourceWorkerQueuePayload' import { ResourceSnapshotTable } from './schema' import { upsertMetadata } from './upsertMetadata' +import { jsonArrayFrom } from '~/sql/sqlHelper' class ParseException extends Exception<{ type: 'parseResourceException'; code: string }> { } diff --git a/xmcl-runtime/resource/core/watchResourcesDirectory.ts b/xmcl-runtime/resource/core/watchResourcesDirectory.ts index ebc08229f..5626d4f1e 100644 --- a/xmcl-runtime/resource/core/watchResourcesDirectory.ts +++ b/xmcl-runtime/resource/core/watchResourcesDirectory.ts @@ -6,6 +6,7 @@ import { copy, watch } from 'fs-extra' import debounce from 'lodash.debounce' import { basename, dirname, isAbsolute, join, resolve } from 'path' import { Logger } from '~/logger' +import { jsonArrayFrom } from '~/sql/sqlHelper' import { AggregateExecutor, WorkerQueue } from '~/util/aggregator' import { AnyError, isSystemError } from '~/util/error' import { linkOrCopyFile } from '~/util/fs' @@ -14,7 +15,6 @@ import { ResourceContext } from './ResourceContext' import { ResourceWorkerQueuePayload } from './ResourceWorkerQueuePayload' import { getFile, getFiles } from './files' import { generateResourceV3, pickMetadata } from './generateResource' -import { jsonArrayFrom } from './helper' import { getOrParseMetadata } from './parseMetadata' import { shouldIgnoreFile } from './pathUtils' import { ResourceSnapshotTable } from './schema' diff --git a/xmcl-runtime/resourcePack/InstanceResourcePacksService.ts b/xmcl-runtime/resourcePack/InstanceResourcePacksService.ts index 12ab42929..390641da8 100644 --- a/xmcl-runtime/resourcePack/InstanceResourcePacksService.ts +++ b/xmcl-runtime/resourcePack/InstanceResourcePacksService.ts @@ -22,8 +22,9 @@ export class InstanceResourcePackService extends AbstractInstanceDomainService i domain = ResourceDomain.ResourcePacks - override link(instancePath: string, force?: boolean): Promise { - const lock = this.semaphoreManager.getLock(LockKey.instance(instancePath)) - return lock.read(() => super.link(instancePath, force)) + override async link(instancePath: string, force?: boolean): Promise { + const lock = this.mutex.of(LockKey.instance(instancePath)) + await lock.waitForUnlock() + return await super.link(instancePath, force) } } diff --git a/xmcl-runtime/resourcePack/ResourcePackPreviewService.ts b/xmcl-runtime/resourcePack/ResourcePackPreviewService.ts index 6d87be1d8..b946ae366 100644 --- a/xmcl-runtime/resourcePack/ResourcePackPreviewService.ts +++ b/xmcl-runtime/resourcePack/ResourcePackPreviewService.ts @@ -6,8 +6,8 @@ import { InstanceService } from '~/instance' import { LaunchService } from '~/launch' import { AbstractService, ExposeServiceKey } from '~/service' import { LauncherApp } from '../app/LauncherApp' -import { Queue } from '../util/mutex' import { InstanceResourcePackService } from './InstanceResourcePacksService' +import { Mutex } from 'async-mutex' interface NamedResourcePackWrapper extends ResourcePackWrapper { path: string @@ -23,7 +23,7 @@ export class ResourcePackPreviewService extends AbstractService implements IReso private cachedJsonVersion: string | undefined - private queue = new Queue() + private queue = new Mutex() private active = false @@ -35,7 +35,7 @@ export class ResourcePackPreviewService extends AbstractService implements IReso super(app) launchService.on('minecraft-start', () => { if (this.active) { - this.queue.waitUntilEmpty().then(() => { + this.queue.waitForUnlock().then(() => { // deactivate once game started this.active = false this.resourceManager.clear() @@ -149,11 +149,11 @@ export class ResourcePackPreviewService extends AbstractService implements IReso if (this.resourceManager.list.length === 0) { // if no resource packs loaded, load it... - if (!this.queue.isWaiting()) { + if (!this.queue.isLocked()) { // TODO: fix this // await this.updateResourcePacks(this.instanceGameSettingService.state.resourcePacks) } else { - await this.queue.waitUntilEmpty() + await this.queue.waitForUnlock() } } diff --git a/xmcl-runtime/save/InstanceSavesService.ts b/xmcl-runtime/save/InstanceSavesService.ts index de7b7f5c6..ad1fe9d62 100644 --- a/xmcl-runtime/save/InstanceSavesService.ts +++ b/xmcl-runtime/save/InstanceSavesService.ts @@ -146,7 +146,7 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa */ async watch(path: string) { requireString(path) - const lock = this.semaphoreManager.getLock(LockKey.instance(path)) + const lock = this.mutex.of(LockKey.instance(path)) const stateManager = await this.app.registry.get(ServiceStateManager) const launchService = await this.app.registry.get(LaunchService) @@ -167,7 +167,7 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa launchService.on('minecraft-exit', onExit) const updateSave = defineAsyncOperation(async (filePath: string) => { - await lock.read(() => readInstanceSaveMetadata(filePath, baseName).then((save) => { + await lock.runExclusive(() => readInstanceSaveMetadata(filePath, baseName).then((save) => { state.instanceSaveUpdate(save) }).catch((e) => { this.warn(`Parse save in ${filePath} failed. Skip it.`) @@ -234,9 +234,9 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa }) .add(savesDir) - const revalidate = () => lock.read(async () => { + const revalidate = async () => { // TODO: getWatched and revalidate - }) + } const dispose = () => { launchService.off('minecraft-exit', onExit) diff --git a/xmcl-runtime/service/Service.ts b/xmcl-runtime/service/Service.ts index 02182cbc1..2a58031e0 100644 --- a/xmcl-runtime/service/Service.ts +++ b/xmcl-runtime/service/Service.ts @@ -1,4 +1,4 @@ -import { createPromiseSignal, getServiceSemaphoreKey, SharedState, PromiseSignal, ServiceKey, State } from '@xmcl/runtime-api' +import { createPromiseSignal, PromiseSignal, ServiceKey, SharedState, State } from '@xmcl/runtime-api' import { join } from 'path' import { EventEmitter } from 'stream' import { Logger } from '~/logger' @@ -11,7 +11,11 @@ export type ServiceConstructor = { export type MutexSerializer = (this: T, ...params: any[]) => string | string[] -export type ParamSerializer = (...params: any[]) => string | undefined +type ParamSerializer = (...params: any[]) => string | undefined + +function getServiceSingletonKey(key: ServiceKey, method: keyof T, params?: string) { + return params ? `${key}.${method as string}(${params})` : `${key}.${method as string}()` +} export const IGNORE_PARAMS: ParamSerializer = () => '' @@ -19,43 +23,6 @@ export const ALL_PARAMS: ParamSerializer = (...pararms) => JSON.stringify(p const InstanceSymbol = Symbol('InstanceSymbol') -export function ReadLock(key: (string | string[] | MutexSerializer)) { - return function (target: T, propertyKey: string, descriptor: PropertyDescriptor) { - const method = descriptor.value as Function - descriptor.value = function readLockDecorator(this: T, ...args: any[]) { - const keyOrKeys = typeof key === 'function' ? key.call(target, ...args) : key - const keys = keyOrKeys instanceof Array ? keyOrKeys : [keyOrKeys] - const promises: Promise<() => void>[] = [] - for (const k of keys) { - const key = k - const lock = this.semaphoreManager.getLock(key) - promises.push(lock.acquireRead()) - } - this.log(`Acquire read locks: ${keys.join(', ')}`) - const exec = () => { - try { - const result = method.apply(this, args) - if (result instanceof Promise) { - return result - } else { - return Promise.resolve(result) - } - } catch (e) { - return Promise.reject(e) - } - } - Object.defineProperty(exec, 'name', { value: `${method.name}$ReadLock$exec` }) - return Promise.all(promises).then((releases) => { - return exec().finally(() => { - this.log(`Release read locks: ${keys.join(', ')}`) - releases.forEach(f => f()) - }) - }) - } - Object.defineProperty(descriptor.value, 'name', { value: `${method.name}$ReadLock` }) - } -} - /** * A service method decorator to make sure this service will acquire mutex to run, ensuring the mutual exclusive. */ @@ -67,8 +34,8 @@ export function Lock(key: (string | string[] | MutexS const keys = keyOrKeys instanceof Array ? keyOrKeys : [keyOrKeys] const promises: Promise<() => void>[] = [] for (const key of keys) { - const lock = this.semaphoreManager.getLock(key) - promises.push(lock.acquireWrite()) + const lock = this.mutex.of(key) + promises.push(lock.acquire()) } this.log(`Acquire locks: ${keys.join(', ')}`) const exec = () => { @@ -121,19 +88,17 @@ export function Singleton(param: ParamSerializer = } const serviceKey = getServiceKey(Object.getPrototypeOf(this).constructor) Object.defineProperty(exec, 'name', { value: `${method.name}$Singleton$exec` }) - const targetKey = getServiceSemaphoreKey(serviceKey, propertyKey, param.call(this, ...args)) + const targetKey = getServiceSingletonKey(serviceKey, propertyKey, param.call(this, ...args)) const last = instances[targetKey] if (last) { return last } else { this.log(`Acquire singleton ${targetKey}`) - this.up(targetKey) const startTime = Date.now() instances[targetKey] = exec().finally(() => { const endTime = Date.now() this.log(`Release singleton ${targetKey}. Took ${endTime - startTime}ms.`) - this.down(targetKey) delete instances[targetKey] }) return instances[targetKey] @@ -170,7 +135,7 @@ export abstract class AbstractService extends EventEmitter { this.error = this.logger.error } - get semaphoreManager() { return this.app.semaphoreManager } + get mutex() { return this.app.mutex } emit(event: string, ...args: any[]): boolean { this.app.controller.broadcast('service-event', { service: getServiceKey(Object.getPrototypeOf(this).constructor), event, args }) @@ -224,14 +189,6 @@ export abstract class AbstractService extends EventEmitter { warn = (m: any, ...a: any[]) => { } - - protected up(key: string) { - this.semaphoreManager.acquire(key) - } - - protected down(key: string) { - this.semaphoreManager.release(key) - } } export abstract class StatefulService> extends AbstractService { diff --git a/xmcl-runtime/util/sqlHelper.ts b/xmcl-runtime/sql/sqlHelper.ts similarity index 100% rename from xmcl-runtime/util/sqlHelper.ts rename to xmcl-runtime/sql/sqlHelper.ts diff --git a/xmcl-runtime/util/mutex.ts b/xmcl-runtime/util/mutex.ts deleted file mode 100644 index bf4089332..000000000 --- a/xmcl-runtime/util/mutex.ts +++ /dev/null @@ -1,47 +0,0 @@ -export enum LockStatus { - Idle, - Reading, - Writing, -} - -export interface SemaphoreListener { - (delta: number, semaphore: number): void -} - -export type Ticket = () => void - -export class Queue { - private queue: Array<[Promise, () => void]> = [] - - async waitInline(): Promise { - let _resolve: () => void - const promise = new Promise((resolve) => { - _resolve = resolve - }) - // last guy in queue - const last: Promise | undefined = this.queue[this.queue.length - 1]?.[0] - // reserve your place in line - this.queue.push([promise, _resolve!]) - // wait last guy to finish - await last - // now my turn - return () => { - // call next to execute - this.queue.shift()?.[1]() - } - } - - /** - * Is queue waiting - */ - isWaiting() { - return this.queue.length > 0 - } - - /** - * Wait the queue is empty - */ - async waitUntilEmpty() { - await this.queue[this.queue.length - 1]?.[0] - } -} diff --git a/xmcl-runtime/util/trafficAgent.ts b/xmcl-runtime/util/trafficAgent.ts deleted file mode 100644 index 0ff30ef78..000000000 --- a/xmcl-runtime/util/trafficAgent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import throttle from 'lodash.throttle' -import debounce from 'lodash.debounce' - -/** - * Create a simple throttle function for the specific key, which is used to generate lock/semaphore key for the service call. - */ -export function createDynamicThrottle any>(f: T, keyExtractor: (...param: Parameters) => string, time: number): T { - const memos: Record)> = {} - const result: T = (((...params: any[]) => { - const key = keyExtractor(...params as any) - if (memos[key]) { - return memos[key](...params as any) as any - } - const func = throttle(f, time) - memos[key] = func - return (func as any)() as any - }) as any as T) - return result -}