/** @jest-environment node */ import { acquireMaterialUploadArchiveLock, acquireMaterialUploadStorageLock, acquireTaskMaterialCompletionLock, acquireTaskMaterialUploadSessionLocks, InProcessMaterialUploadReadWriteLock, resolveScopedLockFileName, } from '@/lib/material-upload-cross-process-lock' describe('material upload shared/exclusive storage lock', () => { test('多个分片可共享读锁,但合并必须等所有分片释放', async () => { const lock = new InProcessMaterialUploadReadWriteLock() const releaseReaderOne = await lock.acquire('shared', 1_000) const releaseReaderTwo = await lock.acquire('shared', 1_000) let writerAcquired = false const writerPromise = lock.acquire('exclusive', 1_000).then((release) => { writerAcquired = true return release }) await Promise.resolve() expect(writerAcquired).toBe(false) expect(lock.snapshot()).toEqual({ activeReaders: 2, writerActive: false, waiting: 1, }) await releaseReaderOne() await Promise.resolve() expect(writerAcquired).toBe(false) await releaseReaderTwo() const releaseWriter = await writerPromise expect(writerAcquired).toBe(true) expect(lock.snapshot().writerActive).toBe(true) await releaseWriter() expect(lock.snapshot()).toEqual({ activeReaders: 0, writerActive: false, waiting: 0, }) }) test('写者排队后新分片不能插队,避免大文件合并永久饥饿', async () => { const lock = new InProcessMaterialUploadReadWriteLock() const releaseInitialReader = await lock.acquire('shared', 1_000) const order: string[] = [] const writerPromise = lock.acquire('exclusive', 1_000).then((release) => { order.push('writer') return release }) const lateReaderPromise = lock.acquire('shared', 1_000).then((release) => { order.push('reader') return release }) await releaseInitialReader() const releaseWriter = await writerPromise expect(order).toEqual(['writer']) await releaseWriter() const releaseLateReader = await lateReaderPromise expect(order).toEqual(['writer', 'reader']) await releaseLateReader() }) test('等待超时会返回可重试的结构化错误且不会污染队列', async () => { const lock = new InProcessMaterialUploadReadWriteLock() const releaseWriter = await lock.acquire('exclusive', 1_000) await expect(lock.acquire('shared', 10)).rejects.toMatchObject({ status: 503, code: 'UPLOAD_STORAGE_BUSY', }) expect(lock.snapshot().waiting).toBe(0) await releaseWriter() const releaseReader = await lock.acquire('shared', 1_000) await releaseReader() }) }) describe('task material completion lock', () => { test('同一素材互斥、不同素材可并行,释放后可重试', async () => { const releaseMaterialOne = await acquireTaskMaterialCompletionLock('material-1', { timeoutMs: 1_000, }) const releaseMaterialTwo = await acquireTaskMaterialCompletionLock('material-2', { timeoutMs: 1_000, }) await expect( acquireTaskMaterialCompletionLock('material-1', { timeoutMs: 10 }), ).rejects.toMatchObject({ status: 409, code: 'TASK_MATERIAL_COMPLETION_BUSY', }) await releaseMaterialTwo() await releaseMaterialOne() const releaseRetry = await acquireTaskMaterialCompletionLock('material-1', { timeoutMs: 1_000, }) releaseRetry.assertHeld() await releaseRetry() }) test('非法素材编号在创建锁文件前拒绝', async () => { await expect(acquireTaskMaterialCompletionLock('../material')).rejects.toMatchObject({ status: 400, code: 'TASK_MATERIAL_ID_INVALID', }) }) test('生产锁名使用 64-bit 哈希,不再只有 256 个桶', () => { const names = new Set( Array.from({ length: 2_000 }, (_, index) => ( resolveScopedLockFileName('task-material', `material-${index}`) )), ) expect(names.size).toBe(2_000) expect(Array.from(names)[0]).toMatch(/^task-material-[a-f0-9]{16}\.lock$/) }) test('重叠 uploadId 的不同素材不能并发消费源文件', async () => { const releaseFirst = await acquireTaskMaterialUploadSessionLocks( ['upload-b', 'upload-a'], { timeoutMs: 1_000 }, ) await expect( acquireTaskMaterialUploadSessionLocks(['upload-a'], { timeoutMs: 10 }), ).rejects.toMatchObject({ status: 409, code: 'TASK_MATERIAL_UPLOAD_SESSION_BUSY', }) await releaseFirst() const releaseRetry = await acquireTaskMaterialUploadSessionLocks( ['upload-a'], { timeoutMs: 1_000 }, ) releaseRetry.assertHeld() await releaseRetry() }) }) describe('material upload archive lock', () => { test('ZIP 之间串行,但不阻塞分片的 shared 发布锁', async () => { const releaseArchive = await acquireMaterialUploadArchiveLock({ timeoutMs: 1_000 }) await expect( acquireMaterialUploadArchiveLock({ timeoutMs: 10 }), ).rejects.toMatchObject({ status: 503, code: 'UPLOAD_STORAGE_BUSY', }) const releaseChunkPublish = await acquireMaterialUploadStorageLock('shared', { timeoutMs: 1_000, }) releaseChunkPublish.assertHeld() await releaseChunkPublish() await releaseArchive() }) })