Skip to content
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

ConnectAsync and ShutdownAsync refactoring #1434

Merged
merged 31 commits into from
Jun 24, 2022
Merged

ConnectAsync and ShutdownAsync refactoring #1434

merged 31 commits into from
Jun 24, 2022

Conversation

bernardnormier
Copy link
Member

This PR refactors ConnectAsync, ShutdownAsync and DisposeAsync (when applicable) in ClientConnection, ServerConnection, ResumableClientConnection, ConnectionPool and Server.

It's only a partial fix - the full fix requires protocol connection refactoring, in one or more follow-up PRs.

ClientConnection/ServerConnection DisposeAsync is no longer an alias for ShutdownAsync, in particular ShutdownAsync can now throw exceptions and its cancellation token behaves like a normal cancellation token.

See #1413 and #1417.

src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
src/IceRpc/ClientConnection.cs Show resolved Hide resolved
src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
src/IceRpc/ConnectionExceptions.cs Outdated Show resolved Hide resolved
src/IceRpc/ConnectionPool.cs Outdated Show resolved Hide resolved
src/IceRpc/ConnectionPool.cs Outdated Show resolved Hide resolved
src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
src/IceRpc/ConnectionPool.cs Outdated Show resolved Hide resolved
@@ -26,6 +26,11 @@ public interface IConnection
/// <param name="request">The outgoing request being sent.</param>
/// <param name="cancel">The cancellation token.</param>
/// <returns>The corresponding <see cref="IncomingResponse"/>.</returns>
/// <exception cref="ConnectionCanceledException">Thrown if the connection was canceled.</exception>
Copy link
Member

Choose a reason for hiding this comment

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

I find the terminology for "a connection being cancelled" weird. I'm not sure users will understand what this means.

Copy link
Member

@pepone pepone left a comment

Choose a reason for hiding this comment

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

The GenericHost sample needs fixing

src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
catch (OperationCanceledException exception) when (exception.CancellationToken != cancel)
{
// exception comes from another call to ConnectAsync
throw new ConnectionCanceledException();
Copy link
Member

Choose a reason for hiding this comment

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

I don't quite understand why we need this exception, the core never handles this concrete type

Copy link
Contributor

Choose a reason for hiding this comment

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

The retry interceptor probably need to be fixed to handle it.

Copy link
Member Author

@bernardnormier bernardnormier Jun 23, 2022

Choose a reason for hiding this comment

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

We need an exception to report that the first/main ConnectAsync failed with an OperationCanceledException and left the connection in an unusable state.

For ConnectAsync, we could abort the connection (in the first failed ConnectAsync) and throw ConnectionClosedException here. For ShutdownAsync, it's not as clear, in particular, I think it would be weird for ShutdownAsync to throw ConnectionClosedException in this situation.

I'd rather leave the fixing of the retry interceptor (if any) for later until we figure out what we want to do here.

src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
Copy link
Contributor

@bentoi bentoi left a comment

Choose a reason for hiding this comment

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

About the ClientConnection.ConnectAsync semantics... It can be called from multiple threads. Shouldn't the cancellation of any of the pending ConnectAsync trigger the connect cancellation? I'm not mistaken, on the fix ConnectAsync call will trigger the connect cancellation. It's the same for ShutdownAsync.

IMO, any canceled connect or shutdown should leave the connection is a state where the only operation that can be called is DisposeAsync. Calling any other operations should raise ConnectionCanceledException.

There's also the issue that we now raise TimeoutException from ConnectAsync the retry interceptor can no longer distinguish the possibly non-retryable invocation timeout or the always retryable connect. Or is the idea to not support retrying invocation timeouts with the retry interceptor?

catch (OperationCanceledException exception) when (exception.CancellationToken != cancel)
{
// exception comes from another call to ConnectAsync
throw new ConnectionCanceledException();
Copy link
Contributor

Choose a reason for hiding this comment

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

The retry interceptor probably need to be fixed to handle it.

src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
src/IceRpc/ClientConnection.cs Show resolved Hide resolved
src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
src/IceRpc/Internal/ServerConnection.cs Outdated Show resolved Hide resolved
src/IceRpc/Server.cs Outdated Show resolved Hide resolved
src/IceRpc/Server.cs Show resolved Hide resolved
src/IceRpc/Server.cs Show resolved Hide resolved
src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
@pepone
Copy link
Member

pepone commented Jun 23, 2022

There's also the issue that we now raise TimeoutException from ConnectAsync the retry interceptor can no longer distinguish the possibly non-retryable invocation timeout or the always retryable connect. Or is the idea to not support retrying invocation timeouts with the retry interceptor?

I think the rule should be to not retry after we exceed the request deadline, if a connect operation timeouts we should still be able to retry, so it is important that we can distinguish between the request deadline expired and other timeouts.

bernardnormier and others added 2 commits June 23, 2022 10:16
Co-authored-by: Joe George <joe@externl.com>
Co-authored-by: Joe George <joe@externl.com>
@bernardnormier
Copy link
Member Author

bernardnormier commented Jun 23, 2022

About the ClientConnection.ConnectAsync semantics... It can be called from multiple threads. Shouldn't the cancellation of any of the pending ConnectAsync trigger the connect cancellation?

No, It's only the first or "main" ConnectAsync call that matters. If it's canceled, then the connection is canceled and it becomes unusable forever.
Concurrent ConnectAsync calls wait for this main ConnectAsync call to complete. If they timeout while waiting, they don't cancel the main ConnectAsync. Their demise does not mark the connection as canceled, and the main ConnectAsync can still complete successfully.

I'm not mistaken, on the fix ConnectAsync call will trigger the connect cancellation. It's the same for ShutdownAsync.

Yes.

IMO, any canceled connect or shutdown should leave the connection is a state where the only operation that can be called is DisposeAsync. Calling any other operations should raise ConnectionCanceledException.

That's not the way I see it.

@bernardnormier
Copy link
Member Author

There's also the issue that we now raise TimeoutException from ConnectAsync the retry interceptor can no longer distinguish the possibly non-retryable invocation timeout or the always retryable connect. Or is the idea to not support retrying invocation timeouts with the retry interceptor?

I think the rule should be to not retry after we exceed the request deadline, if a connect operation timeouts we should still be able to retry, so it is important that we can distinguish between the request deadline expired and other timeouts.

The solution is obvious here: you install your deadline interceptor before your retry interceptor. Then your retry interceptor won't see Timeout exceptions thrown by your deadline interceptor.

@bernardnormier
Copy link
Member Author

As far as retrying on connect Timeout exceptions, the retry interceptor always handles this properly. It's going to retry on ANY exception if the request is idempotent or its payload was not read yet:

                    catch (Exception ex)
                    {
                        response = null;
                        exception = ex;

                        // ConnectionClosedException is a graceful connection closure that is always safe to retry.
                        if (ex is ConnectionClosedException ||
                            request.Fields.ContainsKey(RequestFieldKey.Idempotent) ||
                            !decorator.IsRead)
                        {
                            retryPolicy = RetryPolicy.Immediately;
                        }
                    }

ClientConnection.InvokeAsync "connects" (and throws TimeoutException if ConnectAsync times out) before reading the payload.

/// <exception cref="ConnectionClosedException">Thrown if the connection is already closed.</exception>
/// <exception cref="OperationCanceledException">Thrown if cancellation was requested through the cancellation
/// token.</exception>
public async Task ConnectAsync(CancellationToken cancel = default)
Copy link
Member

@pepone pepone Jun 23, 2022

Choose a reason for hiding this comment

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

Shouldn't the implementation create a cancelation token source with _connectTimeout as we do in InvokeAsync, I don't see why this is only done when ConnectAsync is called from InvokeAsync.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, the idea is if you call ConnectAsync or ShutdownAsync directly, you pass a cancellation token and decide if/when this call times out through this cancellation token.

Copy link
Member

Choose a reason for hiding this comment

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

This doesn't feel right, if you create a connection and passed a ConnectTimeout it should apply when you call ConnectAsync

Copy link
Member Author

Choose a reason for hiding this comment

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

No, see #1417 point 1.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, this means the connection pool is not using connection timeouts anymore.

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to revisit ConnectionPool in a follow-up PR.

examples/GenericHost/Server/Program.cs Outdated Show resolved Hide resolved
src/IceRpc/ClientConnection.cs Outdated Show resolved Hide resolved
Copy link
Member

@externl externl left a comment

Choose a reason for hiding this comment

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

Looks good to me!

@bernardnormier bernardnormier merged commit 61f7302 into icerpc:main Jun 24, 2022
@bernardnormier bernardnormier deleted the connect-async branch August 25, 2022 18:23
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.

None yet

4 participants