Skip to content

Commit

Permalink
Refactor IO strategies to share commonalities
Browse files Browse the repository at this point in the history
  • Loading branch information
mimischi committed Dec 4, 2024
1 parent 0a83116 commit 4390a8e
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,11 @@
//
//===----------------------------------------------------------------------===//

/// How to perform copies. Currently only relevant to directory level copies when using
/// ``FileSystemProtocol/copyItem(at:to:strategy:shouldProceedAfterError:shouldCopyItem:)`` or other
/// overloads that use the default behaviour.
public struct CopyStrategy: Hashable, Sendable {
// Avoid exposing to prevent breaking changes
/// How many file descriptors to open when performing I/O operations.
private struct IOStrategy: Hashable, Sendable {
internal enum Wrapped: Hashable, Sendable {
// platformDefault is reified into one of the concrete options below:

case sequential
// Constraints on this value are enforced only on creation of `CopyStrategy`. The early
// error check is desirable over validating on downstream use.
case parallel(_ maxDescriptors: Int)
}

Expand All @@ -41,14 +35,14 @@ public struct CopyStrategy: Hashable, Sendable {
//
// That said, empirical testing for this has not been performed, suggestions welcome.
//
// Note: for now we model the directory scan as needing two handles because, during the creation
// of the destination directory we hold the handle for a while copying attributes
// a much more complex internal state machine could allow doing two of these if desired
// This may not result in a faster copy though so things are left simple
// Note: The directory scan is modelled after a copy strategy needing two handles: during the
// creation of the destination directory we hold the handle while copying attributes. A much
// more complex internal state machine could allow doing two of these if desired. This may not
// result in a faster copy though so things are left simple.
internal static func determinePlatformDefault() -> Wrapped {
#if os(macOS) || os(Linux) || os(Windows)
// 4 concurrent file copies/directory scans. Avoiding storage system contention is of utmost
// importance.
// Eight file descriptors allow for four concurrent file copies/directory scans. Avoiding
// storage system contention is of utmost importance.
//
// Testing was performed on an SSD, while copying objects (a dense directory of small files
// and subdirectories of similar shape) to the same volume, totalling 12GB. Results showed
Expand All @@ -68,7 +62,30 @@ public struct CopyStrategy: Hashable, Sendable {
}
}

extension CopyStrategy {
/// How to perform copies. Currently only relevant to directory level copies when using
/// ``FileSystemProtocol/copyItem(at:to:strategy:shouldProceedAfterError:shouldCopyItem:)`` or other
/// overloads that use the default behaviour.
public struct CopyStrategy: Hashable, Sendable {
// Avoid exposing to prevent breaking changes
internal enum Wrapped: Hashable, Sendable {
// platformDefault is reified into one of the concrete options below:

case sequential
// Constraints on this value are enforced only on creation of `CopyStrategy`. The early
// error check is desirable over validating on downstream use.
case parallel(_ maxDescriptors: Int)
}

internal let wrapped: Wrapped
private init(_ strategy: IOStrategy.Wrapped) {
switch strategy {
case .sequential:
self.wrapped = Wrapped.sequential
case let .parallel(maxDescriptors):
self.wrapped = Wrapped.parallel(maxDescriptors)
}
}

// A copy fundamentally can't work without two descriptors unless you copy everything into
// memory which is infeasible/inefficient for large copies.
private static let minDescriptorsAllowed = 2
Expand All @@ -79,7 +96,7 @@ extension CopyStrategy {
/// Current assumptions (which are subject to change):
/// - Only one copy operation would be performed at once
/// - The copy operation is not intended to be the primary activity on the device
public static let platformDefault: Self = Self(Self.determinePlatformDefault())
public static let platformDefault: Self = Self(IOStrategy.determinePlatformDefault())

/// The copy is done asynchronously, but only one operation will occur at a time. This is the
/// only way to guarantee only one callback to the `shouldCopyItem` will happen at a time.
Expand Down Expand Up @@ -121,3 +138,74 @@ extension CopyStrategy: CustomStringConvertible {
}
}
}

/// How to perform file deletions. Currently only relevant to directory level deletions when using
/// ``FileSystemProtocol/removeItem(path:strategy:recursively:)`` or other overloads that use the
/// default behaviour.
public struct RemovalStrategy: Hashable, Sendable {
// Avoid exposing to prevent breaking changes
internal enum Wrapped: Hashable, Sendable {
// platformDefault is reified into one of the concrete options below:

case sequential
// Constraints on this value are enforced only on creation of `RemovalStrategy`. The early
// error check is desirable over validating on downstream use.
case parallel(_ maxDescriptors: Int)
}

internal let wrapped: Wrapped
private init(_ strategy: IOStrategy.Wrapped) {
switch strategy {
case .sequential:
self.wrapped = Wrapped.sequential
case let .parallel(maxDescriptors):
self.wrapped = Wrapped.parallel(maxDescriptors)
}
}

// A deletion requires no file descriptors. We only consume a file descriptor while scanning the
// contents of a directory, so the minimum is 1.
private static let minRequiredDescriptors = 1

/// Operate in whatever manner is deemed a reasonable default for the platform. This will limit
/// the maximum file descriptors usage based on reasonable defaults.
///
/// Current assumptions (which are subject to change):
/// - Only one delete operation would be performed at once
/// - The delete operation is not intended to be the primary activity on the device
public static let platformDefault: Self = Self(IOStrategy.determinePlatformDefault())

/// Traversal of directories and removal of files will be done sequentially without any
/// parallelization.
public static let sequential: Self = Self(.sequential)

/// Allow for one or more directory scans to run at the same time. Removal of files will happen
/// on asynchronous tasks in parallel.
///
/// Setting `maxDescriptors` to 1, will limit the speed of directory discovery. Deletion of
/// files within that directory will run in parallel, but discovery of subdirectories will be
/// limited to one at a time.
public static func parallel(maxDescriptors: Int) throws -> Self {
guard maxDescriptors >= Self.minRequiredDescriptors else {
throw FileSystemError(
code: .invalidArgument,
message:
"Can't do a remove operation without at least one file descriptor '\(maxDescriptors)' is illegal",
cause: nil,
location: .here()
)
}
return .init(.parallel(maxDescriptors))
}
}

extension RemovalStrategy: CustomStringConvertible {
public var description: String {
switch self.wrapped {
case .sequential:
return "sequential"
case let .parallel(maxDescriptors):
return "parallel with max \(maxDescriptors) descriptors"
}
}
}
82 changes: 0 additions & 82 deletions Sources/NIOFileSystem/RemovalStrategy.swift

This file was deleted.

0 comments on commit 4390a8e

Please sign in to comment.