Add user event fired when waiting for connectivity. (#95)
Motivation: On Apple's devices it is considered a best practice to wait for network connectivity in cases when a connection request cannot immediately be satisfied, rather than erroring out. This reflects the dynamic and fluid network environment on Apple devices, with their many network interfaces and complex interactions between radios, VPNs, and network devices. While NIOTS supports this model of interaction (and indeed uses it out of the box), and supports configuring it (by setting `NIOTSChannelOptions.waitForActivity`), a key pillar is missing: observability. Right now we don't actually _tell_ the user when we're waiting for connectivity. This makes it difficult to act on this information. Modifications: - Added a user event, `NIOTSNetworkEvents.WaitingForConnectivity` that is fired when connectivity could not be established, but the situation may change in future. Result: Our users can tell their users when they're waiting for something!
This commit is contained in:
parent
2ac8fde712
commit
2937017e27
|
|
@ -688,6 +688,7 @@ extension NIOTSConnectionChannel {
|
|||
// This means the connection cannot currently be completed. We should notify the pipeline
|
||||
// here, or support this with a channel option or something, but for now for the sake of
|
||||
// demos we will just allow ourselves into this stage.
|
||||
self.pipeline.fireUserInboundEventTriggered(NIOTSNetworkEvents.WaitingForConnectivity(transientError: err))
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,5 +67,18 @@ public enum NIOTSNetworkEvents {
|
|||
/// The endpoint to which we want to bind.
|
||||
public let endpoint: NWEndpoint
|
||||
}
|
||||
|
||||
/// This event is fired when when the OS has informed NIO that it cannot immediately connect
|
||||
/// to the remote endpoint, but that it is possible that changes in network conditions may
|
||||
/// allow connection in future. This can occur in cases where the route is not currently
|
||||
/// satisfiable (e.g. because airplane mode is on, or because the app is forbidden from using cellular)
|
||||
/// but where a change in network state may allow the connection.
|
||||
public struct WaitingForConnectivity: NIOTSNetworkEvent {
|
||||
/// The reason the connection couldn't be established at this time.
|
||||
///
|
||||
/// Note that these reasons are _not fatal_: applications are strongly advised not to treat them
|
||||
/// as fatal, and instead to use them as information to inform UI decisions.
|
||||
public var transientError: NWError
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ final class DisableWaitingAfterConnect: ChannelOutboundHandler {
|
|||
|
||||
final class PromiseOnActiveHandler: ChannelInboundHandler {
|
||||
typealias InboundIn = Any
|
||||
typealias InboudOut = Any
|
||||
typealias InboundOut = Any
|
||||
|
||||
private let promise: EventLoopPromise<Void>
|
||||
|
||||
|
|
@ -102,6 +102,27 @@ final class PromiseOnActiveHandler: ChannelInboundHandler {
|
|||
}
|
||||
}
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, tvOS 12.0, *)
|
||||
final class EventWaiter<Event>: ChannelInboundHandler {
|
||||
typealias InboundIn = Any
|
||||
typealias InboundOut = Any
|
||||
|
||||
private var eventWaiter: EventLoopPromise<Event>?
|
||||
|
||||
init(_ eventWaiter: EventLoopPromise<Event>) {
|
||||
self.eventWaiter = eventWaiter
|
||||
}
|
||||
|
||||
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
|
||||
if let event = event as? Event {
|
||||
var promise = Optional<EventLoopPromise<Event>>.none
|
||||
swap(&promise, &self.eventWaiter)
|
||||
promise?.succeed(event)
|
||||
}
|
||||
context.fireUserInboundEventTriggered(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, tvOS 12.0, *)
|
||||
class NIOTSConnectionChannelTests: XCTestCase {
|
||||
|
|
@ -735,5 +756,27 @@ class NIOTSConnectionChannelTests: XCTestCase {
|
|||
|
||||
XCTAssertNoThrow(try workFuture.wait())
|
||||
}
|
||||
|
||||
func testConnectingInvolvesWaiting() throws {
|
||||
let loop = self.group.next()
|
||||
let eventPromise = loop.makePromise(of: NIOTSNetworkEvents.WaitingForConnectivity.self)
|
||||
let eventRecordingHandler = EventWaiter<NIOTSNetworkEvents.WaitingForConnectivity>(eventPromise)
|
||||
|
||||
let connectBootstrap = NIOTSConnectionBootstrap(group: loop)
|
||||
.channelInitializer { channel in channel.pipeline.addHandler(eventRecordingHandler) }
|
||||
.connectTimeout(.seconds(5)) // This is the worst-case test time: normally it'll be faster as we don't wait for this.
|
||||
|
||||
let target = NWEndpoint.hostPort(host: "example.invalid", port: 80)
|
||||
|
||||
// We don't wait here, as the connect attempt should timeout. If it doesn't, we'll close it.
|
||||
connectBootstrap.connect(endpoint: target).whenSuccess { conn in
|
||||
XCTFail("DNS resolution should have returned NXDOMAIN but did not: DNS hijacking forces this test to fail")
|
||||
conn.close(promise: nil)
|
||||
}
|
||||
|
||||
// We don't actually investigate this because the error is going to be very OS specific. It just mustn't
|
||||
// throw
|
||||
XCTAssertNoThrow(try eventPromise.futureResult.wait())
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Reference in New Issue