Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parallel removal of items #3008

Merged
merged 25 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
90c02f3
Add parallel removal of items
mimischi Nov 29, 2024
ac5fe9f
Format files
mimischi Nov 30, 2024
4db67c0
Simplify structure by relying on dfs
mimischi Nov 30, 2024
09d11a9
Add documentation to new code and rename functions
mimischi Nov 30, 2024
861c188
Attempt not to break API
mimischi Nov 30, 2024
1d60824
Fix documentation check, and some indents
mimischi Dec 1, 2024
a97776b
Refactor for a cleaner structure
mimischi Dec 2, 2024
1a2f27e
Add `TokenBucket` adaptation from SwiftPM
mimischi Dec 3, 2024
2a1d510
Add `maxDescriptors` to `RemovalStrategy.swift`
mimischi Dec 3, 2024
5d7a178
Update tests
mimischi Dec 3, 2024
7a91ae6
Update `ParallelRemoval` to use `TokenBucket`
mimischi Dec 3, 2024
1b010e5
Add `FileSystemProtocol` method to not break API
mimischi Dec 3, 2024
b336a44
Actually stop breaking the API
mimischi Dec 4, 2024
0a83116
Format file
mimischi Dec 4, 2024
4390a8e
Refactor IO strategies to share commonalities
mimischi Dec 4, 2024
f00e17a
Fix typo in comment on function signature
mimischi Dec 4, 2024
1e4d5b5
Address reviews for `TokenBucket.swift`
mimischi Dec 6, 2024
dbd3ded
Address `ParallelRemoval.swift` review comments
mimischi Dec 6, 2024
5b536ca
Address `IOStrategy.swift` reviews
mimischi Dec 6, 2024
3d990b1
Remove unnecessary implementations
mimischi Dec 6, 2024
9146b91
Remove new arguments to tests function calls
mimischi Dec 6, 2024
338ff35
Re-use directory discovery logic
mimischi Dec 9, 2024
7ed6d7d
Makr `TokenBucket` as `@unchecked Sendable`
mimischi Dec 9, 2024
e9df08e
Revert "Re-use directory discovery logic"
mimischi Dec 10, 2024
e98388f
Merge branch 'main' into parallel-removal
glbrntt Dec 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,12 @@ This product contains a derivation of the mocking infrastructure from Swift Syst
* https://www.apache.org/licenses/LICENSE-2.0
* HOMEPAGE:
* https://github.com/apple/swift-system

---

This product contains a derivation of "TokenBucket.swift" from Swift Package Manager.

* LICENSE (Apache License 2.0):
* https://www.apache.org/licenses/LICENSE-2.0
* HOMEPAGE:
* https://github.com/swiftlang/swift-package-manager
121 changes: 0 additions & 121 deletions Sources/NIOFileSystem/CopyStrategy.swift

This file was deleted.

80 changes: 53 additions & 27 deletions Sources/NIOFileSystem/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ public struct FileSystem: Sendable, FileSystemProtocol {
}
}

/// See ``FileSystemProtocol/removeItem(at:strategy:recursively:)``
///
/// Deletes the file or directory (and its contents) at `path`.
///
/// Only regular files, symbolic links and directories may be removed. If the file at `path` is
Expand All @@ -387,11 +389,14 @@ public struct FileSystem: Sendable, FileSystemProtocol {
///
/// - Parameters:
/// - path: The path to delete.
/// - removalStrategy: Whether to delete files sequentially (one-by-one), or perform a
/// concurrent scan of the tree at `path` and delete files when they are found.
/// - removeItemRecursively: Whether or not to remove items recursively.
/// - Returns: The number of deleted items which may be zero if `path` did not exist.
@discardableResult
public func removeItem(
at path: FilePath,
strategy removalStrategy: RemovalStrategy,
recursively removeItemRecursively: Bool
) async throws -> Int {
// Try to remove the item: we might just get lucky.
Expand Down Expand Up @@ -421,39 +426,60 @@ public struct FileSystem: Sendable, FileSystemProtocol {
)
}

var (subdirectories, filesRemoved) = try await self.withDirectoryHandle(
atPath: path
) { directory in
var subdirectories = [FilePath]()
var filesRemoved = 0
switch removalStrategy.wrapped {
case .sequential:
return try await self.removeItemSequentially(at: path)
case let .parallel(maxDescriptors):
return try await self.removeConcurrently(at: path, maxDescriptors)
}

for try await batch in directory.listContents().batched() {
for entry in batch {
switch entry.type {
case .directory:
subdirectories.append(entry.path)
case let .failure(errno):
throw FileSystemError.remove(errno: errno, path: path, location: .here())
}
}

default:
filesRemoved += try await self.removeOneItem(at: entry.path)
}
@discardableResult
private func removeItemSequentially(
at path: FilePath
) async throws -> Int {
var (subdirectories, filesRemoved) = try await self.withDirectoryHandle(
atPath: path
) { directory in
var subdirectories = [FilePath]()
var filesRemoved = 0

for try await batch in directory.listContents().batched() {
for entry in batch {
switch entry.type {
case .directory:
subdirectories.append(entry.path)

default:
filesRemoved += try await self.removeOneItem(at: entry.path)
}
}

return (subdirectories, filesRemoved)
}

for subdirectory in subdirectories {
filesRemoved += try await self.removeItem(at: subdirectory)
}
return (subdirectories, filesRemoved)
}

// The directory should be empty now. Remove ourself.
filesRemoved += try await self.removeOneItem(at: path)
for subdirectory in subdirectories {
filesRemoved += try await self.removeItemSequentially(at: subdirectory)
}

return filesRemoved
// The directory should be empty now. Remove ourself.
filesRemoved += try await self.removeOneItem(at: path)

case let .failure(errno):
throw FileSystemError.remove(errno: errno, path: path, location: .here())
}
return filesRemoved

}

private func removeConcurrently(
at path: FilePath,
_ maxDescriptors: Int
) async throws -> Int {
let bucket: TokenBucket = .init(tokens: maxDescriptors)
return try await self.discoverAndRemoveItemsInTree(at: path, bucket)
}

/// Moves the named file or directory to a new location.
Expand Down Expand Up @@ -490,7 +516,7 @@ public struct FileSystem: Sendable, FileSystemProtocol {
case .differentLogicalDevices:
// Fall back to copy and remove.
try await self.copyItem(at: sourcePath, to: destinationPath)
try await self.removeItem(at: sourcePath)
try await self.removeItem(at: sourcePath, strategy: .platformDefault)
}
}

Expand Down Expand Up @@ -518,9 +544,9 @@ public struct FileSystem: Sendable, FileSystemProtocol {
withItemAt existingPath: FilePath
) async throws {
do {
try await self.removeItem(at: destinationPath)
try await self.removeItem(at: destinationPath, strategy: .platformDefault)
try await self.moveItem(at: existingPath, to: destinationPath)
try await self.removeItem(at: existingPath)
try await self.removeItem(at: existingPath, strategy: .platformDefault)
} catch let error as FileSystemError {
throw FileSystemError(
message: "Can't replace '\(destinationPath)' with '\(existingPath)'.",
Expand Down
76 changes: 66 additions & 10 deletions Sources/NIOFileSystem/FileSystemProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,19 +249,23 @@ public protocol FileSystemProtocol: Sendable {
/// at the given path then this function returns zero.
///
/// If the item at the `path` is a directory and `removeItemRecursively` is `true` then the
/// contents of all of its subdirectories will be removed recursively before the directory
/// at `path`. Symbolic links are removed (but their targets are not deleted).
/// contents of all of its subdirectories will be removed recursively before the directory at
/// `path`. Symbolic links are removed (but their targets are not deleted).
///
/// - Parameters:
/// - path: The path to delete.
/// - removeItemRecursively: If the item being removed is a directory, remove it by
/// recursively removing its children. Setting this to `true` is synonymous with
/// calling `rm -r`, setting this false is synonymous to calling `rmdir`. Ignored if
/// - removalStrategy: Whether to delete files sequentially (one-by-one), or perform a
/// concurrent scan of the tree at `path` and delete files when they are found. Ignored if
/// the item being removed isn't a directory.
/// - removeItemRecursively: If the item being removed is a directory, remove it by
/// recursively removing its children. Setting this to `true` is synonymous with calling
/// `rm -r`, setting this false is synonymous to calling `rmdir`. Ignored if the item
/// being removed isn't a directory.
/// - Returns: The number of deleted items which may be zero if `path` did not exist.
@discardableResult
func removeItem(
at path: FilePath,
strategy removalStrategy: RemovalStrategy,
recursively removeItemRecursively: Bool
) async throws -> Int

Expand Down Expand Up @@ -588,9 +592,12 @@ extension FileSystemProtocol {
/// The item to be removed must be a regular file, symbolic link or directory. If no file exists
/// at the given path then this function returns zero.
///
/// If the item at the `path` is a directory then the contents of all of its subdirectories
/// will be removed recursively before the directory at `path`. Symbolic links are removed (but
/// their targets are not deleted).
/// If the item at the `path` is a directory then the contents of all of its subdirectories will
/// be removed recursively before the directory at `path`. Symbolic links are removed (but their
/// targets are not deleted).
///
/// The strategy for deletion will be determined automatically depending on the discovered
/// platform.
///
/// - Parameters:
/// - path: The path to delete.
Expand All @@ -599,7 +606,56 @@ extension FileSystemProtocol {
public func removeItem(
at path: FilePath
) async throws -> Int {
try await self.removeItem(at: path, recursively: true)
try await self.removeItem(at: path, strategy: .platformDefault, recursively: true)
}

/// Deletes the file or directory (and its contents) at `path`.
///
/// The item to be removed must be a regular file, symbolic link or directory. If no file exists
/// at the given path then this function returns zero.
///
/// If the item at the `path` is a directory then the contents of all of its subdirectories will
/// be removed recursively before the directory at `path`. Symbolic links are removed (but their
/// targets are not deleted).
///
/// The strategy for deletion will be determined automatically depending on the discovered
/// platform.
///
/// - Parameters:
/// - path: The path to delete.
/// - removeItemRecursively: If the item being removed is a directory, remove it by
/// recursively removing its children. Setting this to `true` is synonymous with calling
/// `rm -r`, setting this false is synonymous to calling `rmdir`. Ignored if the item
/// being removed isn't a directory.
/// - Returns: The number of deleted items which may be zero if `path` did not exist.
@discardableResult
public func removeItem(
at path: FilePath,
recursively removeItemRecursively: Bool
) async throws -> Int {
try await self.removeItem(at: path, strategy: .platformDefault, recursively: removeItemRecursively)
}

/// Deletes the file or directory (and its contents) at `path`.
///
/// The item to be removed must be a regular file, symbolic link or directory. If no file exists
/// at the given path then this function returns zero.
///
/// If the item at the `path` is a directory then the contents of all of its subdirectories will
/// be removed recursively before the directory at `path`. Symbolic links are removed (but their
/// targets are not deleted).
///
/// - Parameters:
/// - path: The path to delete.
/// - removalStrategy: Whether to delete files sequentially (one-by-one), or perform a
/// concurrent scan of the tree at `path` and delete files when they are found.
/// - Returns: The number of deleted items which may be zero if `path` did not exist.
@discardableResult
public func removeItem(
at path: FilePath,
strategy removalStrategy: RemovalStrategy
) async throws -> Int {
try await self.removeItem(at: path, strategy: removalStrategy, recursively: true)
mimischi marked this conversation as resolved.
Show resolved Hide resolved
}

/// Create a directory at the given path.
Expand Down Expand Up @@ -659,7 +715,7 @@ extension FileSystemProtocol {
try await execute(handle, directory)
}
} tearDown: { _ in
try await self.removeItem(at: directory, recursively: true)
try await self.removeItem(at: directory, strategy: .platformDefault, recursively: true)
}
}
}
Loading
Loading