-
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 1 commit
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 | ||
---|---|---|---|---|
|
@@ -34,7 +34,7 @@ public class Channel { | |||
public var host: String | ||||
|
||||
/// Connectivity state observers | ||||
private var observers: [ConnectivityObserver] = [] | ||||
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() | ||||
} | ||||
|
@@ -86,83 +85,77 @@ public class Channel { | |||
return ConnectivityState.connectivityState(cgrpc_channel_check_connectivity_state(underlyingChannel, tryToConnect ? 1 : 0)) | ||||
} | ||||
|
||||
public func subscribe(sourceState: ConnectivityState, tryToConnect: Bool = false, callback: @escaping (ConnectivityState) -> ()) { | ||||
var observer = observers.first(where: { $0.state == sourceState }) | ||||
|
||||
if observer == nil { | ||||
let newObserver = ConnectivityObserver(state: sourceState, underlyingChannel: underlyingChannel, tryToConnect: tryToConnect) | ||||
observers.append(newObserver) | ||||
observer = newObserver | ||||
} | ||||
|
||||
observer?.callbacks.append(callback) | ||||
observer?.polling = true | ||||
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: Equatable { | ||||
let state: ConnectivityState | ||||
let queue: CompletionQueue | ||||
let underlyingChannel: UnsafeMutableRawPointer | ||||
let underlyingCompletionQueue: UnsafeMutableRawPointer | ||||
private(set) var tryToConnect: Bool | ||||
var callbacks: [(ConnectivityState) -> ()] = [] | ||||
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 { | ||||
if polling == true && oldValue == false { | ||||
run() | ||||
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(state: ConnectivityState, underlyingChannel: UnsafeMutableRawPointer, tryToConnect: Bool) { | ||||
self.state = state | ||||
init(underlyingChannel: UnsafeMutableRawPointer, callback: @escaping (ConnectivityState) -> ()) { | ||||
self.underlyingChannel = underlyingChannel | ||||
self.tryToConnect = tryToConnect | ||||
self.underlyingCompletionQueue = cgrpc_completion_queue_create_for_next() | ||||
self.queue = CompletionQueue(underlyingCompletionQueue: self.underlyingCompletionQueue, name: "Connectivity State") | ||||
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 { | ||||
queue.shutdown() | ||||
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, let underlyingState = self.lastState.underlyingState else { return } | ||||
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 !self.callbacks.isEmpty && !self.tryToConnect else { | ||||
self.polling = false | ||||
break | ||||
} | ||||
|
||||
defer { self.tryToConnect = false } | ||||
|
||||
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.queue.wait(timeout: deadline) | ||||
let event = self.completionQueue.wait(timeout: deadline) | ||||
|
||||
if event.success == 1 || self.tryToConnect { | ||||
let newState = ConnectivityState.connectivityState(cgrpc_channel_check_connectivity_state(self.underlyingChannel, self.tryToConnect ? 1 : 0)) | ||||
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 } | ||||
|
||||
if self.lastState == self.state { | ||||
self.callbacks.forEach({ $0(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 |
||||
} | ||||
} | ||||
} | ||||
|
||||
static func == (lhs: ConnectivityObserver, rhs: ConnectivityObserver) -> Bool { | ||||
return lhs.state == rhs.state | ||||
private func shutdown() { | ||||
completionQueue.shutdown() | ||||
} | ||||
} | ||||
} | ||||
|
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
.