Merge nucleic/fuzzy-yarn-otter-2pji into dev
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user