gongxiangp2/lib/__tests__/material-upload-cross-process-lock.test.ts
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

172 lines
5.3 KiB
TypeScript

/** @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()
})
})