111 lines
4.1 KiB
Swift
111 lines
4.1 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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
|
|
)
|
|
}
|
|
}
|