Skip to content
This repository has been archived by the owner on Sep 13, 2022. It is now read-only.

Add async methods in System.Data.Common, implement IAsyncDisposable #1283

Merged
merged 1 commit into from
Jul 2, 2019

Conversation

roji
Copy link
Member

@roji roji commented Jun 26, 2019

DO NOT MERGE until dotnet/corefx#39070 is merged


This brings over https://github.com/dotnet/corefx/issues/35012 (already merged for .NET Core 3.0) to .NET Standard 2.1.

One thing I noticed, is that in the corefx implementation the types have been updated to implement IAsyncDisposable, but in the ref they have not (although DisposeAsync() has been added). Can someone please confirm whether the interface should get added there?

@roji roji requested review from a team as code owners June 26, 2019 14:51
@roji roji force-pushed the SystemDataAsync branch from 974d564 to 8597488 Compare June 26, 2019 14:59
@roji
Copy link
Member Author

roji commented Jun 26, 2019

/cc @divega @ajcvickers @terrajobst

Copy link

@divega divega left a comment

Choose a reason for hiding this comment

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

LGTM. I assume we need @terrajobst to approve/merge.

@terrajobst
Copy link

LGTM. I assume we need @terrajobst to approve/merge.

We need all board representatives to sign off.

Tagging @marek-safar @agocke

@@ -466,9 +474,11 @@ public abstract partial class DbDataReader : System.MarshalByRefObject, System.C
public abstract int RecordsAffected { get; }
public virtual int VisibleFieldCount { get { throw null; } }
public virtual void Close() { }
public virtual System.Threading.Tasks.Task CloseAsync(System.Threading.CancellationToken cancellationToken = default) { throw null; }
Copy link
Member

@agocke agocke Jun 27, 2019

Choose a reason for hiding this comment

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

One note: it seems a little weird to accept a cancellation token here. The reasoning for not including a CancellationToken in DisposeAsync is as follows:

DisposeAsync accepting a CancellationToken: while in theory it makes sense that anything async can be canceled, disposal is about cleanup, closing things out, free'ing resources, etc., which is generally not something that should be canceled; cleanup is still important for work that's canceled. The same CancellationToken that caused the actual work to be canceled would typically be the same token passed to DisposeAsync, making DisposeAsync worthless because cancellation of the work would cause DisposeAsync to be a nop. If someone wants to avoid being blocked waiting for disposal, they can avoid waiting on the resulting ValueTask, or wait on it only for some period of time.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the comment! It seems like there may be cause to treat CloseAsync a bit differently from DisposeAsync.

First, it's less likely that the same CancellationToken that caused the work to be canceled would end up being passed to CloseAsync (assuming the standard async using pattern etc.).

Also, at least for DbConnection, the same object can closed and opened several times, so closing isn't really about terminal cleanup (object continues to be usable afterwards), but possibly a bit more similar to a standard I/O operation. That's admittedly less true for DbDataReader which can't be reopened once closed - although some properties on it remain alive (and in some cases even only accessible) after close and before dispose.

@divega @terrajobst what do you think? Will hold off on merging this for a bit.

Copy link

@divega divega Jun 28, 2019

Choose a reason for hiding this comment

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

@roji, I came to the exact same conclusions as you without reading your comment 😄 In particular about the difference between CloseAsync and DisposeAsync.

But then I looked at some of the code we have already put together in EF Core in preparation for this PR to see if I could find any interesting anecdotal information. And it turns out that we have a few different cases that made me change my mind completely.

TL;DR: I think I found proof that @agocke is right, and having CancellationToken on CloseAsync is actually a pit of failure.

Here are the examples:

All in all, it seems that we are using CloseAsync for cleanup (e.g. in finally blocks) and it is hard to figure why passing the same cancellation token that you are passing to everything else is the wrong thing to do.

@roji, @ajcvickers I will create an issue in EF Core to track removing the cancellation token. If you agree, can we get this changes into .NET Core and this PR?

Copy link

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

On DbConnection, it seems a little problematic to have OpenAsync accept a token but not CloseAsync - but I do agree that the pit of failure here outweighs that. Am fine with removing the token from CloseAsync, will wait to hear @ajcvickers's opinion.

Choose a reason for hiding this comment

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

I'm on the fence, but I'm fine with removing it.

Copy link

@divega divega Jul 1, 2019

Choose a reason for hiding this comment

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

On DbConnection, it seems a little problematic to have OpenAsync accept a token but not CloseAsync

Do you have in mind any specific case in which this is problematic?

I haven't come up with any case in which because of hoe OpenAsync() is called, we would need CloseAsync() to accept a CancellationToken to achieve the correct behavior.

Example 1:

try
{
    await connection.OpenAsync(ct);
    command.Connection = connection;
    await command.ExecuteNonQueryAsync(ct);
}
finally
{
    await connection.CloseAsync();
}
  • If cancellation is triggered after OpenAsync() executes but before ExecuteNoQueryAsync() executes. The latter should then throw, causing the finally block to be executed. Since the connection is open, CloseAsync should close the connection.

  • If cancellation is triggered before OpenAsync() executes, the latter will not open the connection and throw, causing the finally block to be executed. Since the connection isn't open, CloseAsync should then be a no-op.

Example 2:

await connection.OpenAsync(ct);
try
{
    command.Connection = connection;
    await command.ExecuteNonQueryAsync(ct);
}
finally
{
    await connection.CloseAsync();
}

If cancellation is triggered before OpenAsync(), the connection should not be opened and the exception should be handled elsewhere. This is the pattern in https://github.com/aspnet/EntityFrameworkCore/blob/master/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs#L142.

If OpenAsync() implementation is buggy and does not throw if its cancelled, then it becomes equivalent to one of the other cases.

Copy link
Member Author

Choose a reason for hiding this comment

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

The scenarios I have in mind are where DbConnection.CloseAsync isn't used for cleanup (in a finally block), but as a close operation where the connection is expected to be reopened. It's also worth noting that this is valid for non-pooled connections - with pooled connections (the common case) CloseAsync is expected to return synchronously in all but some rare uncommon cases (since it's putting the connection back in the pool).

But I do agree that scenarios where close cancellation is important are niche, and see more potential damage in keeping the token (i.e. when the same cancelled token is accidentally in passed for cleanup). Since @ajcvickers is OK with removing, I can submit a PR to corefx and see whether @stephentoub or @terrajobst have any objections.

Copy link
Member Author

Choose a reason for hiding this comment

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

Submitted PR to corefx for token removal: dotnet/corefx#39070

@roji roji force-pushed the SystemDataAsync branch from 8597488 to 2ee72da Compare June 28, 2019 11:52
roji added a commit to roji/corefx that referenced this pull request Jul 1, 2019
Affects DbDataReader and DbConnection, since these APIs are very likely
to be used for cleanup only, in which case a cancellation token is
an anti-pattern (similar to why DisposeAsync doesn't accept one).

See discussion here:
dotnet/standard#1283 (review)
roji added a commit to roji/corefx that referenced this pull request Jul 1, 2019
Affects DbDataReader and DbConnection, since these APIs are very likely
to be used for cleanup only, in which case a cancellation token is
an anti-pattern (similar to why DisposeAsync doesn't accept one).

See discussion here:
dotnet/standard#1283 (review)

Fixes #39069
@terrajobst
Copy link

@roji @divega

OK, I'll wait until dotnet/corefx#39070 is being merged.

@terrajobst terrajobst added * NO MERGE * Applied to pull-requests that aren't ready to be merged yet. netstandard-api This tracks requests for standardizing APIs. labels Jul 1, 2019
@terrajobst terrajobst added this to the .NET Standard 2.1 milestone Jul 2, 2019
@wtgodbe
Copy link
Member

wtgodbe commented Jul 2, 2019

Approved for preview7

wtgodbe pushed a commit to dotnet/corefx that referenced this pull request Jul 2, 2019
Affects DbDataReader and DbConnection, since these APIs are very likely
to be used for cleanup only, in which case a cancellation token is
an anti-pattern (similar to why DisposeAsync doesn't accept one).

See discussion here:
dotnet/standard#1283 (review)

Fixes #39069
@wtgodbe wtgodbe removed the * NO MERGE * Applied to pull-requests that aren't ready to be merged yet. label Jul 2, 2019
@wtgodbe wtgodbe merged commit 84650d1 into dotnet:master Jul 2, 2019
wtgodbe pushed a commit to wtgodbe/corefx that referenced this pull request Jul 2, 2019
…9070)

Affects DbDataReader and DbConnection, since these APIs are very likely
to be used for cleanup only, in which case a cancellation token is
an anti-pattern (similar to why DisposeAsync doesn't accept one).

See discussion here:
dotnet/standard#1283 (review)

Fixes #39069
roji added a commit to roji/standard that referenced this pull request Jul 2, 2019
@roji
Copy link
Member Author

roji commented Jul 2, 2019

Seems like this was merged before the cancellation token was removed from CloseAsync - note that dotnet/corefx#39070 has already been merged, removing it from corefx.

Have just submitted #1304 to remove the token from the two CloseAsync methods.

wtgodbe added a commit to dotnet/corefx that referenced this pull request Jul 2, 2019
…39122)

Affects DbDataReader and DbConnection, since these APIs are very likely
to be used for cleanup only, in which case a cancellation token is
an anti-pattern (similar to why DisposeAsync doesn't accept one).

See discussion here:
dotnet/standard#1283 (review)

Fixes #39069
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…orefx#39070)

Affects DbDataReader and DbConnection, since these APIs are very likely
to be used for cleanup only, in which case a cancellation token is
an anti-pattern (similar to why DisposeAsync doesn't accept one).

See discussion here:
dotnet/standard#1283 (review)

Fixes dotnet/corefx#39069

Commit migrated from dotnet/corefx@539ed31
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
netstandard-api This tracks requests for standardizing APIs.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants