Deadlock in the GC Finalizer thread
-----------------------------------
Key: DNET-382
URL: http://tracker.firebirdsql.org/browse/DNET-382
Project: .NET Data provider
Issue Type: Bug
Components: ADO.NET Provider
Affects Versions: 2.6
Environment: Not important
Reporter: Fernando Nájera
Assignee: Jiri Cincura
Priority: Blocker
The .NET Provider 2.6.0 sometimes deadlocks with the GC. After a lot of
research I have found the problem: sometimes the GC is triggered in a "bad
moment" causing the deadlock.
My case: I have an FbConnection and FbTransaction. I create a FbCommand,
execute it, but forget to call .Dispose(). Then I call FbTransaction.Commit(),
but during its execution, the GC is triggered (remember that GC can be
triggered at any moment, and apparently I got the worst one). If the timing is
bad enough, a deadlock happens.
Reproducing this issue is normally very difficult, but I have managed to create
a test case that will always deadlock:
1. In FirebirdSql.Data.FirebirdClient, modify AssemblyInfo.cs and add:
[assembly: InternalsVisibleTo ("FirebirdSql.Data.UnitTests,
PublicKey=0024000004800000940000000602000000240000525341310004000001000100efef0d6d73af0b39be7ad5932256d0dbcce6e4bfec20d3697f52d9057e61b9b432d026bce894d519ee4c4d8bfa0853b88c779c4718cf0f8cd070fddb62e9835113d334f9105456692a459c4de434e49b7a789b6785a49febf71d6fb0efffd58945e906ce1442fca026064610d9e89aa4cf15625b0c468b650db8e222cc2e37c3")]
2. In FirebirdSql.Data.UnitTests, change the project properties and make the
dll signed using the same key as FirebirdSql.Data.FirebirdClient.
These two steps are required so the tests can access the field added in step 3.
3. In FirebirdSql.Data.FirebirdClient, modify
FirebirdSql.Data.Client.Managed.Version10.GdsStatement to add a new field and a
call inside TransactionUpdated.
// ...
// new field used by the tests to force a GC call exactly in the moment where
it hurts the most
public static Action DEBUG_WhenTransactionUpdated;
protected override void TransactionUpdated(object sender, EventArgs e)
{
// call the method set by the tests
if (DEBUG_WhenTransactionUpdated != null) { DEBUG_WhenTransactionUpdated
(); }
lock (this)
{
if (this.Transaction != null && this.TransactionUpdate != null)
{
// ...
Note that this change won't affect the DLL in any way as
DEBUG_WhenTransactionUpdated is null by default.
But it provides a nice "injection point" for our test, used in step 4.
4. In FirebirdSql.Data.UnitTests, create a new unit test and add this test:
[Test]
public void TestHang ()
{
FbConnectionStringBuilder csb = base.BuildConnectionStringBuilder ();
try
{
using (FbConnection cnn = new FbConnection (csb.ToString ()))
{
cnn.Open ();
using (FbTransaction tx = cnn.BeginTransaction ())
{
// NOTE: not "using" here
FbCommand cmd = new FbCommand ("SELECT * FROM TEST", cnn, tx);
cmd.ExecuteNonQuery ();
cmd = null; // important - make GC think that we are finished
with the command, and it can be gathered
// GC can be triggered at any point - in this test, we trigger
it exactly when we are inside the WhenTransactionUpdated event
global::FirebirdSql.Data.Client.Managed.Version10.GdsStatement.DEBUG_WhenTransactionUpdated
= delegate
{
// to be realistic, we will invoke the GC from yet a
different thread
Thread th = new Thread (new ThreadStart (delegate
{
// simulate, in a different thread, a GC pass
GC.Collect ();
GC.WaitForPendingFinalizers ();
GC.Collect ();
})) { IsBackground = true };
th.Start ();
// give some time to the thread to run and cause the
damage, then continue with the "TransactionUpdated" code
Thread.Sleep (2000);
};
// this will hang...
tx.Commit ();
}
}
}
finally
{
// clean up... although this test will hang, so this code doesn't get a
chance...
global::FirebirdSql.Data.Client.Managed.Version10.GdsStatement.DEBUG_WhenTransactionUpdated
= null;
}
}
5. Run the test. It will hang (never complete).
If you debug the situation, you will see this:
a) TestRunnerThread stack (in **inverse** order):
FirebirdSql.Data.UnitTests.DLL!FirebirdSql.Data.UnitTests.DeadlockWithGCTests.TestHang()
Line 60 + 0xb bytes C#
FirebirdSql.Data.FirebirdClient.DLL!FirebirdSql.Data.FirebirdClient.FbTransaction.Commit()
Line 169 + 0xc bytes C#
which takes lock on FbTransaction
FirebirdSql.Data.FirebirdClient.DLL!FirebirdSql.Data.Client.Managed.Version10.GdsTransaction.Commit()
Line 174 + 0x2f bytes C#
which takes lock on database.SyncObject
>
> FirebirdSql.Data.FirebirdClient.DLL!FirebirdSql.Data.Client.Managed.Version10.GdsStatement.TransactionUpdated(object
> sender = {FirebirdSql.Data.Client.Managed.Version10.GdsTransaction},
> System.EventArgs e = {System.EventArgs}) Line 659 + 0x11 bytes C#
which deadlocks while trying to lock on the GdsStatement
mscorlib.dll!System.Threading.Monitor.Enter(object obj, ref bool
lockTaken) + 0x14 bytes
b) GC Finalizer thread (in **inverse** order):
System.dll!System.ComponentModel.Component.Finalize() + 0x18 bytes
FirebirdSql.Data.FirebirdClient.DLL!FirebirdSql.Data.FirebirdClient.FbCommand.Dispose(bool
disposing = false) Line 396 + 0x8 bytes C#
which takes lock on the FbCommand
FirebirdSql.Data.FirebirdClient.DLL!FirebirdSql.Data.FirebirdClient.FbCommand.Release()
Line 842 + 0xe bytes C#
FirebirdSql.Data.FirebirdClient.DLL!FirebirdSql.Data.Common.StatementBase.Dispose()
Line 177 + 0x10 bytes C#
FirebirdSql.Data.FirebirdClient.DLL!FirebirdSql.Data.Client.Managed.Version10.GdsStatement.Dispose(bool
disposing = true) Line 183 + 0xa bytes C#
which takes lock on the GdsStatement
FirebirdSql.Data.FirebirdClient.DLL!FirebirdSql.Data.Common.StatementBase.Release()
Line 261 + 0x10 bytes C#
>
> FirebirdSql.Data.FirebirdClient.DLL!FirebirdSql.Data.Client.Managed.Version11.GdsStatement.Free(int
> option = 2) Line 203 + 0x21 bytes C#
which deadlocks while trying to lock the database.SyncObject
mscorlib.dll!System.Threading.Monitor.Enter(object obj, ref bool
lockTaken) + 0x14 bytes
As forced as this situation might seem by the looks of the test, I have to say
that this is not a border case at all: this bug is affecting my software and I
have seen this deadlock happening in several ocassions in several computers.
I am going to change my code to ensure that I call FbCommand.Dispose() before
losing the variable reference, which should prevent this deadlock.
However, considering that it is the GC thread then one that deadlocks (apart
from the program itself), and that it is "easy" to forget to call .Dispose in
every single object that implements IDisposable, it might be worth trying to
fix this issue. Following the instructions of the Bug Tracker, this should be a
Blocker Priority Issue as it effectively blocks the program when it happens -
even if it happens very rarely.
--
This message is automatically generated by JIRA.
-
If you think it was sent incorrectly contact one of the administrators:
http://tracker.firebirdsql.org/secure/Administrators.jspa
-
For more information on JIRA, see: http://www.atlassian.com/software/jira
------------------------------------------------------------------------------
vRanger cuts backup time in half-while increasing security.
With the market-leading solution for virtual backup and recovery,
you get blazing-fast, flexible, and affordable data protection.
Download your free trial now.
http://p.sf.net/sfu/quest-d2dcopy1
_______________________________________________
Firebird-net-provider mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/firebird-net-provider