//===----------------------------------------------------------------------===// // Copyright © 2025-2026 Apple Inc. and the Containerization project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// import ContainerizationError import Foundation import GRPCCore /// Monotonic deadlines for operations which cross a process or transport boundary. /// /// Keep generated gRPC calls behind ``performGRPC(operation:deadline:_:)``. Requiring both an /// operation class and an absolute deadline makes a call which silently falls back to generated /// default options conspicuous during review and straightforward to reject in CI. public struct DeadlinePolicy: Sendable { public typealias Deadline = ContinuousClock.Instant public enum Operation: String, Sendable { case dial case statistics case createProcess case startProcess case processControl case deleteProcess case agentClose case waitProcess case filesystem } public struct Bounds: Sendable { public var dial: Duration = .seconds(3) public var statistics: Duration = .seconds(2) public var createProcess: Duration = .seconds(10) public var startProcess: Duration = .seconds(15) public var processControl: Duration = .seconds(3) public var deleteProcess: Duration = .seconds(30) public var agentClose: Duration = .seconds(3) public var waitProcess: Duration = .seconds(30) public var filesystem: Duration = .seconds(10) public init() {} } public static let standard = DeadlinePolicy() public var bounds: Bounds public init(bounds: Bounds = Bounds()) { self.bounds = bounds } public func bound(for operation: Operation) -> Duration { switch operation { case .dial: bounds.dial case .statistics: bounds.statistics case .createProcess: bounds.createProcess case .startProcess: bounds.startProcess case .processControl: bounds.processControl case .deleteProcess: bounds.deleteProcess case .agentClose: bounds.agentClose case .waitProcess: bounds.waitProcess case .filesystem: bounds.filesystem } } public func deadline(for operation: Operation, clock: ContinuousClock = .init()) -> Deadline { clock.now.advanced(by: bound(for: operation)) } /// Invoke a generated gRPC call with the remaining part of an absolute monotonic deadline. /// gRPC aborts the RPC when this timeout expires, so the server operation and client-side /// continuation aren't left live after the caller receives a timeout. public func performGRPC( operation: Operation, deadline: Deadline, _ call: @Sendable (GRPCCore.CallOptions) async throws -> Result ) async throws -> Result { let remaining = ContinuousClock().now.duration(to: deadline) guard remaining > .zero else { throw timeoutError(for: operation) } var options = GRPCCore.CallOptions.defaults options.timeout = remaining do { return try await call(options) } catch let error as RPCError where error.code == .deadlineExceeded { throw timeoutError(for: operation, cause: error) } } public func timeoutError(for operation: Operation, cause: (any Error)? = nil) -> ContainerizationError { ContainerizationError( .timeout, message: "\(operation.rawValue) exceeded its monotonic deadline", cause: cause ) } }