Skip to content

Conversation

@glbrntt
Copy link
Collaborator

@glbrntt glbrntt commented Jun 19, 2019

Motivation:

Clients could lose their connections to the server for a number of
reasons, we offered no means to automatically reconnect.

Modification:

  • ClientConnection now stores a future Channel and future multiplexer,
    as opposed to future ClientConnections holding a channel and
    multiplexer.
  • This allows us to replace the future channel and future multiplexer
    when the client is closed (but not explicitly via close()).
  • The state can be monitored via a delegate or by registering callbacks
    on the next transition to a given state.
  • Reconnection uses the same backoff logic used for the initial
    connection creation.

Results:

Clients can automatically reconnect when their connection goes away.

/// - Parameter configuration: The configuration to start the connection with.
public class func start(_ configuration: Configuration) -> EventLoopFuture<ClientConnection> {
return start(configuration, backoffIterator: configuration.connectionBackoff?.makeIterator())
/// The `EventLoop` this connection is using. Note that this _may_ change over time.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of allowing this to change; could we re-use the previous channel's event loop instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean re-use it for the new channel? Or store it and always return it here? Because the former isn't possible.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping for the former; why would that be a problem?

(My concern is that Vapor does a lot of thread-local stuff and generally expects all "sub-requests" to run on the same event loop the original request is handled on. Open to keep this as-is for now, though, given that one could simply pass in the request's EventLoop as the client's EventLoopGroup argument.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take that back -- I didn't realise EventLoop conformed to EventLoopGroup!

/// - Parameter state: The state on which to call the given callback.
/// - Parameter callback: The closure to call once the given state has been transitioned to. The
/// `callback` can be removed by passing in `nil`.
public func onNext(state: ConnectivityState, callback: Callback?) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for as opposed to just providing a delegate, and why is the callback reset after every change to that state?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question; my take on it is that they're useful for different periods of time. You can achieve exactly the same with a delegate but in some cases this is just much more convenient (the tests are a good example of this). The delegate is useful for longer time periods (logging/monitoring). This is useful for single events (e.g. my connection has shutdown, I can now update my UI) which is why it resets after each change. Definitely open to not resetting after each change though as I realise that's a slightly strange decision!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if we don't reset, we could have the user pass in a closure instead that could switch over the states they are interested in and discard the rest via default:.

At that point, one could also argue over removing the delegate and only letting the user provide callback closures. And at that point we could even keep an array of closures, in case the user wants more than one observer.

To be honest, having two different ways of passing in callback closures feels a bit weird right now, but this is not a blocker. We can revisit it later (before 1.0).

self.wait(for: [reconnectionReady], timeout: 1.0)
XCTAssertEqual(self.stateDelegate.clearStates(), [.connecting, .ready])

// Ensure we can actually make a call.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also test making a call after the first server has shut down, but before the second one has been started?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, but the reconnect will already be taking place: if we wait() on that call to complete (before starting the server) it will eventually time out the reconnect (and cause a .shutdown).

If we don't wait() on it, then it will eventually connect and that call will succeed.

I'll add the former as a separate test.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't wait() on it, then it will eventually connect and that call will succeed.

I think that's what we should do — start the call while the server is offline, then wait() for it to succeed after the server is back up.

glbrntt added 2 commits June 21, 2019 14:52
Motivation:

Clients could lose their connections to the server for a number of
reasons, we offered no means to automatically reconnect.

Modification:

- `ClientConnection` now stores a future Channel and future multiplexer,
  as opposed to future `ClientConnection`s holding a channel and
  multiplexer.
- This allows us to replace the future channel and future multiplexer
  when the client is closed (but not explicitly via `close()`).
- The state can be monitored via a delegate or by registering callbacks
  on the next transition to a given state.
- Reconnection uses the same backoff logic used for the initial
  connection creation.

Results:

Clients can automatically reconnect when their connection goes away.
@MrMage MrMage merged commit 36e1e99 into grpc:nio Jun 21, 2019
@glbrntt glbrntt deleted the reconnect branch July 10, 2019 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants