Skip to content
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
19 changes: 16 additions & 3 deletions src/Microsoft.Data.Sqlite.Core/SqliteTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,13 @@ private void RollbackInternal()
{
try
{
if (!ExternalRollback)
// The handle can already be gone when the connection was torn down underneath the
// transaction. There is nothing left to roll back in that case, and reaching into it
// only throws.
if (!ExternalRollback
&& _connection!.Handle is { IsClosed: false, IsInvalid: false })
Comment thread
AndriySvyryd marked this conversation as resolved.
{
sqlite3_rollback_hook(_connection!.Handle, null, null);
sqlite3_rollback_hook(_connection.Handle, null, null);
_connection.ExecuteNonQuery("ROLLBACK;");
}
}
Expand All @@ -236,7 +240,16 @@ private void RollbackInternal()

private void RollbackExternal(object userData)
{
sqlite3_rollback_hook(_connection!.Handle, null, null);
// SQLite invokes this while it rolls back, which includes the implicit rollback inside
// sqlite3_close_v2. The handle is being released by then, so clearing the hook through it
// throws, and on the pool prune timer that exception has nowhere to go and takes the
// process down. The hook is torn down with the connection anyway.
var handle = _connection!.Handle;
if (handle is { IsClosed: false, IsInvalid: false })
{
sqlite3_rollback_hook(handle, null, null);
}

ExternalRollback = true;
}
}
32 changes: 32 additions & 0 deletions test/Microsoft.Data.Sqlite.Tests/SqliteTransactionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -432,4 +432,36 @@ public void Savepoint()

private static void CreateTestTable(SqliteConnection connection)
=> connection.ExecuteNonQuery("CREATE TABLE TestTable (TestColumn INTEGER)");

[Fact]
public void Handle_can_be_disposed_with_an_open_transaction()
{
using var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
using var transaction = connection.BeginTransaction();

// SQLite rolls the open transaction back inside sqlite3_close_v2 and fires the rollback
// hook while the handle is being released. Clearing the hook through that same handle threw
// out of the native callback, which is fatal wherever the close happens to run.
connection.Handle!.Dispose();

Assert.True(transaction.ExternalRollback);
}

[Fact]
public void Rollback_works_when_the_handle_is_already_closed()
{
using var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
var transaction = connection.BeginTransaction();

// Clearing the hook first means closing the handle does not run RollbackExternal, so
// ExternalRollback stays false and RollbackInternal is the one that reaches for the handle.
sqlite3_rollback_hook(connection.Handle, null, null);
connection.Handle!.Dispose();

Assert.False(transaction.ExternalRollback);

transaction.Rollback();
}
}
Loading