Fix a second Dispose of NpgsqlTransaction rolling back a later transaction by sanjaysingh · Pull Request #6670 · npgsql/npgsql · GitHub
Skip to content
Open
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
7 changes: 3 additions & 4 deletions src/Npgsql/Internal/NpgsqlConnector.cs
6 changes: 5 additions & 1 deletion src/Npgsql/NpgsqlConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,11 @@ async ValueTask<NpgsqlTransaction> BeginTransaction(bool async, IsolationLevel l
// But we start a user action to check the cancellation token and generate exceptions
using var _ = connector.StartUserAction(cancellationToken);

connector.Transaction ??= new NpgsqlTransaction(connector);
// A disposed transaction stays disposed. Reusing it would make a later Dispose roll back
// whatever transaction now occupies that object (#6665).
if (connector.Transaction is null || connector.Transaction.IsDisposed)
connector.Transaction = new NpgsqlTransaction(connector);

connector.Transaction.Init(level, options);
return connector.Transaction;
}
Expand Down
29 changes: 15 additions & 14 deletions src/Npgsql/NpgsqlTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ internal void Init(IsolationLevel isolationLevel, NpgsqlTransactionOptions optio

_connector.TransactionStatus = TransactionStatus.Pending;
_isolationLevel = isolationLevel;
IsDisposed = false;
Debug.Assert(!IsDisposed);

LogMessages.StartedTransaction(_transactionLogger, isolationLevel, _connector.Id);
}
Expand Down Expand Up @@ -355,7 +355,7 @@ protected override void Dispose(bool disposing)
}
}

IsDisposed = true;
MarkDisposed();
}
}

Expand All @@ -371,7 +371,7 @@ public override ValueTask DisposeAsync()
return DisposeAsyncInternal();
}

IsDisposed = true;
MarkDisposed();
}

return default;
Expand All @@ -390,7 +390,7 @@ async ValueTask DisposeAsyncInternal()
LogMessages.ExceptionDuringTransactionDispose(_transactionLogger, _connector.Id, ex);
}

IsDisposed = true;
MarkDisposed();
}
}

Expand All @@ -400,8 +400,17 @@ async ValueTask DisposeAsyncInternal()
/// </summary>
internal void DisposeImmediately(Exception? disposeReason)
{
IsDisposed = true;
_disposeReason = disposeReason;
MarkDisposed();
}

// Drop the connector's reference so the next BeginTransaction allocates a new transaction
// instead of resurrecting this one.
void MarkDisposed()
{
IsDisposed = true;
if (_connector is not null && ReferenceEquals(_connector.Transaction, this))
_connector.Transaction = null;
}

#endregion
Expand Down Expand Up @@ -451,15 +460,7 @@ internal void UnbindIfNecessary()
// See #3306
if (!IsDisposed)
{
if (_connector.UnboundTransaction is { IsDisposed: true } previousTransaction)
{
previousTransaction._connector = _connector;
_connector.Transaction = previousTransaction;
}
else
_connector.Transaction = null;

_connector.UnboundTransaction = this;
_connector.Transaction = null;
_connector = null!;
}
}
Expand Down
29 changes: 28 additions & 1 deletion test/Npgsql.Tests/TransactionTests.cs