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:
Cory Benfield 2020-06-17 09:38:07 +01:00 committed by GitHub
parent 2ac8fde712
commit 2937017e27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 58 additions and 1 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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