From 7ca6c120fc7d1e2504543a3b43f8d75580061dad Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Mon, 25 Feb 2019 16:15:06 -0800 Subject: [PATCH 1/5] Add ability to manually shut down channels Addresses https://github.com/grpc/grpc-swift/issues/198. It's currently not possible to manually shut down a gRPC channel, which means that the only way to shut down a channel is to deallocate it. This requirement is a bit fragile for cases where a consumer wants to manually shut down a channel, since it requires confidence that nothing else is holding a strong reference to the channel. This PR: - Adds a `shutdown` function to `Channel`, allowing consumers to arbitrarily shut down connections - Cancels all existing calls that are using the channel upon shutdown - Validates that existing calls will throw errors when attempting to read/write from a previously shut down channel - Ensures creating new calls using previously shut down channels will result in the initializer throwing (already handled by code generators) This change is increasingly relevant because consumers will need to shut down channels and restart them to help mitigate issues when switching between wifi/cellular as mentioned here: https://github.com/grpc/grpc-swift/issues/337. --- Examples/SimpleXcode/Simple/Document.swift | 17 ++-- Sources/Examples/Simple/main.swift | 2 +- Sources/SwiftGRPC/Core/Channel.swift | 46 +++++++-- Sources/SwiftGRPC/Runtime/ClientCall.swift | 19 ++-- Tests/LinuxMain.swift | 1 + .../SwiftGRPCTests/ChannelShutdownTests.swift | 97 +++++++++++++++++++ Tests/SwiftGRPCTests/GRPCTests.swift | 6 +- 7 files changed, 159 insertions(+), 29 deletions(-) create mode 100644 Tests/SwiftGRPCTests/ChannelShutdownTests.swift diff --git a/Examples/SimpleXcode/Simple/Document.swift b/Examples/SimpleXcode/Simple/Document.swift index 7bf8de8bf..aeded6892 100644 --- a/Examples/SimpleXcode/Simple/Document.swift +++ b/Examples/SimpleXcode/Simple/Document.swift @@ -144,16 +144,17 @@ class Document: NSDocument { if !self.isRunning() { break } - let method = (i < steps) ? "/hello" : "/quit" - let call = self.channel.makeCall(method) - - let metadata = try! Metadata([ - "x": "xylophone", - "y": "yu", - "z": "zither" - ]) do { + let method = (i < steps) ? "/hello" : "/quit" + let call = try self.channel.makeCall(method) + + let metadata = try Metadata([ + "x": "xylophone", + "y": "yu", + "z": "zither" + ]) + try call.start(.unary, metadata: metadata, message: messageData) { callResult in diff --git a/Sources/Examples/Simple/main.swift b/Sources/Examples/Simple/main.swift index 2840f1729..2d349a6d7 100644 --- a/Sources/Examples/Simple/main.swift +++ b/Sources/Examples/Simple/main.swift @@ -30,7 +30,7 @@ func client() throws { let method = (i < steps - 1) ? "/hello" : "/quit" print("calling " + method) - let call = c.makeCall(method) + let call = try! c.makeCall(method) let metadata = try Metadata([ "x": "xylophone", diff --git a/Sources/SwiftGRPC/Core/Channel.swift b/Sources/SwiftGRPC/Core/Channel.swift index 4fffdf704..bad2ce4ec 100644 --- a/Sources/SwiftGRPC/Core/Channel.swift +++ b/Sources/SwiftGRPC/Core/Channel.swift @@ -21,12 +21,16 @@ import Foundation /// A gRPC Channel public class Channel { private let mutex = Mutex() + /// Weak references to API calls using this channel that are in-flight + private let activeCalls = NSHashTable.weakObjects() /// Pointer to underlying C representation private let underlyingChannel: UnsafeMutableRawPointer /// Completion queue for channel call operations private let completionQueue: CompletionQueue /// Observer for connectivity state changes. Created lazily if needed private var connectivityObserver: ConnectivityObserver? + /// Whether the gRPC channel has been shut down + private var hasBeenShutdown = false /// Timeout for new calls public var timeout: TimeInterval = 600.0 @@ -34,6 +38,14 @@ public class Channel { /// Default host to use for new calls public var host: String + /// Errors that may be thrown by the channel + enum Error: Swift.Error { + /// Action cannot be performed because the channel has already been shut down + case alreadyShutdown + /// Failed to create a new call within the gRPC stack + case callCreationFailed + } + /// Initializes a gRPC channel /// /// - Parameter address: the address of the server to be called @@ -94,12 +106,21 @@ public class Channel { completionQueue.run() // start a loop that watches the channel's completion queue } - deinit { + /// Shut down the channel. No new calls may be made using this channel after it is shut down. Any in-flight calls using this channel will be canceled + public func shutdown() { self.mutex.synchronize { + guard !self.hasBeenShutdown else { return } + + self.hasBeenShutdown = true self.connectivityObserver?.shutdown() + cgrpc_channel_destroy(self.underlyingChannel) + self.completionQueue.shutdown() + self.activeCalls.allObjects.forEach { $0.cancel() } } - cgrpc_channel_destroy(self.underlyingChannel) - self.completionQueue.shutdown() + } + + deinit { + self.shutdown() } /// Constructs a Call object to make a gRPC API call @@ -108,11 +129,20 @@ public class Channel { /// - Parameter host: the gRPC host name for the call. If unspecified, defaults to the Client host /// - Parameter timeout: a timeout value in seconds /// - Returns: a Call object that can be used to perform the request - public func makeCall(_ method: String, host: String = "", timeout: TimeInterval? = nil) -> Call { - let host = host.isEmpty ? self.host : host - let timeout = timeout ?? self.timeout - let underlyingCall = cgrpc_channel_create_call(underlyingChannel, method, host, timeout)! - return Call(underlyingCall: underlyingCall, owned: true, completionQueue: completionQueue) + public func makeCall(_ method: String, host: String? = nil, timeout: TimeInterval? = nil) throws -> Call { + guard (self.mutex.synchronize { !self.hasBeenShutdown }) else { + throw Error.alreadyShutdown + } + + guard let underlyingCall = cgrpc_channel_create_call( + self.underlyingChannel, method, host ?? self.host, timeout ?? self.timeout) else + { + throw Error.callCreationFailed + } + + let call = Call(underlyingCall: underlyingCall, owned: true, completionQueue: self.completionQueue) + self.mutex.synchronize { self.activeCalls.add(call) } + return call } /// Check the current connectivity state diff --git a/Sources/SwiftGRPC/Runtime/ClientCall.swift b/Sources/SwiftGRPC/Runtime/ClientCall.swift index 8473c92a1..48f4725d2 100644 --- a/Sources/SwiftGRPC/Runtime/ClientCall.swift +++ b/Sources/SwiftGRPC/Runtime/ClientCall.swift @@ -13,27 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import Dispatch -import Foundation import SwiftProtobuf public protocol ClientCall: class { static var method: String { get } - + /// Cancel the call. func cancel() } -open class ClientCallBase: ClientCall { +open class ClientCallBase { open class var method: String { fatalError("needs to be overridden") } public let call: Call /// Create a call. - public init(_ channel: Channel) { - call = channel.makeCall(type(of: self).method) + public init(_ channel: Channel) throws { + self.call = try channel.makeCall(type(of: self).method) + } +} + +extension ClientCallBase: ClientCall { + public func cancel() { + self.call.cancel() } - - public func cancel() { call.cancel() } } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 91d0a9807..0cc7c5c78 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -22,6 +22,7 @@ XCTMain([ testCase(gRPCTests.allTests), testCase(ChannelArgumentTests.allTests), testCase(ChannelConnectivityTests.allTests), + testCase(ChannelShutdownTests.allTests), testCase(ClientCancellingTests.allTests), testCase(ClientTestExample.allTests), testCase(ClientTimeoutTests.allTests), diff --git a/Tests/SwiftGRPCTests/ChannelShutdownTests.swift b/Tests/SwiftGRPCTests/ChannelShutdownTests.swift new file mode 100644 index 000000000..92dee642a --- /dev/null +++ b/Tests/SwiftGRPCTests/ChannelShutdownTests.swift @@ -0,0 +1,97 @@ +/* + * Copyright 2018, gRPC Authors All rights reserved. + * + * 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 + * + * http://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. + */ +@testable import SwiftGRPC +import XCTest + +final class ChannelShutdownTests: BasicEchoTestCase { + static var allTests: [(String, (ChannelShutdownTests) -> () throws -> Void)] { + return [ + ("testThrowsWhenCreatingCallWithAlreadyShutDownChannel", testThrowsWhenCreatingCallWithAlreadyShutDownChannel), + ("testCallReceiveThrowsWhenChannelIsShutDown", testCallReceiveThrowsWhenChannelIsShutDown), + ("testCallCloseThrowsWhenChannelIsShutDown", testCallCloseThrowsWhenChannelIsShutDown), + ("testCallCloseAndReceiveThrowsWhenChannelIsShutDown", testCallCloseAndReceiveThrowsWhenChannelIsShutDown), + ("testCallSendThrowsWhenChannelIsShutDown", testCallSendThrowsWhenChannelIsShutDown), + ("testCancelsActiveCallWhenShutdownIsCalled", testCancelsActiveCallWhenShutdownIsCalled), + ] + } +} + +extension ChannelShutdownTests { + func testThrowsWhenCreatingCallWithAlreadyShutDownChannel() { + self.client.channel.shutdown() + + XCTAssertThrowsError(try self.client.channel.makeCall("foobar")) { error in + XCTAssertEqual(.alreadyShutdown, error as? Channel.Error) + } + } + + func testCallReceiveThrowsWhenChannelIsShutDown() { + let call = try! self.client.channel.makeCall("foo") + self.client.channel.shutdown() + + XCTAssertThrowsError(try call.receiveMessage(completion: { _ in })) { error in + XCTAssertEqual(.completionQueueShutdown, error as? CallError) + XCTAssertNotNil(self.client.channel) + } + } + + func testCallCloseThrowsWhenChannelIsShutDown() { + let call = try! self.client.channel.makeCall("foo") + self.client.channel.shutdown() + + XCTAssertThrowsError(try call.close()) { error in + XCTAssertEqual(.completionQueueShutdown, error as? CallError) + XCTAssertNotNil(self.client.channel) + } + } + + func testCallCloseAndReceiveThrowsWhenChannelIsShutDown() { + let call = try! self.client.channel.makeCall("foo") + self.client.channel.shutdown() + + XCTAssertThrowsError(try call.closeAndReceiveMessage(completion: { _ in })) { error in + XCTAssertEqual(.completionQueueShutdown, error as? CallError) + XCTAssertNotNil(self.client.channel) + } + } + + func testCallSendThrowsWhenChannelIsShutDown() { + let call = try! self.client.channel.makeCall("foo") + self.client.channel.shutdown() + + XCTAssertThrowsError(try call.sendMessage(data: Data())) { error in + XCTAssertEqual(.completionQueueShutdown, error as? CallError) + XCTAssertNotNil(self.client.channel) + } + } + + func testCancelsActiveCallWhenShutdownIsCalled() { + let errorExpectation = self.expectation(description: "error is returned to call when channel is shut down") + let call = try! self.client.channel.makeCall("foo") + + try! call.receiveMessage { result in + XCTAssertFalse(result.success) + errorExpectation.fulfill() + } + + self.client.channel.shutdown() + self.waitForExpectations(timeout: 0.1) + + XCTAssertThrowsError(try call.close()) { error in + XCTAssertEqual(.completionQueueShutdown, error as? CallError) + } + } +} diff --git a/Tests/SwiftGRPCTests/GRPCTests.swift b/Tests/SwiftGRPCTests/GRPCTests.swift index aadd4b8df..546fb37f8 100644 --- a/Tests/SwiftGRPCTests/GRPCTests.swift +++ b/Tests/SwiftGRPCTests/GRPCTests.swift @@ -177,7 +177,7 @@ func callUnary(channel: Channel) throws { func callUnaryIndividual(channel: Channel, message: Data, shouldSucceed: Bool) throws { let sem = DispatchSemaphore(value: 0) let method = hello - let call = channel.makeCall(method) + let call = try channel.makeCall(method) let metadata = try Metadata(initialClientMetadata) try call.start(.unary, metadata: metadata, message: message) { response in @@ -228,7 +228,7 @@ func callServerStream(channel: Channel) throws { let sem = DispatchSemaphore(value: 0) let method = helloServerStream - let call = channel.makeCall(method) + let call = try channel.makeCall(method) try call.start(.serverStreaming, metadata: metadata, message: message) { response in @@ -270,7 +270,7 @@ func callBiDiStream(channel: Channel) throws { let sem = DispatchSemaphore(value: 0) let method = helloBiDiStream - let call = channel.makeCall(method) + let call = try channel.makeCall(method) try call.start(.bidiStreaming, metadata: metadata, message: nil) { response in From 744315152e6c8e1d53a8b146511959e6f2833582 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Wed, 27 Feb 2019 09:46:31 -0800 Subject: [PATCH 2/5] CR --- Sources/SwiftGRPC/Core/Channel.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftGRPC/Core/Channel.swift b/Sources/SwiftGRPC/Core/Channel.swift index bad2ce4ec..77a5d9808 100644 --- a/Sources/SwiftGRPC/Core/Channel.swift +++ b/Sources/SwiftGRPC/Core/Channel.swift @@ -130,18 +130,19 @@ public class Channel { /// - Parameter timeout: a timeout value in seconds /// - Returns: a Call object that can be used to perform the request public func makeCall(_ method: String, host: String? = nil, timeout: TimeInterval? = nil) throws -> Call { - guard (self.mutex.synchronize { !self.hasBeenShutdown }) else { + self.mutex.lock() + defer { self.mutex.unlock() } + + guard !self.hasBeenShutdown else { throw Error.alreadyShutdown } guard let underlyingCall = cgrpc_channel_create_call( - self.underlyingChannel, method, host ?? self.host, timeout ?? self.timeout) else - { - throw Error.callCreationFailed - } + self.underlyingChannel, method, host ?? self.host, timeout ?? self.timeout) + else { throw Error.callCreationFailed } let call = Call(underlyingCall: underlyingCall, owned: true, completionQueue: self.completionQueue) - self.mutex.synchronize { self.activeCalls.add(call) } + self.activeCalls.add(call) return call } From 3ee3cce2175da555478879c4770f695d5190299e Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Wed, 27 Feb 2019 10:55:32 -0800 Subject: [PATCH 3/5] try! -> try --- Sources/Examples/Simple/main.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Examples/Simple/main.swift b/Sources/Examples/Simple/main.swift index 2d349a6d7..e935e0b6b 100644 --- a/Sources/Examples/Simple/main.swift +++ b/Sources/Examples/Simple/main.swift @@ -30,7 +30,7 @@ func client() throws { let method = (i < steps - 1) ? "/hello" : "/quit" print("calling " + method) - let call = try! c.makeCall(method) + let call = try c.makeCall(method) let metadata = try Metadata([ "x": "xylophone", @@ -38,7 +38,7 @@ func client() throws { "z": "zither" ]) - try! call.start(.unary, metadata: metadata, message: message) { + try call.start(.unary, metadata: metadata, message: message) { response in print("status:", response.statusCode) print("statusMessage:", response.statusMessage!) From 63d8e3ed0812641c574e15ee9fd807a142fb72a4 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Wed, 27 Feb 2019 14:12:28 -0800 Subject: [PATCH 4/5] support linux --- Sources/SwiftGRPC/Core/Channel.swift | 77 ++++++++++++++++++---------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/Sources/SwiftGRPC/Core/Channel.swift b/Sources/SwiftGRPC/Core/Channel.swift index 77a5d9808..69f4955f6 100644 --- a/Sources/SwiftGRPC/Core/Channel.swift +++ b/Sources/SwiftGRPC/Core/Channel.swift @@ -21,12 +21,12 @@ import Foundation /// A gRPC Channel public class Channel { private let mutex = Mutex() - /// Weak references to API calls using this channel that are in-flight - private let activeCalls = NSHashTable.weakObjects() /// Pointer to underlying C representation private let underlyingChannel: UnsafeMutableRawPointer /// Completion queue for channel call operations private let completionQueue: CompletionQueue + /// Weak references to API calls using this channel that are in-flight + private var activeCalls = [WeakReference]() /// Observer for connectivity state changes. Created lazily if needed private var connectivityObserver: ConnectivityObserver? /// Whether the gRPC channel has been shut down @@ -51,39 +51,32 @@ public class Channel { /// - Parameter address: the address of the server to be called /// - Parameter secure: if true, use TLS /// - Parameter arguments: list of channel configuration options - public init(address: String, secure: Bool = true, arguments: [Argument] = []) { + public convenience init(address: String, secure: Bool = true, arguments: [Argument] = []) { gRPC.initialize() - host = address - let argumentWrappers = arguments.map { $0.toCArg() } - underlyingChannel = withExtendedLifetime(argumentWrappers) { + let argumentWrappers = arguments.map { $0.toCArg() } + self.init(host: address, underlyingChannel: withExtendedLifetime(argumentWrappers) { var argumentValues = argumentWrappers.map { $0.wrapped } if secure { return cgrpc_channel_create_secure(address, kRootCertificates, nil, nil, &argumentValues, Int32(arguments.count)) } else { return cgrpc_channel_create(address, &argumentValues, Int32(arguments.count)) } - } - completionQueue = CompletionQueue(underlyingCompletionQueue: cgrpc_channel_completion_queue(underlyingChannel), name: "Client") - completionQueue.run() // start a loop that watches the channel's completion queue + }) } /// Initializes a gRPC channel /// /// - Parameter address: the address of the server to be called /// - Parameter arguments: list of channel configuration options - public init(googleAddress: String, arguments: [Argument] = []) { + public convenience init(googleAddress: String, arguments: [Argument] = []) { gRPC.initialize() - host = googleAddress - let argumentWrappers = arguments.map { $0.toCArg() } - underlyingChannel = withExtendedLifetime(argumentWrappers) { + let argumentWrappers = arguments.map { $0.toCArg() } + self.init(host: googleAddress, underlyingChannel: withExtendedLifetime(argumentWrappers) { var argumentValues = argumentWrappers.map { $0.wrapped } return cgrpc_channel_create_google(googleAddress, &argumentValues, Int32(arguments.count)) - } - - completionQueue = CompletionQueue(underlyingCompletionQueue: cgrpc_channel_completion_queue(underlyingChannel), name: "Client") - completionQueue.run() // start a loop that watches the channel's completion queue + }) } /// Initializes a gRPC channel @@ -93,17 +86,14 @@ public class Channel { /// - Parameter clientCertificates: a PEM representation of the client certificates to use /// - Parameter clientKey: a PEM representation of the client key to use /// - Parameter arguments: list of channel configuration options - public init(address: String, certificates: String = kRootCertificates, clientCertificates: String? = nil, clientKey: String? = nil, arguments: [Argument] = []) { + public convenience init(address: String, certificates: String = kRootCertificates, clientCertificates: String? = nil, clientKey: String? = nil, arguments: [Argument] = []) { gRPC.initialize() - host = address - let argumentWrappers = arguments.map { $0.toCArg() } - underlyingChannel = withExtendedLifetime(argumentWrappers) { + let argumentWrappers = arguments.map { $0.toCArg() } + self.init(host: address, underlyingChannel: withExtendedLifetime(argumentWrappers) { var argumentValues = argumentWrappers.map { $0.wrapped } return cgrpc_channel_create_secure(address, certificates, clientCertificates, clientKey, &argumentValues, Int32(arguments.count)) - } - completionQueue = CompletionQueue(underlyingCompletionQueue: cgrpc_channel_completion_queue(underlyingChannel), name: "Client") - completionQueue.run() // start a loop that watches the channel's completion queue + }) } /// Shut down the channel. No new calls may be made using this channel after it is shut down. Any in-flight calls using this channel will be canceled @@ -115,7 +105,7 @@ public class Channel { self.connectivityObserver?.shutdown() cgrpc_channel_destroy(self.underlyingChannel) self.completionQueue.shutdown() - self.activeCalls.allObjects.forEach { $0.cancel() } + self.activeCalls.forEach { $0.value?.cancel() } } } @@ -142,7 +132,7 @@ public class Channel { else { throw Error.callCreationFailed } let call = Call(underlyingCall: underlyingCall, owned: true, completionQueue: self.completionQueue) - self.activeCalls.add(call) + self.activeCalls.append(WeakReference(value: call)) return call } @@ -170,4 +160,39 @@ public class Channel { observer.addConnectivityObserver(callback: callback) } } + + // MARK: - Private + + private init(host: String, underlyingChannel: UnsafeMutableRawPointer) { + self.host = host + self.underlyingChannel = underlyingChannel + self.completionQueue = CompletionQueue(underlyingCompletionQueue: cgrpc_channel_completion_queue(underlyingChannel), + name: "Client") + + self.completionQueue.run() + self.scheduleActiveCallCleanUp() + } + + private func scheduleActiveCallCleanUp() { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 10.0) { [weak self] in + self?.cleanUpActiveCalls() + } + } + + private func cleanUpActiveCalls() { + self.mutex.synchronize { + self.activeCalls = self.activeCalls.filter { $0.value != nil } + } + self.scheduleActiveCallCleanUp() + } +} + +/// Used to hold weak references to objects since `NSHashTable.weakObjects()` isn't available on Linux. +/// If/when this type becomes available on Linux, this should be replaced. +private final class WeakReference { + weak var value: T? + + init(value: T) { + self.value = value + } } From fc1327ee1115444037f3b92198998a4dd7198d77 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Thu, 28 Feb 2019 06:59:28 -0800 Subject: [PATCH 5/5] CR --- Sources/SwiftGRPC/Core/Channel.swift | 20 +++++++++---------- .../SwiftGRPCTests/ChannelShutdownTests.swift | 8 ++------ 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Sources/SwiftGRPC/Core/Channel.swift b/Sources/SwiftGRPC/Core/Channel.swift index 69f4955f6..baa3417ff 100644 --- a/Sources/SwiftGRPC/Core/Channel.swift +++ b/Sources/SwiftGRPC/Core/Channel.swift @@ -18,6 +18,16 @@ import CgRPC #endif import Foundation +/// Used to hold weak references to objects since `NSHashTable.weakObjects()` isn't available on Linux. +/// If/when this type becomes available on Linux, this should be replaced. +private final class WeakReference { + private(set) weak var value: T? + + init(value: T) { + self.value = value + } +} + /// A gRPC Channel public class Channel { private let mutex = Mutex() @@ -186,13 +196,3 @@ public class Channel { self.scheduleActiveCallCleanUp() } } - -/// Used to hold weak references to objects since `NSHashTable.weakObjects()` isn't available on Linux. -/// If/when this type becomes available on Linux, this should be replaced. -private final class WeakReference { - weak var value: T? - - init(value: T) { - self.value = value - } -} diff --git a/Tests/SwiftGRPCTests/ChannelShutdownTests.swift b/Tests/SwiftGRPCTests/ChannelShutdownTests.swift index 92dee642a..40602a069 100644 --- a/Tests/SwiftGRPCTests/ChannelShutdownTests.swift +++ b/Tests/SwiftGRPCTests/ChannelShutdownTests.swift @@ -42,9 +42,8 @@ extension ChannelShutdownTests { let call = try! self.client.channel.makeCall("foo") self.client.channel.shutdown() - XCTAssertThrowsError(try call.receiveMessage(completion: { _ in })) { error in + XCTAssertThrowsError(try call.receiveMessage { _ in }) { error in XCTAssertEqual(.completionQueueShutdown, error as? CallError) - XCTAssertNotNil(self.client.channel) } } @@ -54,7 +53,6 @@ extension ChannelShutdownTests { XCTAssertThrowsError(try call.close()) { error in XCTAssertEqual(.completionQueueShutdown, error as? CallError) - XCTAssertNotNil(self.client.channel) } } @@ -62,9 +60,8 @@ extension ChannelShutdownTests { let call = try! self.client.channel.makeCall("foo") self.client.channel.shutdown() - XCTAssertThrowsError(try call.closeAndReceiveMessage(completion: { _ in })) { error in + XCTAssertThrowsError(try call.closeAndReceiveMessage { _ in }) { error in XCTAssertEqual(.completionQueueShutdown, error as? CallError) - XCTAssertNotNil(self.client.channel) } } @@ -74,7 +71,6 @@ extension ChannelShutdownTests { XCTAssertThrowsError(try call.sendMessage(data: Data())) { error in XCTAssertEqual(.completionQueueShutdown, error as? CallError) - XCTAssertNotNil(self.client.channel) } }