Files

111 lines
4.1 KiB
Swift
Raw Permalink Normal View History

//===----------------------------------------------------------------------===//
// 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<Result: Sendable>(
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
)
}
}