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

Recover connection state counting after external close #9792

Merged
merged 1 commit into from
Sep 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/EFCore.Relational.Specification.Tests/TransactionTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,45 @@ public virtual void EnlistTransaction_throws_if_ambient_transaction_started()
}
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public virtual async Task Externally_closed_connections_are_handled_correctly(bool async)
{
DbConnection connection;
using (var context = CreateContextWithConnectionString())
{
var set = context.Set<TransactionCustomer>();

if (async)
{
await context.Database.OpenConnectionAsync();
}
else
{
context.Database.OpenConnection();
}

connection = context.Database.GetDbConnection();

connection.Close();

var _ = async ? await set.ToListAsync() : set.ToList();

Assert.Equal(ConnectionState.Open, connection.State);

context.Database.CloseConnection();

Assert.Equal(ConnectionState.Closed, connection.State);

_ = async ? await set.ToListAsync() : set.ToList();

Assert.Equal(ConnectionState.Closed, connection.State);
}

Assert.Equal(ConnectionState.Closed, connection.State);
}

protected virtual void AssertStoreInitialState()
{
using (var context = CreateContext())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public static readonly IDictionary<Type, ServiceCharacteristics> RelationalServi
{ typeof(IMemberTranslator), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(ICompositeMethodCallTranslator), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IQuerySqlGeneratorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IRelationalTransactionFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(ICommandBatchPreparer), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IModificationCommandBatchFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IMigrationsModelDiffer), new ServiceCharacteristics(ServiceLifetime.Scoped) },
Expand Down Expand Up @@ -162,6 +163,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
TryAdd<ISqlTranslatingExpressionVisitorFactory, SqlTranslatingExpressionVisitorFactory>();
TryAdd<INamedConnectionStringResolver, NamedConnectionStringResolver>();
TryAdd<IEvaluatableExpressionFilter, RelationalEvaluatableExpressionFilter>();
TryAdd<IRelationalTransactionFactory, RelationalTransactionFactory>();

ServiceCollectionMap.GetInfrastructure()
.AddDependencySingleton<RelationalCompositeMemberTranslatorDependencies>()
Expand All @@ -179,6 +181,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
.AddDependencySingleton<SelectExpressionDependencies>()
.AddDependencySingleton<RelationalValueBufferFactoryDependencies>()
.AddDependencySingleton<RelationalProjectionExpressionVisitorDependencies>()
.AddDependencySingleton<RelationalTransactionFactoryDependencies>()
.AddDependencyScoped<RelationalConventionSetBuilderDependencies>()
.AddDependencyScoped<RelationalDatabaseCreatorDependencies>()
.AddDependencyScoped<HistoryRepositoryDependencies>()
Expand Down
35 changes: 23 additions & 12 deletions src/EFCore.Relational/Query/Internal/AsyncQueryingEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,29 @@ private async Task<bool> BufferlessMoveNext(bool buffer, CancellationToken cance
{
await _relationalQueryContext.Connection.OpenAsync(cancellationToken);

var relationalCommand
= _shaperCommandContext
.GetRelationalCommand(_relationalQueryContext.ParameterValues);

await _relationalQueryContext.Connection
.RegisterBufferableAsync(this, cancellationToken);

_dataReader
= await relationalCommand.ExecuteReaderAsync(
_relationalQueryContext.Connection,
_relationalQueryContext.ParameterValues,
cancellationToken);
try
{
var relationalCommand
= _shaperCommandContext
.GetRelationalCommand(_relationalQueryContext.ParameterValues);

await _relationalQueryContext.Connection
.RegisterBufferableAsync(this, cancellationToken);

_dataReader
= await relationalCommand.ExecuteReaderAsync(
_relationalQueryContext.Connection,
_relationalQueryContext.ParameterValues,
cancellationToken);
}
catch
{
// If failure happens creating the data reader, then it won't be available to
// handle closing the connection, so do it explicitly here to preserve ref counting.
_relationalQueryContext.Connection.Close();

throw;
}

_dbDataReader = _dataReader.DbDataReader;
_shaperCommandContext.NotifyReaderCreated(_dbDataReader);
Expand Down
27 changes: 19 additions & 8 deletions src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,27 @@ private bool BufferlessMoveNext(bool buffer)
{
_relationalQueryContext.Connection.Open();

var relationalCommand
= _shaperCommandContext
.GetRelationalCommand(_relationalQueryContext.ParameterValues);
try
{
var relationalCommand
= _shaperCommandContext
.GetRelationalCommand(_relationalQueryContext.ParameterValues);

_relationalQueryContext.Connection.RegisterBufferable(this);
_relationalQueryContext.Connection.RegisterBufferable(this);

_dataReader
= relationalCommand.ExecuteReader(
_relationalQueryContext.Connection,
_relationalQueryContext.ParameterValues);
_dataReader
= relationalCommand.ExecuteReader(
_relationalQueryContext.Connection,
_relationalQueryContext.ParameterValues);
}
catch
{
// If failure happens creating the data reader, then it won't be available to
// handle closing the connection, so do it explicitly here to preserve ref counting.
_relationalQueryContext.Connection.Close();

throw;
}

_dbDataReader = _dataReader.DbDataReader;
_shaperCommandContext.NotifyReaderCreated(_dbDataReader);
Expand Down
36 changes: 36 additions & 0 deletions src/EFCore.Relational/Storage/IRelationalTransactionFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Data.Common;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace Microsoft.EntityFrameworkCore.Storage
{
/// <summary>
/// <para>
/// A factory for creating <see cref="RelationalTransaction" /> instances.
/// </para>
/// <para>
/// This type is typically used by database providers It is generally not used in application code.
/// </para>
/// </summary>
public interface IRelationalTransactionFactory
{
/// <summary>
/// Creates a <see cref="RelationalTransaction" /> instance.
/// </summary>
/// <param name="connection"> The connection to the database. </param>
/// <param name="transaction"> The underlying <see cref="DbTransaction" />. </param>
/// <param name="logger"> The logger to write to. </param>
/// <param name="transactionOwned">
/// A value indicating whether the transaction is owned by this class (i.e. if it can be disposed when this class is disposed).
/// </param>
/// <returns> A new <see cref="RelationalTransaction" /> instance. </returns>
RelationalTransaction Create(
[NotNull] IRelationalConnection connection,
[NotNull] DbTransaction transaction,
[NotNull] IDiagnosticsLogger<DbLoggerCategory.Database.Transaction> logger,
bool transactionOwned);
}
}
20 changes: 7 additions & 13 deletions src/EFCore.Relational/Storage/RelationalConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ private IDbContextTransaction BeginTransactionWithNoPreconditions(IsolationLevel
var dbTransaction = DbConnection.BeginTransaction(isolationLevel);

CurrentTransaction
= new RelationalTransaction(
= Dependencies.RelationalTransactionFactory.Create(
this,
dbTransaction,
Dependencies.TransactionLogger,
Expand Down Expand Up @@ -275,7 +275,7 @@ public virtual IDbContextTransaction UseTransaction(DbTransaction transaction)

Open();

CurrentTransaction = new RelationalTransaction(
CurrentTransaction = Dependencies.RelationalTransactionFactory.Create(
this,
transaction,
Dependencies.TransactionLogger,
Expand Down Expand Up @@ -339,10 +339,8 @@ public virtual bool Open(bool errorsExpected = false)
wasOpened = true;
ClearTransactions();
}
else
{
_openedCount++;
}

_openedCount++;
Copy link
Member

Choose a reason for hiding this comment

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

Interesting that the tests pass, but this won't work for connection resiliency. Count would be incremented on every retry.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@AndriySvyryd I added a test using connection resiliency and fixed a test bug, but can't get product code to to fail. Probably going to need some help writing a test that demonstrates this.

Copy link
Member

Choose a reason for hiding this comment

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

I found the issue in test code: TestRelationalTransaction.ClearTransaction() calls _testConnection.Close() for some reason, hiding the product bug.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@AndriySvyryd I simplified TestRelationalTransaction and TestSqlServerConnection so that they don't have any logic other than causing failures...still can't reproduce the issue.

Copy link
Member

Choose a reason for hiding this comment

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

I see, that was unrelated then. The code you added to TestRelationalCommandBuilderFactory hides a bug.
I've also added tests for the other places where an ExecutionStrategy is used: see origin\MoreResiliencyTests ce4bf0d

Copy link
Contributor Author

Choose a reason for hiding this comment

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

New bits up with query fix.


HandleAmbientTransactions();

Expand Down Expand Up @@ -375,10 +373,8 @@ public virtual async Task<bool> OpenAsync(CancellationToken cancellationToken, b
wasOpened = true;
ClearTransactions();
}
else
{
_openedCount++;
}

_openedCount++;

HandleAmbientTransactions();

Expand All @@ -388,7 +384,7 @@ public virtual async Task<bool> OpenAsync(CancellationToken cancellationToken, b
private void ClearTransactions()
{
var previousOpenedCount = _openedCount;
_openedCount++;
_openedCount += 2;
CurrentTransaction?.Dispose();
CurrentTransaction = null;
EnlistedTransaction = null;
Expand Down Expand Up @@ -436,7 +432,6 @@ private void OpenDbConnection(bool errorsExpected)
if (_openedCount == 0)
{
_openedInternally = true;
_openedCount++;
}
}

Expand Down Expand Up @@ -476,7 +471,6 @@ private async Task OpenDbConnectionAsync(bool errorsExpected, CancellationToken
if (_openedCount == 0)
{
_openedInternally = true;
_openedCount++;
}
}

Expand Down
30 changes: 25 additions & 5 deletions src/EFCore.Relational/Storage/RelationalConnectionDependencies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,25 @@ public sealed class RelationalConnectionDependencies
/// <param name="transactionLogger"> The logger to which transaction messages will be written. </param>
/// <param name="connectionLogger"> The logger to which connection messages will be written. </param>
/// <param name="connectionStringResolver"> A service for resolving a connection string from a name. </param>
/// <param name="relationalTransactionFactory"> A service for creating <see cref="RelationalTransaction"/> instances. </param>
public RelationalConnectionDependencies(
[NotNull] IDbContextOptions contextOptions,
[NotNull] IDiagnosticsLogger<DbLoggerCategory.Database.Transaction> transactionLogger,
[NotNull] IDiagnosticsLogger<DbLoggerCategory.Database.Connection> connectionLogger,
[NotNull] INamedConnectionStringResolver connectionStringResolver)
[NotNull] INamedConnectionStringResolver connectionStringResolver,
[NotNull] IRelationalTransactionFactory relationalTransactionFactory)
{
Check.NotNull(contextOptions, nameof(contextOptions));
Check.NotNull(transactionLogger, nameof(transactionLogger));
Check.NotNull(connectionLogger, nameof(connectionLogger));
Check.NotNull(connectionStringResolver, nameof(connectionStringResolver));
Check.NotNull(relationalTransactionFactory, nameof(relationalTransactionFactory));

ContextOptions = contextOptions;
TransactionLogger = transactionLogger;
ConnectionLogger = connectionLogger;
ConnectionStringResolver = connectionStringResolver;
RelationalTransactionFactory = relationalTransactionFactory;
}

/// <summary>
Expand All @@ -82,6 +86,11 @@ public RelationalConnectionDependencies(
/// </summary>
public INamedConnectionStringResolver ConnectionStringResolver { get; }

/// <summary>
/// A service for creating <see cref="RelationalTransaction"/> instances.
/// </summary>
public IRelationalTransactionFactory RelationalTransactionFactory { get; }

/// <summary>
/// Clones this dependency parameter object with one service replaced.
/// </summary>
Expand All @@ -90,7 +99,7 @@ public RelationalConnectionDependencies(
/// </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public RelationalConnectionDependencies With([NotNull] IDbContextOptions contextOptions)
=> new RelationalConnectionDependencies(contextOptions, TransactionLogger, ConnectionLogger, ConnectionStringResolver);
=> new RelationalConnectionDependencies(contextOptions, TransactionLogger, ConnectionLogger, ConnectionStringResolver, RelationalTransactionFactory);

/// <summary>
/// Clones this dependency parameter object with one service replaced.
Expand All @@ -100,7 +109,7 @@ public RelationalConnectionDependencies With([NotNull] IDbContextOptions context
/// </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public RelationalConnectionDependencies With([NotNull] IDiagnosticsLogger<DbLoggerCategory.Database.Connection> connectionLogger)
=> new RelationalConnectionDependencies(ContextOptions, TransactionLogger, connectionLogger, ConnectionStringResolver);
=> new RelationalConnectionDependencies(ContextOptions, TransactionLogger, connectionLogger, ConnectionStringResolver, RelationalTransactionFactory);

/// <summary>
/// Clones this dependency parameter object with one service replaced.
Expand All @@ -110,7 +119,7 @@ public RelationalConnectionDependencies With([NotNull] IDiagnosticsLogger<DbLogg
/// </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public RelationalConnectionDependencies With([NotNull] IDiagnosticsLogger<DbLoggerCategory.Database.Transaction> transactionLogger)
=> new RelationalConnectionDependencies(ContextOptions, transactionLogger, ConnectionLogger, ConnectionStringResolver);
=> new RelationalConnectionDependencies(ContextOptions, transactionLogger, ConnectionLogger, ConnectionStringResolver, RelationalTransactionFactory);

/// <summary>
/// Clones this dependency parameter object with one service replaced.
Expand All @@ -120,6 +129,17 @@ public RelationalConnectionDependencies With([NotNull] IDiagnosticsLogger<DbLogg
/// </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public RelationalConnectionDependencies With([NotNull] INamedConnectionStringResolver connectionStringResolver)
=> new RelationalConnectionDependencies(ContextOptions, TransactionLogger, ConnectionLogger, connectionStringResolver);
=> new RelationalConnectionDependencies(ContextOptions, TransactionLogger, ConnectionLogger, connectionStringResolver, RelationalTransactionFactory);

/// <summary>
/// Clones this dependency parameter object with one service replaced.
/// </summary>
/// <param name="relationalTransactionFactory">
/// A replacement for the current dependency of this type.
/// </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public RelationalConnectionDependencies With([NotNull] IRelationalTransactionFactory relationalTransactionFactory)
=> new RelationalConnectionDependencies(ContextOptions, TransactionLogger, ConnectionLogger, ConnectionStringResolver, relationalTransactionFactory);

}
}
54 changes: 54 additions & 0 deletions src/EFCore.Relational/Storage/RelationalTransactionFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Data.Common;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Storage
{
/// <summary>
/// <para>
/// A factory for creating <see cref="RelationalTransaction" /> instances.
/// </para>
/// <para>
/// This type is typically used by database providers It is generally not used in application code.
/// </para>
/// </summary>
public class RelationalTransactionFactory : IRelationalTransactionFactory
{
/// <summary>
/// Initializes a new instance of the <see cref="RelationalTransactionFactory" /> class.
/// </summary>
/// <param name="dependencies"> Parameter object containing dependencies for this service. </param>
public RelationalTransactionFactory([NotNull] RelationalTransactionFactoryDependencies dependencies)
{
Check.NotNull(dependencies, nameof(dependencies));

Dependencies = dependencies;
}

/// <summary>
/// Parameter object containing dependencies for this service.
/// </summary>
protected virtual RelationalTransactionFactoryDependencies Dependencies { get; }

/// <summary>
/// Creates a <see cref="RelationalTransaction" /> instance.
/// </summary>
/// <param name="connection"> The connection to the database. </param>
/// <param name="transaction"> The underlying <see cref="DbTransaction" />. </param>
/// <param name="logger"> The logger to write to. </param>
/// <param name="transactionOwned">
/// A value indicating whether the transaction is owned by this class (i.e. if it can be disposed when this class is disposed).
/// </param>
/// <returns> A new <see cref="RelationalTransaction" /> instance. </returns>
public virtual RelationalTransaction Create(
IRelationalConnection connection,
DbTransaction transaction,
IDiagnosticsLogger<DbLoggerCategory.Database.Transaction> logger,
bool transactionOwned)
=> new RelationalTransaction(connection, transaction, logger, transactionOwned);
}
}
Loading