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 Firebird-net-provider@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/firebird-net-provider