fix(vfs): prevent race condition in bulkWrite by ordering operations
- Process mkdir operations sequentially first (sorted by path depth) - Then process write/delete/update operations in parallel batches - Prevents duplicate directory entities when mkdir and write for related paths are in the same batch - Add comprehensive tests for bulkWrite race condition scenarios - Update API documentation for accuracy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ec6fe0c039
commit
c8eb813a15
7 changed files with 906 additions and 340 deletions
|
|
@ -2338,8 +2338,48 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort bulk operations to prevent race conditions
|
||||
*
|
||||
* Strategy:
|
||||
* 1. mkdir operations first, sorted by path depth (shallowest first)
|
||||
* 2. Other operations (write, delete, update) after, in original order
|
||||
*
|
||||
* This ensures parent directories exist before files are written,
|
||||
* preventing duplicate entity creation from concurrent mkdir calls.
|
||||
*/
|
||||
private sortBulkOperations(operations: Array<{
|
||||
type: 'write' | 'delete' | 'mkdir' | 'update'
|
||||
path: string
|
||||
data?: Buffer | string
|
||||
options?: any
|
||||
}>): Array<typeof operations[number]> {
|
||||
const mkdirOps: typeof operations = []
|
||||
const otherOps: typeof operations = []
|
||||
|
||||
for (const op of operations) {
|
||||
if (op.type === 'mkdir') {
|
||||
mkdirOps.push(op)
|
||||
} else {
|
||||
otherOps.push(op)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort mkdir by path depth (shallowest first)
|
||||
mkdirOps.sort((a, b) => {
|
||||
const depthA = (a.path.match(/\//g) || []).length
|
||||
const depthB = (b.path.match(/\//g) || []).length
|
||||
return depthA !== depthB ? depthA - depthB : a.path.localeCompare(b.path)
|
||||
})
|
||||
|
||||
return [...mkdirOps, ...otherOps]
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk write operations for performance
|
||||
*
|
||||
* v6.5.0: Prevents race condition by processing mkdir operations
|
||||
* sequentially before parallel batch processing of other operations.
|
||||
*/
|
||||
async bulkWrite(operations: Array<{
|
||||
type: 'write' | 'delete' | 'mkdir' | 'update'
|
||||
|
|
@ -2357,10 +2397,33 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
failed: [] as Array<{ operation: any, error: string }>
|
||||
}
|
||||
|
||||
// Process operations in batches for better performance
|
||||
// Sort operations: mkdirs first (by depth), then others
|
||||
const sortedOps = this.sortBulkOperations(operations)
|
||||
|
||||
// Separate mkdir operations for sequential processing
|
||||
const mkdirOps = sortedOps.filter(op => op.type === 'mkdir')
|
||||
const otherOps = sortedOps.filter(op => op.type !== 'mkdir')
|
||||
|
||||
// Phase 1: Process mkdir operations SEQUENTIALLY
|
||||
// This prevents the race condition where parallel mkdir calls
|
||||
// create duplicate directory entities due to mutex timing window
|
||||
for (const op of mkdirOps) {
|
||||
try {
|
||||
await this.mkdir(op.path, op.options)
|
||||
result.successful++
|
||||
} catch (error: any) {
|
||||
result.failed.push({
|
||||
operation: op,
|
||||
error: error.message || 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Process other operations in parallel batches
|
||||
// These can safely run in parallel since parent directories now exist
|
||||
const batchSize = 10
|
||||
for (let i = 0; i < operations.length; i += batchSize) {
|
||||
const batch = operations.slice(i, i + batchSize)
|
||||
for (let i = 0; i < otherOps.length; i += batchSize) {
|
||||
const batch = otherOps.slice(i, i + batchSize)
|
||||
|
||||
// Process batch in parallel
|
||||
const promises = batch.map(async (op) => {
|
||||
|
|
@ -2372,9 +2435,6 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
case 'delete':
|
||||
await this.unlink(op.path)
|
||||
break
|
||||
case 'mkdir':
|
||||
await this.mkdir(op.path, op.options)
|
||||
break
|
||||
case 'update': {
|
||||
// Update only metadata without changing content
|
||||
const entityId = await this.pathResolver.resolve(op.path)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue