-
Notifications
You must be signed in to change notification settings - Fork 420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement connectivity state property and observers #196
Changes from 3 commits
0bd14d8
76faf09
255ce88
4e94ef4
0891482
e853032
4ca615a
ad6e022
0d25ef8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -15,6 +15,7 @@ | |||
*/ | ||||
#if SWIFT_PACKAGE | ||||
import CgRPC | ||||
import Dispatch | ||||
#endif | ||||
import Foundation | ||||
|
||||
|
@@ -31,10 +32,9 @@ public class Channel { | |||
|
||||
/// Default host to use for new calls | ||||
public var host: String | ||||
|
||||
public var connectivityState: ConnectivityState? { | ||||
return ConnectivityState.fromCEnum(cgrpc_channel_check_connectivity_state(underlyingChannel, 0)) | ||||
} | ||||
|
||||
/// Connectivity state observers | ||||
private var connectivityObservers: [ConnectivityObserver] = [] | ||||
|
||||
/// Initializes a gRPC channel | ||||
/// | ||||
|
@@ -47,8 +47,7 @@ public class Channel { | |||
} else { | ||||
underlyingChannel = cgrpc_channel_create(address) | ||||
} | ||||
completionQueue = CompletionQueue( | ||||
underlyingCompletionQueue: cgrpc_channel_completion_queue(underlyingChannel), name: "Client") | ||||
completionQueue = CompletionQueue(underlyingCompletionQueue: cgrpc_channel_completion_queue(underlyingChannel), name: "Client") | ||||
completionQueue.run() // start a loop that watches the channel's completion queue | ||||
} | ||||
|
||||
|
@@ -59,13 +58,13 @@ public class Channel { | |||
/// - Parameter host: an optional hostname override | ||||
public init(address: String, certificates: String, host: String?) { | ||||
self.host = address | ||||
underlyingChannel = cgrpc_channel_create_secure(address, certificates, host) | ||||
completionQueue = CompletionQueue( | ||||
underlyingCompletionQueue: cgrpc_channel_completion_queue(underlyingChannel), name: "Client") | ||||
underlyingChannel = cgrpc_channel_create_secure(address, certificates, &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 | ||||
} | ||||
|
||||
deinit { | ||||
connectivityObservers.forEach { $0.polling = false } | ||||
cgrpc_channel_destroy(underlyingChannel) | ||||
completionQueue.shutdown() | ||||
} | ||||
|
@@ -81,4 +80,139 @@ public class Channel { | |||
let underlyingCall = cgrpc_channel_create_call(underlyingChannel, method, host, timeout)! | ||||
return Call(underlyingCall: underlyingCall, owned: true, completionQueue: completionQueue) | ||||
} | ||||
|
||||
public func connectivityState(tryToConnect: Bool = false) -> ConnectivityState { | ||||
return ConnectivityState.connectivityState(cgrpc_channel_check_connectivity_state(underlyingChannel, tryToConnect ? 1 : 0)) | ||||
} | ||||
|
||||
public func subscribe(callback: @escaping (ConnectivityState) -> ()) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||||
let observer = ConnectivityObserver(underlyingChannel: underlyingChannel, callback: callback) | ||||
observer.polling = true | ||||
connectivityObservers.append(observer) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In theory there could be race conditions here of |
||||
} | ||||
} | ||||
|
||||
private extension Channel { | ||||
class ConnectivityObserver { | ||||
private let completionQueue: CompletionQueue | ||||
private let underlyingChannel: UnsafeMutableRawPointer | ||||
private let underlyingCompletionQueue: UnsafeMutableRawPointer | ||||
private let callback: (ConnectivityState) -> () | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. super-nit: how about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||||
private var lastState: ConnectivityState | ||||
private let queue: OperationQueue | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please just use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got rid of |
||||
|
||||
var polling: Bool = false { | ||||
didSet { | ||||
queue.addOperation { [weak self] in | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks to me like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got rid of |
||||
guard let `self` = self else { return } | ||||
|
||||
if self.polling == true && oldValue == false { | ||||
self.run() | ||||
} else if self.polling == false && oldValue == true { | ||||
self.shutdown() | ||||
} | ||||
} | ||||
} | ||||
} | ||||
|
||||
init(underlyingChannel: UnsafeMutableRawPointer, callback: @escaping (ConnectivityState) -> ()) { | ||||
self.underlyingChannel = underlyingChannel | ||||
self.underlyingCompletionQueue = cgrpc_completion_queue_create_for_next() | ||||
self.completionQueue = CompletionQueue(underlyingCompletionQueue: self.underlyingCompletionQueue, name: "Connectivity State") | ||||
self.callback = callback | ||||
self.lastState = ConnectivityState.connectivityState(cgrpc_channel_check_connectivity_state(self.underlyingChannel, 0)) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I would add a
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||||
|
||||
queue = OperationQueue() | ||||
queue.maxConcurrentOperationCount = 1 | ||||
queue.qualityOfService = .background | ||||
} | ||||
|
||||
deinit { | ||||
shutdown() | ||||
} | ||||
|
||||
private func run() { | ||||
DispatchQueue.global().async { [weak self] in | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use the thread-spawning pattern now used in e.g.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||||
guard let `self` = self else { return } | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the line that retains To be honest, I'd like to have a test that spins up 100 observers to ensure all their spinloop threads get spun down once the channel closes (i.e. ensure no leaks are happening), but I can understand if you don't want to add one (should be fairly easy, though). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, I think this has not been resolved yet (2). |
||||
|
||||
while self.polling { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got rid of |
||||
guard let underlyingState = self.lastState.underlyingState else { return } | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log an error in this case, as this would be very unexpected? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||||
|
||||
let deadline: TimeInterval = 0.2 | ||||
cgrpc_channel_watch_connectivity_state(self.underlyingChannel, self.underlyingCompletionQueue, underlyingState, deadline, nil) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't |
||||
let event = self.completionQueue.wait(timeout: deadline) | ||||
|
||||
if event.success == 1 { | ||||
let newState = ConnectivityState.connectivityState(cgrpc_channel_check_connectivity_state(self.underlyingChannel, 1)) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure that this should always try to connect? This would make the connectivity observer reconnect all the time, even though the actual channel is currently not used by the client code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, I missed that one. |
||||
|
||||
guard newState != self.lastState else { continue } | ||||
defer { self.lastState = newState } | ||||
|
||||
self.callback(newState) | ||||
} | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest to handle queue shutdown at this point as well and exit the loop in that case. FYI, this loop retains There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might miss something here, but I don't think this loop retains |
||||
} | ||||
} | ||||
} | ||||
|
||||
private func shutdown() { | ||||
completionQueue.shutdown() | ||||
} | ||||
} | ||||
} | ||||
|
||||
extension Channel { | ||||
public enum ConnectivityState { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! This is much better than what I had added before. |
||||
/// Channel has just been initialized | ||||
case initialized | ||||
/// Channel is idle | ||||
case idle | ||||
/// Channel is connecting | ||||
case connecting | ||||
/// Channel is ready for work | ||||
case ready | ||||
/// Channel has seen a failure but expects to recover | ||||
case transientFailure | ||||
/// Channel has seen a failure that it cannot recover from | ||||
case shutdown | ||||
/// Channel connectivity state is unknown | ||||
case unknown | ||||
|
||||
fileprivate static func connectivityState(_ value: grpc_connectivity_state) -> ConnectivityState { | ||||
switch value { | ||||
case GRPC_CHANNEL_INIT: | ||||
return .initialized | ||||
case GRPC_CHANNEL_IDLE: | ||||
return .idle | ||||
case GRPC_CHANNEL_CONNECTING: | ||||
return .connecting | ||||
case GRPC_CHANNEL_READY: | ||||
return .ready | ||||
case GRPC_CHANNEL_TRANSIENT_FAILURE: | ||||
return .transientFailure | ||||
case GRPC_CHANNEL_SHUTDOWN: | ||||
return .shutdown | ||||
default: | ||||
return .unknown | ||||
} | ||||
} | ||||
|
||||
fileprivate var underlyingState: grpc_connectivity_state? { | ||||
switch self { | ||||
case .initialized: | ||||
return GRPC_CHANNEL_INIT | ||||
case .idle: | ||||
return GRPC_CHANNEL_IDLE | ||||
case .connecting: | ||||
return GRPC_CHANNEL_CONNECTING | ||||
case .ready: | ||||
return GRPC_CHANNEL_READY | ||||
case .transientFailure: | ||||
return GRPC_CHANNEL_TRANSIENT_FAILURE | ||||
case .shutdown: | ||||
return GRPC_CHANNEL_SHUTDOWN | ||||
default: | ||||
return nil | ||||
} | ||||
} | ||||
} | ||||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either use a lock for setting
polling
, or use$0.shutdown()
(see the other comments).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got rid of
polling
.