From b00617901a07ed15e648bbdd57aa58f93bafbca4 Mon Sep 17 00:00:00 2001 From: kushagrasikka Date: Tue, 17 Jun 2025 07:26:42 -0400 Subject: [PATCH] docs: improve documentation and error messages for ContainerizationExtras - Enhanced IPv4Address documentation with comprehensive examples and parameter descriptions - Improved error messages in NetworkAddressError with actionable guidance - Added detailed documentation for CIDRAddress including usage examples and method descriptions - Expanded AsyncLock documentation with use cases and threading safety information - Enhanced Timeout documentation with implementation details and performance notes - Improved error message clarity in Timeout.swift by replacing generic fatalError These changes make the API more accessible to new contributors and provide better developer experience through clear error messages and comprehensive documentation. --- .../ContainerizationExtras/AsyncLock.swift | 84 +++++++++- .../ContainerizationExtras/CIDRAddress.swift | 158 ++++++++++++++++-- .../ContainerizationExtras/IPAddress.swift | 110 +++++++++++- .../NetworkAddress.swift | 24 ++- Sources/ContainerizationExtras/Timeout.swift | 82 ++++++++- 5 files changed, 419 insertions(+), 39 deletions(-) diff --git a/Sources/ContainerizationExtras/AsyncLock.swift b/Sources/ContainerizationExtras/AsyncLock.swift index 702a3f23..2bdf0266 100644 --- a/Sources/ContainerizationExtras/AsyncLock.swift +++ b/Sources/ContainerizationExtras/AsyncLock.swift @@ -16,21 +16,95 @@ import Foundation -/// `AsyncLock` provides a familiar locking API, with the main benefit being that it -/// is safe to call async methods while holding the lock. This is primarily used in spots -/// where an actor makes sense, but we may need to ensure we don't fall victim to actor -/// reentrancy issues. +/// An async-safe mutual exclusion lock for coordinating access to shared resources. +/// +/// `AsyncLock` provides a familiar locking API with the key benefit that it's safe to call +/// async methods while holding the lock. This addresses scenarios where traditional actors +/// might suffer from reentrancy issues or where you need explicit sequential access control. +/// +/// ## Use Cases +/// - Protecting shared mutable state that requires async operations +/// - Coordinating access to resources that don't support concurrent operations +/// - Avoiding actor reentrancy issues in complex async workflows +/// - Ensuring sequential execution of async operations +/// +/// ## Example usage: +/// ```swift +/// actor ResourceManager { +/// private let lock = AsyncLock() +/// private var resources: [String] = [] +/// +/// func addResource(_ name: String) async { +/// await lock.withLock { context in +/// // Async operations are safe within the lock +/// let processedName = await processResourceName(name) +/// resources.append(processedName) +/// await notifyObservers(about: processedName) +/// } +/// } +/// +/// func getResourceCount() async -> Int { +/// await lock.withLock { context in +/// return resources.count +/// } +/// } +/// } +/// ``` +/// +/// ## Threading Safety +/// This lock is designed for use within actors or other async contexts and provides +/// mutual exclusion without blocking threads. Operations are queued and resumed +/// sequentially as the lock becomes available. public actor AsyncLock { private var busy = false private var queue: ArraySlice> = [] + /// A context object provided to closures executed within the lock. + /// + /// The context serves as proof that the code is executing within the lock's + /// critical section. While currently empty, it may be extended in the future + /// to provide lock-specific functionality. public struct Context: Sendable { fileprivate init() {} } + /// Creates a new AsyncLock instance. + /// + /// The lock starts in an unlocked state and is ready for immediate use. public init() {} - /// withLock provides a scoped locking API to run a function while holding the lock. + /// Executes a closure while holding the lock, ensuring exclusive access. + /// + /// - Parameter body: An async closure to execute while holding the lock. + /// The closure receives a `Context` parameter as proof of lock ownership. + /// - Returns: The value returned by the closure + /// - Throws: Any error thrown by the closure + /// + /// This method provides scoped locking - the lock is automatically acquired before + /// the closure executes and released when the closure completes (either normally + /// or by throwing an error). + /// + /// If the lock is already held, the current operation will suspend until the lock + /// becomes available. Operations are queued and executed in FIFO order. + /// + /// ## Example: + /// ```swift + /// let lock = AsyncLock() + /// var counter = 0 + /// + /// // Safely increment counter with async work + /// let result = await lock.withLock { context in + /// let oldValue = counter + /// await Task.sleep(nanoseconds: 1_000_000) // Simulate async work + /// counter = oldValue + 1 + /// return counter + /// } + /// ``` + /// + /// ## Performance Notes + /// - The lock uses actor isolation, so there's no thread blocking + /// - Suspended operations consume minimal memory + /// - Lock contention is resolved in first-in-first-out order public func withLock(_ body: @Sendable @escaping (Context) async throws -> T) async rethrows -> T { while self.busy { await withCheckedContinuation { cc in diff --git a/Sources/ContainerizationExtras/CIDRAddress.swift b/Sources/ContainerizationExtras/CIDRAddress.swift index 55c9a6e7..029b3b8f 100644 --- a/Sources/ContainerizationExtras/CIDRAddress.swift +++ b/Sources/ContainerizationExtras/CIDRAddress.swift @@ -14,22 +14,56 @@ // limitations under the License. //===----------------------------------------------------------------------===// -/// Describes an IPv4 CIDR address block. +/// Represents an IPv4 CIDR (Classless Inter-Domain Routing) address block. +/// +/// A CIDR block defines a range of IP addresses using a base address and a prefix length. +/// This struct provides functionality for subnet calculations, address containment checks, +/// and network overlap detection. +/// +/// ## Example usage: +/// ```swift +/// // Create from CIDR notation +/// let cidr = try CIDRAddress("192.168.1.0/24") +/// print(cidr.lower) // 192.168.1.0 +/// print(cidr.upper) // 192.168.1.255 +/// +/// // Check if an address is in the block +/// let testAddr = try IPv4Address("192.168.1.100") +/// print(cidr.contains(ipv4: testAddr)) // true +/// +/// // Get address index within the block +/// if let index = cidr.getIndex(testAddr) { +/// print("Address index: \(index)") // 100 +/// } +/// ``` public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable { - /// The base IPv4 address of the CIDR block. + /// The base (network) IPv4 address of the CIDR block. + /// This is the lowest address in the range with all host bits set to 0. public let lower: IPv4Address - /// The last IPv4 address of the CIDR block + /// The broadcast IPv4 address of the CIDR block. + /// This is the highest address in the range with all host bits set to 1. public let upper: IPv4Address - /// The IPv4 address component of the CIDR block. + /// The IPv4 address component used to create this CIDR block. + /// This may be any address within the block, not necessarily the network address. public let address: IPv4Address - /// The address prefix length for the CIDR block, which determines its size. + /// The prefix length (subnet mask) for the CIDR block, which determines its size. + /// Valid range is 0-32, where 32 represents a single host and 0 represents all IPv4 addresses. public let prefixLength: PrefixLength - /// Create an CIDR address block from its text representation. + /// Create a CIDR address block from its text representation. + /// + /// - Parameter cidr: A string in CIDR notation (e.g., "192.168.1.0/24") + /// - Throws: `NetworkAddressError.invalidCIDR` if the format is invalid + /// + /// ## Example: + /// ```swift + /// let cidr = try CIDRAddress("10.0.0.0/8") // 10.0.0.0 - 10.255.255.255 + /// let host = try CIDRAddress("192.168.1.1/32") // Single host + /// ``` public init(_ cidr: String) throws { let split = cidr.components(separatedBy: "/") guard split.count == 2 else { @@ -48,7 +82,20 @@ public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable { upper = IPv4Address(fromValue: lower.value + prefixLength.suffixMask32) } - /// Create a CIDR address from a member IP and a prefix length. + /// Create a CIDR address block from an IP address and prefix length. + /// + /// - Parameters: + /// - address: Any IPv4 address within the desired network + /// - prefixLength: The subnet mask length (0-32) + /// - Throws: `NetworkAddressError.invalidCIDR` if the prefix length is invalid + /// + /// ## Example: + /// ```swift + /// let addr = try IPv4Address("192.168.1.150") + /// let cidr = try CIDRAddress(addr, prefixLength: 24) + /// print(cidr.description) // "192.168.1.150/24" + /// print(cidr.lower) // "192.168.1.0" + /// ``` public init(_ address: IPv4Address, prefixLength: PrefixLength) throws { guard prefixLength >= 0 && prefixLength <= 32 else { throw NetworkAddressError.invalidCIDR(cidr: "\(address)/\(prefixLength)") @@ -60,7 +107,23 @@ public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable { upper = IPv4Address(fromValue: lower.value + prefixLength.suffixMask32) } - /// Create the smallest CIDR block that includes the lower and upper bounds. + /// Create the smallest CIDR block that encompasses the given address range. + /// + /// - Parameters: + /// - lower: The lowest IPv4 address that must be included + /// - upper: The highest IPv4 address that must be included + /// - Throws: `NetworkAddressError.invalidAddressRange` if lower > upper + /// + /// This initializer finds the minimal prefix length that creates a CIDR block + /// containing both the lower and upper addresses. + /// + /// ## Example: + /// ```swift + /// let start = try IPv4Address("192.168.1.100") + /// let end = try IPv4Address("192.168.1.200") + /// let cidr = try CIDRAddress(lower: start, upper: end) + /// // Results in a block that contains both addresses + /// ``` public init(lower: IPv4Address, upper: IPv4Address) throws { guard lower.value <= upper.value else { throw NetworkAddressError.invalidAddressRange(lower: lower.description, upper: upper.description) @@ -85,9 +148,25 @@ public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable { self.upper = upper } - /// Get the offset of the specified address, relative to the - /// base address for the CIDR block, returning nil if the block - /// does not contain the address. + /// Get the zero-based index of the specified address within this CIDR block. + /// + /// - Parameter address: The IPv4 address to find the index for + /// - Returns: The index of the address within the block, or `nil` if not contained + /// + /// The index represents the offset from the network base address (lower bound). + /// This is useful for address allocation and iteration within a subnet. + /// + /// ## Example: + /// ```swift + /// let cidr = try CIDRAddress("192.168.1.0/24") + /// let addr = try IPv4Address("192.168.1.10") + /// if let index = cidr.getIndex(addr) { + /// print("Address index: \(index)") // 10 + /// } + /// + /// let outOfRange = try IPv4Address("192.168.2.1") + /// print(cidr.getIndex(outOfRange)) // nil + /// ``` public func getIndex(_ address: IPv4Address) -> UInt32? { guard address.value >= lower.value && address.value <= upper.value else { return nil @@ -96,35 +175,84 @@ public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable { return address.value - lower.value } - /// Return true if the CIDR block contains the specified address. + /// Check if the CIDR block contains the specified IPv4 address. + /// + /// - Parameter ipv4: The IPv4 address to test for containment + /// - Returns: `true` if the address is within this CIDR block's range + /// + /// ## Example: + /// ```swift + /// let cidr = try CIDRAddress("10.0.0.0/8") + /// print(cidr.contains(ipv4: try IPv4Address("10.5.1.1"))) // true + /// print(cidr.contains(ipv4: try IPv4Address("192.168.1.1"))) // false + /// ``` public func contains(ipv4: IPv4Address) -> Bool { lower.value <= ipv4.value && ipv4.value <= upper.value } - /// Return true if the CIDR block contains all addresses of another CIDR block. + /// Check if this CIDR block completely contains another CIDR block. + /// + /// - Parameter cidr: The other CIDR block to test for containment + /// - Returns: `true` if the other block is entirely within this block + /// + /// ## Example: + /// ```swift + /// let large = try CIDRAddress("192.168.0.0/16") // /16 network + /// let small = try CIDRAddress("192.168.1.0/24") // /24 subnet + /// print(large.contains(cidr: small)) // true + /// print(small.contains(cidr: large)) // false + /// ``` public func contains(cidr: CIDRAddress) -> Bool { lower.value <= cidr.lower.value && cidr.upper.value <= upper.value } - /// Return true if the CIDR block shares any addresses with another CIDR block. + /// Check if this CIDR block shares any addresses with another CIDR block. + /// + /// - Parameter cidr: The other CIDR block to test for overlap + /// - Returns: `true` if the blocks have any addresses in common + /// + /// This method detects any form of overlap: partial overlap, complete containment, + /// or identical ranges. + /// + /// ## Example: + /// ```swift + /// let cidr1 = try CIDRAddress("192.168.1.0/24") + /// let cidr2 = try CIDRAddress("192.168.1.128/25") + /// let cidr3 = try CIDRAddress("192.168.2.0/24") + /// + /// print(cidr1.overlaps(cidr: cidr2)) // true (cidr2 is subset) + /// print(cidr1.overlaps(cidr: cidr3)) // false (different networks) + /// ``` public func overlaps(cidr: CIDRAddress) -> Bool { (lower.value <= cidr.lower.value && upper.value >= cidr.lower.value) || (upper.value >= cidr.upper.value && lower.value <= cidr.upper.value) } - /// Retrieve the text representation of the CIDR block. + /// Returns the text representation of the CIDR block in standard notation. + /// + /// The format is "address/prefix_length" where address is the original address + /// used to create the block (not necessarily the network address). public var description: String { "\(address)/\(prefixLength)" } } +// MARK: - Codable Conformance extension CIDRAddress: Codable { + /// Creates a CIDRAddress from a JSON string representation. + /// + /// - Parameter decoder: The decoder to read data from + /// - Throws: `DecodingError` if the string is not valid CIDR notation public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let text = try container.decode(String.self) try self.init(text) } + /// Encodes the CIDRAddress as a JSON string in CIDR notation. + /// + /// - Parameter encoder: The encoder to write data to + /// - Throws: `EncodingError` if encoding fails public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.description) diff --git a/Sources/ContainerizationExtras/IPAddress.swift b/Sources/ContainerizationExtras/IPAddress.swift index 850b4fea..e9ceb393 100644 --- a/Sources/ContainerizationExtras/IPAddress.swift +++ b/Sources/ContainerizationExtras/IPAddress.swift @@ -15,11 +15,42 @@ //===----------------------------------------------------------------------===// /// Facilitates conversion between IPv4 address representations. +/// +/// `IPv4Address` provides multiple ways to create and work with IPv4 addresses: +/// - From dotted-decimal strings (e.g., "192.168.1.1") +/// - From network byte arrays in big-endian order +/// - From 32-bit integer values +/// +/// The struct supports common networking operations like subnet prefix calculation +/// and provides seamless integration with JSON encoding/decoding. +/// +/// ## Example usage: +/// ```swift +/// // Create from different representations +/// let addr1 = try IPv4Address("192.168.1.1") +/// let addr2 = try IPv4Address(fromNetworkBytes: [192, 168, 1, 1]) +/// let addr3 = IPv4Address(fromValue: 0xc0a80101) +/// +/// // All three represent the same address +/// print(addr1 == addr2 && addr2 == addr3) // true +/// +/// // Get network prefix +/// let network = addr1.prefix(prefixLength: 24) // 192.168.1.0 +/// ``` public struct IPv4Address: Codable, CustomStringConvertible, Equatable, Sendable { - /// The address as a 32-bit integer. + /// The address as a 32-bit integer in host byte order. public let value: UInt32 - /// Create an address from a dotted-decimal string, such as "192.168.64.10". + /// Create an address from a dotted-decimal string. + /// + /// - Parameter fromString: An IPv4 address in dotted-decimal notation (e.g., "192.168.64.10") + /// - Throws: `NetworkAddressError.invalidStringAddress` if the string format is invalid + /// + /// ## Example: + /// ```swift + /// let address = try IPv4Address("10.0.0.1") + /// print(address.description) // "10.0.0.1" + /// ``` public init(_ fromString: String) throws { let split = fromString.components(separatedBy: ".") if split.count != 4 { @@ -37,8 +68,17 @@ public struct IPv4Address: Codable, CustomStringConvertible, Equatable, Sendable value = parsedValue } - /// Create an address from an array of four bytes in network order (big-endian), - /// such as [192, 168, 64, 10]. + /// Create an address from an array of four bytes in network order (big-endian). + /// + /// - Parameter fromNetworkBytes: An array of exactly 4 bytes representing the IPv4 address + /// - Throws: `NetworkAddressError.invalidNetworkByteAddress` if the array doesn't contain exactly 4 bytes + /// + /// ## Example: + /// ```swift + /// let bytes: [UInt8] = [192, 168, 1, 100] + /// let address = try IPv4Address(fromNetworkBytes: bytes) + /// print(address.description) // "192.168.1.100" + /// ``` public init(fromNetworkBytes: [UInt8]) throws { guard fromNetworkBytes.count == 4 else { throw NetworkAddressError.invalidNetworkByteAddress(address: fromNetworkBytes) @@ -51,12 +91,31 @@ public struct IPv4Address: Codable, CustomStringConvertible, Equatable, Sendable | UInt32(fromNetworkBytes[3]) } - /// Create an address from a 32-bit integer, such as 0xc0a8_400a. + /// Create an address from a 32-bit integer value. + /// + /// - Parameter fromValue: A 32-bit integer representing the IPv4 address in host byte order + /// + /// ## Example: + /// ```swift + /// let address = IPv4Address(fromValue: 0xc0a80164) // 192.168.1.100 + /// print(address.description) // "192.168.1.100" + /// ``` public init(fromValue: UInt32) { value = fromValue } - /// Retrieve the address as an array of bytes in network byte order. + /// Returns the address as an array of bytes in network byte order (big-endian). + /// + /// - Returns: An array of 4 bytes representing the IPv4 address + /// + /// This is useful for network programming where you need to send the address + /// over the network in the standard big-endian format. + /// + /// ## Example: + /// ```swift + /// let address = try IPv4Address("10.0.0.1") + /// let bytes = address.networkBytes // [10, 0, 0, 1] + /// ``` public var networkBytes: [UInt8] { [ UInt8((value >> 24) & 0xff), @@ -66,25 +125,58 @@ public struct IPv4Address: Codable, CustomStringConvertible, Equatable, Sendable ] } - /// Retrieve the address as a dotted decimal string. + /// Returns the address as a dotted-decimal string. + /// + /// This property provides the standard human-readable representation of the IPv4 address. public var description: String { networkBytes.map(String.init).joined(separator: ".") } - /// Create the base IPv4 address for a network that contains this - /// address and uses the specified subnet mask length. + /// Create the network base address for a subnet containing this address. + /// + /// - Parameter prefixLength: The subnet mask length (0-32 bits) + /// - Returns: The base IPv4 address of the network containing this address + /// + /// This method applies the subnet mask to get the network portion of the address, + /// setting all host bits to zero. + /// + /// ## Example: + /// ```swift + /// let address = try IPv4Address("192.168.1.150") + /// let network = address.prefix(prefixLength: 24) + /// print(network.description) // "192.168.1.0" + /// + /// let subnetwork = address.prefix(prefixLength: 28) + /// print(subnetwork.description) // "192.168.1.144" + /// ``` public func prefix(prefixLength: PrefixLength) -> IPv4Address { IPv4Address(fromValue: value & prefixLength.prefixMask32) } } +// MARK: - Codable Conformance extension IPv4Address { + /// Creates an IPv4Address from a JSON string representation. + /// + /// - Parameter decoder: The decoder to read data from + /// - Throws: `DecodingError` if the string is not a valid IPv4 address format + /// + /// The JSON representation uses the standard dotted-decimal string format. + /// + /// ## Example JSON: + /// ```json + /// "192.168.1.1" + /// ``` public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let text = try container.decode(String.self) try self.init(text) } + /// Encodes the IPv4Address as a JSON string in dotted-decimal format. + /// + /// - Parameter encoder: The encoder to write data to + /// - Throws: `EncodingError` if encoding fails public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.description) diff --git a/Sources/ContainerizationExtras/NetworkAddress.swift b/Sources/ContainerizationExtras/NetworkAddress.swift index c2b826ce..d496a32a 100644 --- a/Sources/ContainerizationExtras/NetworkAddress.swift +++ b/Sources/ContainerizationExtras/NetworkAddress.swift @@ -22,26 +22,31 @@ public enum NetworkAddressError: Swift.Error, Equatable, CustomStringConvertible case invalidAddressForSubnet(address: String, cidr: String) case invalidAddressRange(lower: String, upper: String) + /// Provides detailed, actionable error descriptions to help developers fix validation issues. public var description: String { switch self { case .invalidStringAddress(let address): - return "invalid IP address string \(address)" + return "Invalid IPv4 address format '\(address)'. Expected dotted-decimal notation with 4 octets (0-255) separated by dots, such as '192.168.1.1' or '10.0.0.255'." case .invalidNetworkByteAddress(let address): - return "invalid IP address bytes \(address)" + return "Invalid IPv4 address bytes \(address). Expected exactly 4 bytes with values in range 0-255, such as [192, 168, 1, 1]." case .invalidCIDR(let cidr): - return "invalid CIDR block: \(cidr)" + return "Invalid CIDR block '\(cidr)'. Expected format 'x.x.x.x/n' where x.x.x.x is a valid IPv4 address and n is a prefix length from 0-32, such as '192.168.1.0/24' or '10.0.0.0/8'." case .invalidAddressForSubnet(let address, let cidr): - return "invalid address \(address) for subnet \(cidr)" + return "Invalid address '\(address)' for subnet '\(cidr)'. The address must be within the network range defined by the CIDR block." case .invalidAddressRange(let lower, let upper): - return "invalid range for addresses \(lower) and \(upper)" + return "Invalid address range from '\(lower)' to '\(upper)'. The lower bound must be less than or equal to the upper bound, and both must be valid IPv4 addresses." } } } +/// Type alias for network prefix lengths (0-32 for IPv4, 0-48 for truncated IPv6). public typealias PrefixLength = UInt8 extension PrefixLength { /// Compute a bit mask that passes the suffix bits, given the network prefix mask length. + /// + /// For IPv4 addresses, this calculates the host portion mask. + /// - Returns: A 32-bit mask where host bits are set to 1 public var suffixMask32: UInt32 { if self <= 0 { return 0xffff_ffff @@ -50,11 +55,17 @@ extension PrefixLength { } /// Compute a bit mask that passes the prefix bits, given the network prefix mask length. + /// + /// For IPv4 addresses, this calculates the network portion mask. + /// - Returns: A 32-bit mask where network bits are set to 1 public var prefixMask32: UInt32 { ~self.suffixMask32 } /// Compute a bit mask that passes the suffix bits, given the network prefix mask length. + /// + /// For truncated IPv6 addresses (48-bit), this calculates the host portion mask. + /// - Returns: A 64-bit mask where host bits are set to 1 (masked to 48 bits) public var suffixMask48: UInt64 { if self <= 0 { return 0x0000_ffff_ffff_ffff @@ -63,6 +74,9 @@ extension PrefixLength { } /// Compute a bit mask that passes the prefix bits, given the network prefix mask length. + /// + /// For truncated IPv6 addresses (48-bit), this calculates the network portion mask. + /// - Returns: A 64-bit mask where network bits are set to 1 (masked to 48 bits) public var prefixMask48: UInt64 { ~self.suffixMask48 & 0x0000_ffff_ffff_ffff } diff --git a/Sources/ContainerizationExtras/Timeout.swift b/Sources/ContainerizationExtras/Timeout.swift index 42b6e55a..4e81dc3d 100644 --- a/Sources/ContainerizationExtras/Timeout.swift +++ b/Sources/ContainerizationExtras/Timeout.swift @@ -16,11 +16,83 @@ import Foundation -/// `Timeout` contains helpers to run an operation and error out if -/// the operation does not finish within a provided time. +/// Provides utilities for executing async operations with time constraints. +/// +/// `Timeout` helps ensure that long-running async operations don't hang indefinitely +/// by automatically canceling them after a specified duration. This is especially +/// useful for network operations, file I/O, or any async task that might block. +/// +/// ## Use Cases +/// - Network requests that might hang +/// - File operations on potentially slow storage +/// - Container or VM operations with unpredictable execution times +/// - Any async operation that needs guaranteed completion time +/// +/// ## Example usage: +/// ```swift +/// // Timeout a network request after 30 seconds +/// do { +/// let data = try await Timeout.run(seconds: 30) { +/// await networkClient.fetchData() +/// } +/// print("Request completed: \(data)") +/// } catch is CancellationError { +/// print("Request timed out after 30 seconds") +/// } +/// +/// // Timeout a container start operation +/// do { +/// let container = try await Timeout.run(seconds: 60) { +/// await containerManager.startContainer(id: "abc123") +/// } +/// print("Container started successfully") +/// } catch is CancellationError { +/// print("Container start timed out") +/// } +/// ``` public struct Timeout { - /// Performs the passed in `operation` and throws a `CancellationError` if the operation - /// doesn't finish in the provided `seconds` amount. + /// Executes an async operation with a timeout, canceling it if it doesn't complete in time. + /// + /// - Parameters: + /// - seconds: The maximum number of seconds to wait for the operation to complete + /// - operation: The async operation to execute with timeout protection + /// - Returns: The result of the operation if it completes within the timeout + /// - Throws: `CancellationError` if the operation doesn't complete within the specified time + /// + /// This method uses structured concurrency to race the provided operation against + /// a timer. If the operation completes first, its result is returned. If the timer + /// expires first, a `CancellationError` is thrown and any pending work is canceled. + /// + /// ## Implementation Details + /// - Uses `TaskGroup` for structured concurrency + /// - Automatically cancels remaining tasks when one completes + /// - The timeout precision is limited by the system's task scheduling + /// - Operations are not forcefully terminated - they receive a cancellation signal + /// + /// ## Example: + /// ```swift + /// // Simple timeout example + /// let result = try await Timeout.run(seconds: 5) { + /// await someAsyncOperation() + /// } + /// + /// // Handling timeout errors + /// do { + /// let data = try await Timeout.run(seconds: 10) { + /// await longRunningOperation() + /// } + /// handleSuccess(data) + /// } catch is CancellationError { + /// handleTimeout() + /// } catch { + /// handleOtherError(error) + /// } + /// ``` + /// + /// ## Performance Notes + /// - Minimal overhead when operations complete quickly + /// - Timer task is automatically cleaned up when operation completes + /// - Uses cooperative cancellation - operations must check for cancellation public static func run( seconds: UInt32, operation: @escaping @Sendable () async -> T @@ -36,7 +108,7 @@ public struct Timeout { } guard let result = try await group.next() else { - fatalError() + fatalError("TaskGroup.next() unexpectedly returned nil") } group.cancelAll()