Browse Source

Provide errors to failed client promises rather than a status (#859)

Motivation:

If an RPC fails for some reason we will convert the error into an
appropriate `GRPCStatus`. This status is used to succeed the status
promise and fail any other promises (initial and trailing metadata, and
if applicable, the response promise). Without looking at logs it's
harder to determine the exact nature of the error.

Modifications:

- Fail the initial metadata, trailing metadata and response promise with
  the error that the status was derived from.

Result:

More helpful errors.
George Barnett 5 years ago
parent
commit
b6fca172f1

+ 9 - 5
Sources/GRPC/ClientCalls/ClientCallTransport.swift

@@ -373,9 +373,11 @@ extension ChannelTransport {
       var loggerWithState = self.logger
       var loggerWithState = self.logger
       loggerWithState[metadataKey: "call_state"] = "\(self.describeCallState())"
       loggerWithState[metadataKey: "call_state"] = "\(self.describeCallState())"
       let errorStatus: GRPCStatus
       let errorStatus: GRPCStatus
+      let errorWithoutContext: Error
 
 
       if let errorWithContext = error as? GRPCError.WithContext {
       if let errorWithContext = error as? GRPCError.WithContext {
         errorStatus = errorWithContext.error.makeGRPCStatus()
         errorStatus = errorWithContext.error.makeGRPCStatus()
+        errorWithoutContext = errorWithContext.error
         self.errorDelegate?.didCatchError(
         self.errorDelegate?.didCatchError(
           errorWithContext.error,
           errorWithContext.error,
           logger: loggerWithState,
           logger: loggerWithState,
@@ -384,14 +386,16 @@ extension ChannelTransport {
         )
         )
       } else if let transformable = error as? GRPCStatusTransformable {
       } else if let transformable = error as? GRPCStatusTransformable {
         errorStatus = transformable.makeGRPCStatus()
         errorStatus = transformable.makeGRPCStatus()
+        errorWithoutContext = error
         self.errorDelegate?.didCatchErrorWithoutContext(error, logger: loggerWithState)
         self.errorDelegate?.didCatchErrorWithoutContext(error, logger: loggerWithState)
       } else {
       } else {
         errorStatus = .processingError
         errorStatus = .processingError
+        errorWithoutContext = error
         self.errorDelegate?.didCatchErrorWithoutContext(error, logger: loggerWithState)
         self.errorDelegate?.didCatchErrorWithoutContext(error, logger: loggerWithState)
       }
       }
 
 
       // Update our state: we're closing.
       // Update our state: we're closing.
-      self.close(withStatus: errorStatus)
+      self.close(error: errorWithoutContext, status: errorStatus)
       promise?.fail(errorStatus)
       promise?.fail(errorStatus)
 
 
     case .closed:
     case .closed:
@@ -402,7 +406,7 @@ extension ChannelTransport {
   /// Close the call, if it's not yet closed with the given status.
   /// Close the call, if it's not yet closed with the given status.
   ///
   ///
   /// Must be called from the event loop.
   /// Must be called from the event loop.
-  private func close(withStatus status: GRPCStatus) {
+  private func close(error: Error, status: GRPCStatus) {
     self.eventLoop.preconditionInEventLoop()
     self.eventLoop.preconditionInEventLoop()
 
 
     switch self.state {
     switch self.state {
@@ -416,7 +420,7 @@ extension ChannelTransport {
       self.scheduledTimeout = nil
       self.scheduledTimeout = nil
 
 
       // Fail any outstanding promises.
       // Fail any outstanding promises.
-      self.responseContainer.fail(with: status)
+      self.responseContainer.fail(with: error, status: status)
 
 
       // Fail any buffered writes.
       // Fail any buffered writes.
       while !self.requestBuffer.isEmpty {
       while !self.requestBuffer.isEmpty {
@@ -439,7 +443,7 @@ extension ChannelTransport {
       self.scheduledTimeout = nil
       self.scheduledTimeout = nil
 
 
       // Fail any outstanding promises.
       // Fail any outstanding promises.
-      self.responseContainer.fail(with: status)
+      self.responseContainer.fail(with: error, status: status)
 
 
       // Close the channel.
       // Close the channel.
       channel.close(mode: .all, promise: nil)
       channel.close(mode: .all, promise: nil)
@@ -502,7 +506,7 @@ extension ChannelTransport: ClientCallInbound {
         // We're not really failing the status here; in some cases the server may fast fail, in which
         // We're not really failing the status here; in some cases the server may fast fail, in which
         // case we'll only see trailing metadata and status: we should fail the initial metadata and
         // case we'll only see trailing metadata and status: we should fail the initial metadata and
         // response in that case.
         // response in that case.
-        self.responseContainer.fail(with: status)
+        self.responseContainer.fail(with: status, status: status)
       }
       }
 
 
     case .closed:
     case .closed:

+ 4 - 4
Sources/GRPC/ClientCalls/ResponsePartContainer.swift

@@ -37,16 +37,16 @@ internal struct ResponsePartContainer<Response: GRPCPayload> {
 
 
   /// Fail all promises - except for the status promise - with the given error status. Succeed the
   /// Fail all promises - except for the status promise - with the given error status. Succeed the
   /// status promise.
   /// status promise.
-  mutating func fail(with status: GRPCStatus) {
-    self.lazyInitialMetadataPromise.fail(status)
+  mutating func fail(with error: Error, status: GRPCStatus) {
+    self.lazyInitialMetadataPromise.fail(error)
 
 
     switch self.responseHandler {
     switch self.responseHandler {
     case .unary(let response):
     case .unary(let response):
-      response.fail(status)
+      response.fail(error)
     case .stream:
     case .stream:
       ()
       ()
     }
     }
-    self.lazyTrailingMetadataPromise.fail(status)
+    self.lazyTrailingMetadataPromise.fail(error)
     // We always succeed the status.
     // We always succeed the status.
     self.lazyStatusPromise.succeed(status)
     self.lazyStatusPromise.succeed(status)
   }
   }

+ 30 - 0
Tests/GRPCTests/ChannelTransportTests.swift

@@ -340,6 +340,36 @@ class ChannelTransportTests: GRPCTestCase {
     // Promise should fail.
     // Promise should fail.
     XCTAssertThrowsError(try requestHeadPromise.futureResult.wait())
     XCTAssertThrowsError(try requestHeadPromise.futureResult.wait())
   }
   }
+
+  func testErrorsAreNotAlwaysStatus() throws {
+    let channel = EmbeddedChannel()
+    let responsePromise = channel.eventLoop.makePromise(of: Response.self)
+    let container = ResponsePartContainer<Response>(
+      eventLoop: channel.eventLoop,
+      unaryResponsePromise: responsePromise
+    )
+
+    let transport = self.makeEmbeddedTransport(channel: channel, container: container)
+    transport.activate(stream: channel)
+
+    // Send an error
+    transport.receiveError(GRPCError.RPCCancelledByClient())
+
+    XCTAssertThrowsError(try transport.responseContainer.lazyInitialMetadataPromise.getFutureResult().wait()) { error in
+      XCTAssertTrue(error is GRPCError.RPCCancelledByClient)
+    }
+
+    XCTAssertThrowsError(try transport.responseContainer.lazyTrailingMetadataPromise.getFutureResult().wait()) { error in
+      XCTAssertTrue(error is GRPCError.RPCCancelledByClient)
+    }
+
+    XCTAssertThrowsError(try responsePromise.futureResult.wait()) { error in
+      XCTAssertTrue(error is GRPCError.RPCCancelledByClient)
+    }
+
+    // Status never fails.
+    XCTAssertNoThrow(try transport.responseContainer.lazyStatusPromise.getFutureResult().wait())
+  }
 }
 }
 
 
 extension _GRPCClientRequestPart {
 extension _GRPCClientRequestPart {

+ 2 - 2
Tests/GRPCTests/ClientCancellingTests.swift

@@ -27,7 +27,7 @@ class ClientCancellingTests: EchoTestCaseBase {
     call.cancel(promise: nil)
     call.cancel(promise: nil)
 
 
     call.response.whenFailure { error in
     call.response.whenFailure { error in
-      XCTAssertEqual((error as? GRPCStatus)?.code, .cancelled)
+      XCTAssertTrue(error is GRPCError.RPCCancelledByClient)
       responseReceived.fulfill()
       responseReceived.fulfill()
     }
     }
 
 
@@ -47,7 +47,7 @@ class ClientCancellingTests: EchoTestCaseBase {
     call.cancel(promise: nil)
     call.cancel(promise: nil)
 
 
     call.response.whenFailure { error in
     call.response.whenFailure { error in
-      XCTAssertEqual((error as? GRPCStatus)?.code, .cancelled)
+      XCTAssertTrue(error is GRPCError.RPCCancelledByClient)
       responseReceived.fulfill()
       responseReceived.fulfill()
     }
     }
 
 

+ 4 - 8
Tests/GRPCTests/ClientTimeoutTests.swift

@@ -50,17 +50,13 @@ class ClientTimeoutTests: GRPCTestCase {
     XCTAssertNoThrow(try self.channel.finish())
     XCTAssertNoThrow(try self.channel.finish())
   }
   }
 
 
-  func assertDeadlineExceeded(_ response: EventLoopFuture<Echo_EchoResponse>, expectation: XCTestExpectation) {
+  func assertRPCTimedOut(_ response: EventLoopFuture<Echo_EchoResponse>, expectation: XCTestExpectation) {
     response.whenComplete { result in
     response.whenComplete { result in
       switch result {
       switch result {
       case .success(let response):
       case .success(let response):
         XCTFail("unexpected response: \(response)")
         XCTFail("unexpected response: \(response)")
       case .failure(let error):
       case .failure(let error):
-        if let status = error as? GRPCStatus {
-          XCTAssertEqual(status.code, .deadlineExceeded)
-        } else {
-          XCTFail("unexpected error: \(error)")
-        }
+        XCTAssertTrue(error is GRPCError.RPCTimedOut)
       }
       }
       expectation.fulfill()
       expectation.fulfill()
     }
     }
@@ -105,7 +101,7 @@ class ClientTimeoutTests: GRPCTestCase {
     let call = client.collect()
     let call = client.collect()
     channel.embeddedEventLoop.advanceTime(by: self.timeout)
     channel.embeddedEventLoop.advanceTime(by: self.timeout)
 
 
-    self.assertDeadlineExceeded(call.response, expectation: responseExpectation)
+    self.assertRPCTimedOut(call.response, expectation: responseExpectation)
     self.assertDeadlineExceeded(call.status, expectation: statusExpectation)
     self.assertDeadlineExceeded(call.status, expectation: statusExpectation)
     self.wait(for: [responseExpectation, statusExpectation], timeout: self.testTimeout)
     self.wait(for: [responseExpectation, statusExpectation], timeout: self.testTimeout)
   }
   }
@@ -116,7 +112,7 @@ class ClientTimeoutTests: GRPCTestCase {
 
 
     let call = client.collect()
     let call = client.collect()
 
 
-    self.assertDeadlineExceeded(call.response, expectation: responseExpectation)
+    self.assertRPCTimedOut(call.response, expectation: responseExpectation)
     self.assertDeadlineExceeded(call.status, expectation: statusExpectation)
     self.assertDeadlineExceeded(call.status, expectation: statusExpectation)
 
 
     call.sendMessage(Echo_EchoRequest(text: "foo"), promise: nil)
     call.sendMessage(Echo_EchoRequest(text: "foo"), promise: nil)

+ 1 - 0
Tests/GRPCTests/XCTestManifests.swift

@@ -22,6 +22,7 @@ extension ChannelTransportTests {
         ("testBufferedWritesAreFailedOnClose", testBufferedWritesAreFailedOnClose),
         ("testBufferedWritesAreFailedOnClose", testBufferedWritesAreFailedOnClose),
         ("testChannelBecomesInactive", testChannelBecomesInactive),
         ("testChannelBecomesInactive", testChannelBecomesInactive),
         ("testChannelError", testChannelError),
         ("testChannelError", testChannelError),
+        ("testErrorsAreNotAlwaysStatus", testErrorsAreNotAlwaysStatus),
         ("testInboundMethodsAfterShutdown", testInboundMethodsAfterShutdown),
         ("testInboundMethodsAfterShutdown", testInboundMethodsAfterShutdown),
         ("testOutboundMethodsAfterShutdown", testOutboundMethodsAfterShutdown),
         ("testOutboundMethodsAfterShutdown", testOutboundMethodsAfterShutdown),
         ("testTimeoutAfterActivating", testTimeoutAfterActivating),
         ("testTimeoutAfterActivating", testTimeoutAfterActivating),