Skip to content

Commit aa69fcb

Browse files
authored
Make the connection pool error public (#1685)
Motivation: Sometimes users end up with connection pool errors but they can't inspect them because the type is internal. Modifications: - Make the connection pool error extensible and public Result: Users can inspect connection pool errors.
1 parent f1669c8 commit aa69fcb

File tree

3 files changed

+126
-60
lines changed

3 files changed

+126
-60
lines changed

Sources/GRPC/ConnectionPool/ConnectionPool.swift

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ internal final class ConnectionPool {
283283

284284
guard case .active = self._state else {
285285
// Fail the promise right away if we're shutting down or already shut down.
286-
promise.fail(ConnectionPoolError.shutdown)
286+
promise.fail(GRPCConnectionPoolError.shutdown)
287287
return
288288
}
289289

@@ -354,7 +354,7 @@ internal final class ConnectionPool {
354354
Metadata.waitersMax: "\(self.maxWaiters)"
355355
]
356356
)
357-
promise.fail(ConnectionPoolError.tooManyWaiters(connectionError: self._mostRecentError))
357+
promise.fail(GRPCConnectionPoolError.tooManyWaiters(connectionError: self._mostRecentError))
358358
return
359359
}
360360

@@ -364,7 +364,7 @@ internal final class ConnectionPool {
364364
// timeout before appending it to the waiters, it wont run until the next event loop tick at the
365365
// earliest (even if the deadline has already passed).
366366
waiter.scheduleTimeout(on: self.eventLoop) {
367-
waiter.fail(ConnectionPoolError.deadlineExceeded(connectionError: self._mostRecentError))
367+
waiter.fail(GRPCConnectionPoolError.deadlineExceeded(connectionError: self._mostRecentError))
368368

369369
if let index = self.waiters.firstIndex(where: { $0.id == waiter.id }) {
370370
self.waiters.remove(at: index)
@@ -550,7 +550,7 @@ internal final class ConnectionPool {
550550

551551
// Fail the outstanding waiters.
552552
while let waiter = self.waiters.popFirst() {
553-
waiter.fail(ConnectionPoolError.shutdown)
553+
waiter.fail(GRPCConnectionPoolError.shutdown)
554554
}
555555

556556
// Cascade the result of the shutdown into the promise.
@@ -864,40 +864,97 @@ extension ConnectionPool {
864864
}
865865
}
866866

867-
@usableFromInline
868-
internal enum ConnectionPoolError: Error {
869-
/// The pool is shutdown or shutting down.
870-
case shutdown
867+
/// An error thrown from the ``GRPCChannelPool``.
868+
public struct GRPCConnectionPoolError: Error, CustomStringConvertible {
869+
public struct Code: Hashable, Sendable, CustomStringConvertible {
870+
enum Code {
871+
case shutdown
872+
case tooManyWaiters
873+
case deadlineExceeded
874+
}
875+
876+
fileprivate var code: Code
877+
878+
private init(_ code: Code) {
879+
self.code = code
880+
}
881+
882+
public var description: String {
883+
String(describing: self.code)
884+
}
871885

872-
/// There are too many waiters in the pool.
873-
case tooManyWaiters(connectionError: Error?)
886+
/// The pool is shutdown or shutting down.
887+
public static var shutdown: Self { Self(.shutdown) }
874888

875-
/// The deadline for creating a stream has passed.
876-
case deadlineExceeded(connectionError: Error?)
889+
/// There are too many waiters in the pool.
890+
public static var tooManyWaiters: Self { Self(.tooManyWaiters) }
891+
892+
/// The deadline for creating a stream has passed.
893+
public static var deadlineExceeded: Self { Self(.deadlineExceeded) }
894+
}
895+
896+
/// The error code.
897+
public var code: Code
898+
899+
/// An underlying error which caused this error to be thrown.
900+
public var underlyingError: Error?
901+
902+
public var description: String {
903+
if let underlyingError = self.underlyingError {
904+
return "\(self.code) (\(underlyingError))"
905+
} else {
906+
return String(describing: self.code)
907+
}
908+
}
909+
910+
/// Create a new connection pool error with the given code and underlying error.
911+
///
912+
/// - Parameters:
913+
/// - code: The error code.
914+
/// - underlyingError: The underlying error which led to this error being thrown.
915+
public init(code: Code, underlyingError: Error? = nil) {
916+
self.code = code
917+
self.underlyingError = underlyingError
918+
}
877919
}
878920

879-
extension ConnectionPoolError: GRPCStatusTransformable {
921+
extension GRPCConnectionPoolError {
880922
@usableFromInline
881-
internal func makeGRPCStatus() -> GRPCStatus {
882-
switch self {
923+
static let shutdown = Self(code: .shutdown)
924+
925+
@inlinable
926+
static func tooManyWaiters(connectionError: Error?) -> Self {
927+
Self(code: .tooManyWaiters, underlyingError: connectionError)
928+
}
929+
930+
@inlinable
931+
static func deadlineExceeded(connectionError: Error?) -> Self {
932+
Self(code: .deadlineExceeded, underlyingError: connectionError)
933+
}
934+
}
935+
936+
extension GRPCConnectionPoolError: GRPCStatusTransformable {
937+
public func makeGRPCStatus() -> GRPCStatus {
938+
switch self.code.code {
883939
case .shutdown:
884940
return GRPCStatus(
885941
code: .unavailable,
886-
message: "The connection pool is shutdown"
942+
message: "The connection pool is shutdown",
943+
cause: self.underlyingError
887944
)
888945

889-
case let .tooManyWaiters(error):
946+
case .tooManyWaiters:
890947
return GRPCStatus(
891948
code: .resourceExhausted,
892949
message: "The connection pool has no capacity for new RPCs or RPC waiters",
893-
cause: error
950+
cause: self.underlyingError
894951
)
895952

896-
case let .deadlineExceeded(error):
953+
case .deadlineExceeded:
897954
return GRPCStatus(
898955
code: .deadlineExceeded,
899956
message: "Timed out waiting for an HTTP/2 stream from the connection pool",
900-
cause: error
957+
cause: self.underlyingError
901958
)
902959
}
903960
}

Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ final class ConnectionPoolTests: GRPCTestCase {
160160
}
161161

162162
XCTAssertThrowsError(try stream.wait()) { error in
163-
XCTAssert((error as? ConnectionPoolError).isShutdown)
163+
XCTAssert((error as? GRPCConnectionPoolError).isShutdown)
164164
}
165165
}
166166

@@ -181,14 +181,14 @@ final class ConnectionPoolTests: GRPCTestCase {
181181
}
182182

183183
XCTAssertThrowsError(try tooManyWaiters.wait()) { error in
184-
XCTAssert((error as? ConnectionPoolError).isTooManyWaiters)
184+
XCTAssert((error as? GRPCConnectionPoolError).isTooManyWaiters)
185185
}
186186

187187
XCTAssertNoThrow(try pool.shutdown().wait())
188188
// All 'waiting' futures will be failed by the shutdown promise.
189189
for waiter in waiting {
190190
XCTAssertThrowsError(try waiter.wait()) { error in
191-
XCTAssert((error as? ConnectionPoolError).isShutdown)
191+
XCTAssert((error as? GRPCConnectionPoolError).isShutdown)
192192
}
193193
}
194194
}
@@ -205,7 +205,7 @@ final class ConnectionPoolTests: GRPCTestCase {
205205

206206
self.eventLoop.advanceTime(to: .uptimeNanoseconds(10))
207207
XCTAssertThrowsError(try waiter.wait()) { error in
208-
XCTAssert((error as? ConnectionPoolError).isDeadlineExceeded)
208+
XCTAssert((error as? GRPCConnectionPoolError).isDeadlineExceeded)
209209
}
210210

211211
XCTAssertEqual(pool.sync.waiters, 0)
@@ -225,7 +225,7 @@ final class ConnectionPoolTests: GRPCTestCase {
225225

226226
self.eventLoop.run()
227227
XCTAssertThrowsError(try waiter.wait()) { error in
228-
XCTAssert((error as? ConnectionPoolError).isDeadlineExceeded)
228+
XCTAssert((error as? GRPCConnectionPoolError).isDeadlineExceeded)
229229
}
230230

231231
XCTAssertEqual(pool.sync.waiters, 0)
@@ -358,7 +358,7 @@ final class ConnectionPoolTests: GRPCTestCase {
358358
XCTAssertNoThrow(try shutdown.wait())
359359
for waiter in others {
360360
XCTAssertThrowsError(try waiter.wait()) { error in
361-
XCTAssert((error as? ConnectionPoolError).isShutdown)
361+
XCTAssert((error as? GRPCConnectionPoolError).isShutdown)
362362
}
363363
}
364364
}
@@ -503,7 +503,7 @@ final class ConnectionPoolTests: GRPCTestCase {
503503
// We need to advance the time to fire the timeout to fail the waiter.
504504
self.eventLoop.advanceTime(to: .uptimeNanoseconds(10))
505505
XCTAssertThrowsError(try waiter1.wait()) { error in
506-
XCTAssert((error as? ConnectionPoolError).isDeadlineExceeded)
506+
XCTAssert((error as? GRPCConnectionPoolError).isDeadlineExceeded)
507507
}
508508

509509
self.eventLoop.run()
@@ -758,8 +758,10 @@ final class ConnectionPoolTests: GRPCTestCase {
758758
self.eventLoop.advanceTime(to: .uptimeNanoseconds(10))
759759

760760
XCTAssertThrowsError(try w1.wait()) { error in
761-
switch error as? ConnectionPoolError {
762-
case .some(.deadlineExceeded(.none)):
761+
switch error as? GRPCConnectionPoolError {
762+
case .some(let error):
763+
XCTAssertEqual(error.code, .deadlineExceeded)
764+
XCTAssertNil(error.underlyingError)
763765
// Deadline exceeded but no underlying error, as expected.
764766
()
765767
default:
@@ -774,10 +776,11 @@ final class ConnectionPoolTests: GRPCTestCase {
774776
self.eventLoop.advanceTime(to: .uptimeNanoseconds(20))
775777

776778
XCTAssertThrowsError(try w2.wait()) { error in
777-
switch error as? ConnectionPoolError {
778-
case let .some(.deadlineExceeded(.some(wrappedError))):
779+
switch error as? GRPCConnectionPoolError {
780+
case let .some(error):
781+
XCTAssertEqual(error.code, .deadlineExceeded)
779782
// Deadline exceeded and we have the underlying error.
780-
XCTAssert(wrappedError is DummyError)
783+
XCTAssert(error.underlyingError is DummyError)
781784
default:
782785
XCTFail("Expected ConnectionPoolError.deadlineExceeded(.some) but got \(error)")
783786
}
@@ -837,9 +840,10 @@ final class ConnectionPoolTests: GRPCTestCase {
837840
$0.eventLoop.makeSucceededVoidFuture()
838841
}
839842
XCTAssertThrowsError(try tooManyWaiters.wait()) { error in
840-
switch error as? ConnectionPoolError {
841-
case .some(.tooManyWaiters(.none)):
842-
()
843+
switch error as? GRPCConnectionPoolError {
844+
case .some(let error):
845+
XCTAssertEqual(error.code, .tooManyWaiters)
846+
XCTAssertNil(error.underlyingError)
843847
default:
844848
XCTFail("Expected ConnectionPoolError.tooManyWaiters(.none) but got \(error)")
845849
}
@@ -849,9 +853,10 @@ final class ConnectionPoolTests: GRPCTestCase {
849853
self.eventLoop.advanceTime(by: .seconds(1))
850854
for waiter in waiters {
851855
XCTAssertThrowsError(try waiter.wait()) { error in
852-
switch error as? ConnectionPoolError {
853-
case .some(.deadlineExceeded(.none)):
854-
()
856+
switch error as? GRPCConnectionPoolError {
857+
case .some(let error):
858+
XCTAssertEqual(error.code, .deadlineExceeded)
859+
XCTAssertNil(error.underlyingError)
855860
default:
856861
XCTFail("Expected ConnectionPoolError.deadlineExceeded(.none) but got \(error)")
857862
}
@@ -869,7 +874,7 @@ final class ConnectionPoolTests: GRPCTestCase {
869874
XCTAssertNil(waiter._scheduledTimeout)
870875

871876
waiter.scheduleTimeout(on: self.eventLoop) {
872-
waiter.fail(ConnectionPoolError.deadlineExceeded(connectionError: nil))
877+
waiter.fail(GRPCConnectionPoolError.deadlineExceeded(connectionError: nil))
873878
}
874879

875880
XCTAssertNotNil(waiter._scheduledTimeout)
@@ -1045,6 +1050,25 @@ final class ConnectionPoolTests: GRPCTestCase {
10451050
}
10461051
}
10471052
}
1053+
1054+
func testConnectionPoolErrorDescription() {
1055+
var error = GRPCConnectionPoolError(code: .deadlineExceeded)
1056+
XCTAssertEqual(String(describing: error), "deadlineExceeded")
1057+
error.code = .shutdown
1058+
XCTAssertEqual(String(describing: error), "shutdown")
1059+
error.code = .tooManyWaiters
1060+
XCTAssertEqual(String(describing: error), "tooManyWaiters")
1061+
1062+
struct DummyError: Error {}
1063+
error.underlyingError = DummyError()
1064+
XCTAssertEqual(String(describing: error), "tooManyWaiters (DummyError())")
1065+
}
1066+
1067+
func testConnectionPoolErrorCodeEquality() {
1068+
let error = GRPCConnectionPoolError(code: .deadlineExceeded)
1069+
XCTAssertEqual(error.code, .deadlineExceeded)
1070+
XCTAssertNotEqual(error.code, .shutdown)
1071+
}
10481072
}
10491073

10501074
extension ConnectionPool {
@@ -1216,31 +1240,16 @@ internal struct HookedStreamLender: StreamLender {
12161240
}
12171241
}
12181242

1219-
extension Optional where Wrapped == ConnectionPoolError {
1243+
extension Optional where Wrapped == GRPCConnectionPoolError {
12201244
internal var isTooManyWaiters: Bool {
1221-
switch self {
1222-
case .some(.tooManyWaiters):
1223-
return true
1224-
case .some(.deadlineExceeded), .some(.shutdown), .none:
1225-
return false
1226-
}
1245+
self?.code == .tooManyWaiters
12271246
}
12281247

12291248
internal var isDeadlineExceeded: Bool {
1230-
switch self {
1231-
case .some(.deadlineExceeded):
1232-
return true
1233-
case .some(.tooManyWaiters), .some(.shutdown), .none:
1234-
return false
1235-
}
1249+
self?.code == .deadlineExceeded
12361250
}
12371251

12381252
internal var isShutdown: Bool {
1239-
switch self {
1240-
case .some(.shutdown):
1241-
return true
1242-
case .some(.tooManyWaiters), .some(.deadlineExceeded), .none:
1243-
return false
1244-
}
1253+
self?.code == .shutdown
12451254
}
12461255
}

Tests/GRPCTests/GRPCStatusTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,10 @@ class GRPCStatusTests: GRPCTestCase {
113113
// No message/cause, so uses the nil backing storage.
114114
XCTAssertEqual(status.testingOnly_storageObjectIdentifier, nilStorageID)
115115

116-
status.cause = ConnectionPoolError.tooManyWaiters(connectionError: nil)
116+
status.cause = GRPCConnectionPoolError.tooManyWaiters(connectionError: nil)
117117
let storageID = status.testingOnly_storageObjectIdentifier
118118
XCTAssertNotEqual(storageID, nilStorageID)
119-
XCTAssert(status.cause is ConnectionPoolError)
119+
XCTAssert(status.cause is GRPCConnectionPoolError)
120120

121121
// The storage of status should be uniquely ref'd, so setting cause to nil should not change
122122
// the backing storage (even if the nil storage could now be used).

0 commit comments

Comments
 (0)