Upgrade grpc-swift-nio-transport to 2.9.0 and remove HTTP2ConnectBuff… (#778)

- Fixes #776
- Release 2.9.0 of `grpc-swift-nio-transport` fixes
  the HTTP/2 initialization race that required a
  `HTTP2ConnectBufferingHandler` for the vminitd
  API. This upgrades the dependency and eliminates
  the workaround.
This commit is contained in:
adityabagchi24 authored and GitHub committed 2026-07-07 08:49:17 -07:00
1 parent bd709ba8cb
commit 7aa4e723f6
8 files changed
+35 -120

No files matched your search

+3 -3
View File
@@ -1,5 +1,5 @@
{
"originHash" : "5d4a569160adc023c31092ec813aeb5f7e7ac67ed853dfeb76f9964d10109bca",
"originHash" : "2b73182b4d0f81b61ee24d8e615d350d28301517488d944fafddde15bc854cdc",
"pins" : [
{
"identity" : "async-http-client",
@@ -24,8 +24,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift-nio-transport.git",
"state" : {
"revision" : "f37e0c2d293cea668b11e10e1fb1c24cb40781ff",
"version" : "2.4.4"
"revision" : "2ca31f06658ed288a2560e23ad649acbb3d6b3a3",
"version" : "2.9.0"
}
},
{
+1 -1
View File
@@ -43,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/grpc/grpc-swift-2.git", from: "2.3.0"),
.package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "2.4.4"),
.package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "2.9.0"),
.package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "2.2.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.36.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
@@ -261,7 +261,7 @@ extension CHVirtualMachineInstance: VirtualMachineInstance {
try await chCall { try await self.client.vmBoot() }
let fh = try await self.dialVminitdWithRetries()
let agent = try Vminitd(connection: fh, group: self.group)
let agent = try await Vminitd(connection: fh, group: self.group)
await self.timeSyncer.start(context: agent)
for ext in self.config.extensions.compactMap({ $0 as? any CHInstanceExtension }) {
@@ -377,7 +377,7 @@ extension CHVirtualMachineInstance: VirtualMachineInstance {
baseSocket: self.workDir.appendingPathComponent("vsock.sock"),
port: Vminitd.port
)
return try Vminitd(connection: fh, group: self.group)
return try await Vminitd(connection: fh, group: self.group)
}
}
@@ -1,90 +0,0 @@
//===----------------------------------------------------------------------===//
// Copyright © 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 GRPCCore
import GRPCNIOTransportCore
import NIOCore
import NIOPosix
/// Buffers incoming bytes until the full gRPC HTTP/2 pipeline is configured, then replays them.
///
/// This prevents the race condition where the vminitd server's initial HTTP/2 SETTINGS frame
/// arrives and is discarded before `configureGRPCClientPipeline` has finished installing
/// `ClientConnectionHandler`.
///
/// The handler is added via `ClientBootstrap.channelInitializer`, which runs before
/// `registerAlreadyConfigured0` adds the fd to epoll/kqueue — guaranteeing it is in place
/// before any bytes can arrive on the socket.
///
/// When `NIOHTTP2Handler` is added to the pipeline (inside `configureGRPCClientPipeline`), its
/// `handlerAdded` fires an outbound flush (the HTTP/2 client preface). We intercept that flush
/// and schedule a deferred removal via the event loop. Because `configureGRPCClientPipeline` runs
/// as a single synchronous event loop task, the deferred removal is guaranteed to run after that
/// entire task completes — i.e., after `ClientConnectionHandler` is also in the pipeline.
/// Buffered bytes are replayed atomically as part of the pipeline removal.
// FIXME: This handler is needed until the swift GRPC libraries offers us a way to create a
// client transport from an existing fd. Remove this type when such an API exists.
public final class HTTP2ConnectBufferingHandler: ChannelDuplexHandler, RemovableChannelHandler {
public typealias InboundIn = ByteBuffer
public typealias InboundOut = ByteBuffer
public typealias OutboundIn = ByteBuffer
public typealias OutboundOut = ByteBuffer
private var removalScheduled = false
private var bufferedReads: [NIOAny] = []
public init() {}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
bufferedReads.append(data)
}
public func channelReadComplete(context: ChannelHandlerContext) {
// Suppress while buffering; a single readComplete is emitted after replay.
}
public func flush(context: ChannelHandlerContext) {
if !removalScheduled {
removalScheduled = true
// Defer removal to the next event loop task. configureGRPCClientPipeline runs as a
// single synchronous event loop task, so this deferred task is guaranteed to run
// after that whole task completes (including ClientConnectionHandler being added).
context.eventLoop.assumeIsolatedUnsafeUnchecked().execute {
context.pipeline.syncOperations.removeHandler(self, promise: nil)
}
}
context.flush()
}
public func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
var didRead = false
while !bufferedReads.isEmpty {
context.fireChannelRead(bufferedReads.removeFirst())
didRead = true
}
if didRead {
context.fireChannelReadComplete()
}
context.leavePipeline(removalToken: removalToken)
}
public func channelInactive(context: ChannelHandlerContext) {
bufferedReads.removeAll()
context.fireChannelInactive()
}
}
@@ -191,7 +191,7 @@ extension VZVirtualMachineInstance: VirtualMachineInstance {
try await self.vm.start(queue: self.queue)
let agent = try Vminitd(
let agent = try await Vminitd(
connection: try await self.vm.waitForAgent(queue: self.queue),
group: self.group
)
@@ -260,7 +260,7 @@ extension VZVirtualMachineInstance: VirtualMachineInstance {
port: Vminitd.port
)
let handle = try conn.dupHandle()
return try Vminitd(connection: handle, group: self.group)
return try await Vminitd(connection: handle, group: self.group)
} catch {
if let err = error as? ContainerizationError {
throw err
+19 -14
View File
@@ -20,9 +20,8 @@ import ContainerizationOCI
import ContainerizationOS
import Foundation
import GRPCCore
import GRPCNIOTransportCore
import NIOCore
import NIOPosix
import GRPCNIOTransportHTTP2
import NIO
/// A remote connection into the vminitd Linux guest agent via a port (vsock).
/// Used to modify the runtime environment of the Linux sandbox.
@@ -34,18 +33,24 @@ public struct Vminitd: Sendable {
public let grpcClient: GRPCClient<HTTP2ClientTransport.WrappedChannel>
private let connectionTask: Task<Void, Error>
public init(connection: FileHandle, group: any EventLoopGroup) throws {
let channel = try ClientBootstrap(group: group)
.channelInitializer { channel in
channel.eventLoop.makeCompletedFuture(withResultOf: {
try channel.pipeline.syncOperations.addHandler(HTTP2ConnectBufferingHandler())
})
public init(connection: FileHandle, group: any EventLoopGroup) async throws {
let transport = try await HTTP2ClientTransport.WrappedChannel.wrapping(
config: .defaults { $0.connection.maxIdleTime = nil },
serviceConfig: .init()
) { configure in
try await withCheckedThrowingContinuation { continuation in
ClientBootstrap(group: group)
.channelInitializer { channel in
configure(channel).map { configured in
continuation.resume(returning: configured)
}
}
.withConnectedSocket(connection.fileDescriptor)
.whenFailure { error in
continuation.resume(throwing: error)
}
}
.withConnectedSocket(connection.fileDescriptor).wait()
let transport = HTTP2ClientTransport.WrappedChannel.wrapping(
channel: channel,
config: .defaults { $0.connection.maxIdleTime = nil }
)
}
let grpcClient = GRPCClient(transport: transport)
self.grpcClient = grpcClient
self.client = Com_Apple_Containerization_Sandbox_V3_SandboxContext.Client(wrapping: self.grpcClient)
+1 -1
View File
@@ -1733,7 +1733,7 @@ extension IntegrationSuite {
try await assertExec(container, id: "create-fifo", cmd: "mkfifo /tmp/test-fifo")
let vsock = try await container.dialVsock(port: 1024)
let vminitd = try Vminitd(connection: vsock, group: Self.eventLoop)
let vminitd = try await Vminitd(connection: vsock, group: Self.eventLoop)
let root = URL(filePath: container.root)
+7 -7
View File
@@ -1,5 +1,5 @@
{
"originHash" : "6ccceb47b6a402e9ac07d23204ec7f4792823b22b96275cd67f8531787a60c04",
"originHash" : "264b211a5ea74fa24ced86faade5901700722c925484a26379cc4a6b40083c6c",
"pins" : [
{
"identity" : "async-http-client",
@@ -24,8 +24,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift-nio-transport.git",
"state" : {
"revision" : "f37e0c2d293cea668b11e10e1fb1c24cb40781ff",
"version" : "2.4.4"
"revision" : "2ca31f06658ed288a2560e23ad649acbb3d6b3a3",
"version" : "2.9.0"
}
},
{
@@ -150,8 +150,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "4e8f4b1c9adaa59315c523540c1ff2b38adc20a9",
"version" : "2.87.0"
"revision" : "cd3e1152083706d77b223fb29110e590efcc70c0",
"version" : "2.101.2"
}
},
{
@@ -168,8 +168,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "5e9e99ec96c53bc2c18ddd10c1e25a3cd97c55e5",
"version" : "1.38.0"
"revision" : "61d1b44f6e4e118792be1cff88ee2bc0267c6f9a",
"version" : "1.44.0"
}
},
{