Skip to content

Commit 60d57c7

Browse files
pepicrftPedro Piñera Buendíaclaude
authored
fix: handle concurrent mkdir race in NIO's createDirectory (#312)
Co-authored-by: Pedro Piñera Buendía <pepicrft@Pedros-MacBook-Pro.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 21afe52 commit 60d57c7

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

Sources/FileSystem/FileSystem.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,17 @@ public struct FileSystem: FileSysteming, Sendable {
550550
.contains(.createTargetParentDirectories)
551551
)
552552
} catch let error as _NIOFileSystem.FileSystemError {
553-
if error.code == .invalidArgument {
553+
if error.code == .fileAlreadyExists,
554+
options.contains(.createTargetParentDirectories)
555+
{
556+
// NIO's createDirectory has a race condition where concurrent calls
557+
// creating the same intermediate directory fail with EEXIST in its
558+
// rebuild loop. Retry — the intermediate now exists.
559+
try await _NIOFileSystem.FileSystem.shared.createDirectory(
560+
at: .init(at.pathString),
561+
withIntermediateDirectories: true
562+
)
563+
} else if error.code == .invalidArgument {
554564
throw FileSystemError.makeDirectoryAbsentParent(at)
555565
} else {
556566
throw error

Tests/FileSystemTests/FileSystemTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,45 @@ private struct TestError: Error, Equatable {}
150150
}
151151
}
152152

153+
func test_makeDirectory_concurrentCreationOfSharedIntermediateDirectories() async throws {
154+
try await subject.runInTemporaryDirectory(prefix: "FileSystem") { temporaryDirectory in
155+
// Given
156+
// Multiple paths sharing the same intermediate directories
157+
let moduleNames = (0 ..< 20).map { "Module\($0)" }
158+
159+
// When — create directories concurrently, all sharing the same intermediates
160+
var errors: [Error] = []
161+
await withTaskGroup(of: Error?.self) { group in
162+
for moduleName in moduleNames {
163+
group.addTask {
164+
do {
165+
let path = temporaryDirectory.appending(
166+
components: "parent", "shared", moduleName
167+
)
168+
try await self.subject.makeDirectory(at: path)
169+
return nil
170+
} catch {
171+
return error
172+
}
173+
}
174+
}
175+
for await error in group {
176+
if let error { errors.append(error) }
177+
}
178+
}
179+
180+
// Then — all directories should have been created without errors
181+
XCTAssertTrue(errors.isEmpty, "Concurrent makeDirectory failed: \(errors)")
182+
for moduleName in moduleNames {
183+
let path = temporaryDirectory.appending(
184+
components: "parent", "shared", moduleName
185+
)
186+
let exists = try await self.subject.exists(path, isDirectory: true)
187+
XCTAssertTrue(exists, "Directory should exist at \(path)")
188+
}
189+
}
190+
}
191+
153192
func test_writeTextFile_and_readTextFile_returnsTheContent() async throws {
154193
try await subject.runInTemporaryDirectory(prefix: "FileSystem") { temporaryDirectory in
155194
// Given

0 commit comments

Comments
 (0)