Bug #59346 NullReferenceException thrown on TransactionScope dispose
Submitted: 7 Jan 2011 10:26 Modified: 18 Feb 2011 15:28
Reporter: Charles Loh Email Updates:
Status: Closed Impact on me:
None 
Category:Connector / NET Severity:S3 (Non-critical)
Version:6.3.6 OS:Any
Assigned to: Reggie Burnett CPU Architecture:Any

[7 Jan 2011 10:26] Charles Loh
Description:
Exception thrown on dispose of TransactionScope.

Affects 6.3.5 as well.

How to repeat:
1) Before running, lock tables t2 read

2) 
        private void btnTestTransactionScope_Click(object sender, EventArgs e)
        {
            using (TransactionScope scope = new TransactionScope())
            {
                DoInsertTablet1();
                DoInsertTablet2(); //X
                scope.Complete();
            }            
        }

        //DoInsertTable2 is similar except for the table name
        private static void DoInsertTable1()
        {

            using (MySqlConnection conn = new MySqlConnection(connectionString))
            using (MySqlCommand cmd = conn.CreateCommand())
            {
                string sql = @"insert into t1 (Val,Val2) values (?value1, ?value2)";
                cmd.CommandText = sql;
                cmd.CommandTimeout = 5;
                cmd.Parameters.AddWithValue("?value1", new Random().Next());
                cmd.Parameters.AddWithValue("?value2", new Random().Next());
                cmd.ExecuteNonQuery();
            }
        }
[7 Jan 2011 10:27] Charles Loh
Environment:
.Net 3.5
Windows XP SP3 (Fully Patched)
[10 Jan 2011 8:56] Tonci Grgin
Hi Charles and thanks for your report.

Can you please attach complete test case so I can check on this.
[10 Jan 2011 10:22] Charles Loh
TestCase for TransactionScope NullReferenceException

Attachment: WindowsFormsApplication1.zip (application/x-zip-compressed, text), 180.86 KiB.

[10 Jan 2011 10:39] Tonci Grgin
Charles, there might be no bug here...  When a user holds a READ LOCK on a table, other users can also read or hold a READ LOCK, but no user can write or hold a WRITE LOCK on that table (which is exactly what you're doing in DoInsertTable2).
[10 Jan 2011 10:52] Tonci Grgin
Charles, I do not see a bug here... If you have guarded your code in the right places you would have seen the actual exception:
+		[MySql.Data.MySqlClient.MySqlException]	{"Timeout expired.  The timeout period elapsed prior to completion of the operation or the server is not responding."}	MySql.Data.MySqlClient.MySqlException
New code:
        private static void DoInsertTable2()
        {
            using (MySqlConnection conn = new MySqlConnection(GetTestDBConn()))
            using (MySqlCommand cmd = conn.CreateCommand())
            {
                conn.Open();
                cmd.CommandText = @"insert into t2 (Val) values (?value)";
                cmd.CommandTimeout = 5;
                cmd.Parameters.AddWithValue("?value", new Random().Next());
                try {
                    cmd.ExecuteNonQuery();
                }
                    catch (Exception ee)
                {
                    throw;
                }
            }
        }
[10 Jan 2011 11:04] Charles Loh
Hi,

The exception that I am getting in the DoTransactionScope method is :

System.NullReferenceException: Object reference not set to an instance of an object.
   at MySql.Data.MySqlClient.NativeDriver.GetResult(Int32& affectedRow, Int32& insertedId)
   at MySql.Data.MySqlClient.Driver.GetResult(Int32 statementId, Int32& affectedRows, Int32& insertedId)
   at MySql.Data.MySqlClient.Driver.NextResult(Int32 statementId)
   at MySql.Data.MySqlClient.MySqlDataReader.NextResult()
   at MySql.Data.MySqlClient.MySqlDataReader.Close()
   at MySql.Data.MySqlClient.MySqlCommand.ResetReader()
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader()
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteNonQuery()
   at MySql.Data.MySqlClient.MySqlTransaction.Rollback()
   at MySql.Data.MySqlClient.MySqlTransactionScope.Rollback(SinglePhaseEnlistment singlePhaseEnlistment)
   at MySql.Data.MySqlClient.MySqlPromotableTransaction.System.Transactions.IPromotableSinglePhaseNotification.Rollback(SinglePhaseEnlistment singlePhaseEnlistment)
   at System.Transactions.DurableEnlistmentAborting.EnterState(InternalEnlistment enlistment)
   at System.Transactions.DurableEnlistmentActive.InternalAborted(InternalEnlistment enlistment)
   at System.Transactions.TransactionStateAborted.EnterState(InternalTransaction tx)
   at System.Transactions.TransactionStateActive.Rollback(InternalTransaction tx, Exception e)
   at System.Transactions.Transaction.Rollback()
   at System.Transactions.TransactionScope.InternalDispose()
   at System.Transactions.TransactionScope.Dispose()
   at WindowsFormsApplication1.Form1.DoTransactionScope() in E:\TestSite\TestSite\WindowsFormsApplication1\Form1.cs:line 60
[10 Jan 2011 11:42] Tonci Grgin
Of course you do since you do not have try...catch in failing code and real exception get's swallowed:
                try {
                    cmd.ExecuteNonQuery();
                }
                    catch (Exception ee)
                {
                    throw;
                }
So please guard your code where error is more than likely to be thrown.

"Need feedback" status is set until I decide on whether NRE should be thrown or not by the connector.
[10 Jan 2011 11:52] Charles Loh
Hi,

Thank you for your clarification.

The reason for not catching the exception in DoInsertTable2 (DataAccessLayer) is that we are allowing the exception be propagated to the BusinessLogicLayer to be handled accordingly.

I've made the modifications to DoInsertTable2 as follows but am still receiving the reported exception in DoTransactionScope:

            try
            {
                using (MySqlConnection conn = new MySqlConnection(GetTestDBConn()))
                using (MySqlCommand cmd = conn.CreateCommand())
                {
                    conn.Open();
                    cmd.CommandText = @"insert into t2 (Val) values (?value)";
                    cmd.CommandTimeout = 5;
                    cmd.Parameters.AddWithValue("?value", new Random().Next());
                    cmd.ExecuteNonQuery();
                }
            }
            catch (Exception)
            {
             //Swallow the exception.  For Simulation purpose only
            }
[10 Jan 2011 12:07] Tonci Grgin
Charles, if you deliberately choose to swallow the exception, why are you wondering? I guess after all these notes, what you actually want is for swallowed exception to be re-thrown after encountering new exception, right? I do not think this is possible but will recheck.
[10 Jan 2011 12:09] Tonci Grgin
What I'd do is replace the:
 //Swallow the exception.  For Simulation purpose only
with
  throw (my custom exception that will be processed in BL layer)
[10 Jan 2011 12:30] Charles Loh
Hi,

To clarify further, in the CallingMethod below, execution will not reach A as the DivisionByZero exception has been swallowed at B and will not be propagated up to the caller.

Hence, in the test case, DoTransactionScope which is similar to the CallingMethod() below should not be encountering any exception.

==================================
public void CallingMethod()
{
  try
  {
    WorkMethod();
  }
  catch(Exception)
  {
     //A:Execution will never reach here
  }
}

public void WorkMethod()
{
   try
   {
     int a = Convert.ToInt32(textbox1.text);  //UserInput
     int b = Convert.ToInt32(textbox2.text);  //UserInput
     int c = a/b;
   }
   catch (Exception)
   {    
      //B:swallow any exception to allow program execution to continue.
   }
}
[10 Jan 2011 13:11] Tonci Grgin
Wrong, it should. TS.complete will call on COMMIT which can not be as other user is holding the lock, as I already explained.
[10 Jan 2011 13:42] Charles Loh
Hi,

Thank You for being patient and the clarification.  Per your feedback, I've made some changes to the test case for testing and am observing the following:

1) You mentioned "throw (my custom exception that will be processed in BL layer)".  I've implemented and throw a custom DataAccessException but am unable to catch it in the calling method but NullReferenceException is caught instead.

2) For testing, I've intentionally removed the line scope.Complete(); in DoTransactionScope() and am also getting the same NullReferenceException.
[10 Jan 2011 13:51] Charles Loh
To add on, in the test case provided, DoInsertTable2 is throwing TimeoutException which gets propagated to the caller DoTransactionScope and execution continues from the catch section (due to the exception).  As such, scope.Complete() is not called.
[11 Jan 2011 12:22] Bogdan Degtyariov
It is expected that application might throw exception when trying to update a locked table, but exception should not be NullReferenceException.

Looks like a bug in Connector/NET. Verified with 6.3.5 and 6.3.6.
[11 Jan 2011 12:27] Charles Loh
Hi Bogdan,

Thank You for the due dilligence in understanding the issue at hand.
[18 Feb 2011 15:28] Reggie Burnett
Fixed in 6.2.5 and 6.3.7+
[11 Mar 2011 16:05] Tony Bedford
An entry has been added to the 6.2.5 and 6.3.7 changelogs: 

A NullReferenceException was thrown on disposal of a TransactionScope object.
[20 Jan 2015 10:45] Petru Simon Mot
Hi,
Not sure if the bug has reappeared or I'm doing it wrong. A mysql timeout exception (generated by a long lock) is swallowed by the TransactionScope's Dispose().  I'm using 6.7.5
These is the code:

// transaction scope factory method
        public static TransactionScope CreateReadCommitted()
        {
            var options = new TransactionOptions
                {
                    IsolationLevel = IsolationLevel.ReadCommitted,
                    Timeout = new TimeSpan(0, 3, 0)
                };

            return new TransactionScope(TransactionScopeOption.Required, options);
        }

//the transactional part:

using (var ts = TransactionScopeBuilder.CreateReadCommitted())
{

// various db updates, one of which failed because of the timeout

}

Inside the using block I am able to catch a DbUpdateException with all relevant errors in innerExceptions.
Outside that block however, I'm only getting a NullReferenceException with no inner exception.

Is this the expected behaviour?

My workaround was to create a wrapper around TransactionScope that deals with the NRE in Dispose.

Thanks,
Petru
[19 Jul 2016 22:27] James Day
Reggie,

This bug was believed to be Fixed in 6.2.5 and 6.3.7+ but we're still seeing reports of something similar in 6.9.7 in public bug http://bugs.mysql.com/bug.php?id=61094 that was asserted by Julio Casal to be a duplicate of this one. Can you take a look at both and maybe update the bugs to say in public comment whether it's still believed to be a duplicate or if not, what would be helpful to you to sort out what's happening now?
[15 Feb 2019 10:33] Davyd McColl
This is most definitely still happening.

I've tested with:
- 6.10.7
- 6.10.8
- 8.0.15

It _doesn't_ happen with the more venerable 6.9.12, however, there are other issues we're experiencing with that version (looks like some connections are abandoned, and after some time, MySQL gets unhappy with the number of dead connections).

full stack trace:

System.NullReferenceException : Object reference not set to an instance of an object.
   at MySql.Data.MySqlClient.NativeDriver.ExecutePacket(MySqlPacket packetToExecute)
   at MySql.Data.MySqlClient.NativeDriver.SendQuery(MySqlPacket queryPacket)
   at MySql.Data.MySqlClient.Driver.SendQuery(MySqlPacket p)
   at MySql.Data.MySqlClient.Statement.ExecuteNext()
   at MySql.Data.MySqlClient.PreparableStatement.Execute()
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteNonQuery()
   at MySql.Data.MySqlClient.MySqlTransaction.Rollback()
   at MySql.Data.MySqlClient.MySqlTransactionScope.Rollback(SinglePhaseEnlistment singlePhaseEnlistment)
   at MySql.Data.MySqlClient.MySqlPromotableTransaction.System.Transactions.IPromotableSinglePhaseNotification.Rollback(SinglePhaseEnlistment singlePhaseEnlistment)
   at System.Transactions.DurableEnlistmentAborting.EnterState(InternalEnlistment enlistment)
   at System.Transactions.TransactionStateAborted.EnterState(InternalTransaction tx)
   at System.Transactions.Transaction.Rollback()
   at System.Transactions.TransactionScope.InternalDispose()
   at System.Transactions.TransactionScope.Dispose()
   at Tests.Bugs.MySqlTests.Bug_WhenTimeoutExceptionOccursInTransaction_ShouldBeAbleToExecuteAnotherQuery() in C:\code\codeo\Clyral.RabbitBus\Tests\Bugs\MySQLTests.cs:line 54

with the following test code:

        [Test]
        public void Bug_WhenTimeoutExceptionOccursInTransaction_ShouldBeAbleToExecuteAnotherQuery()
        {
            var connectionString = ConfigurationManager.ConnectionStrings["Tests"].ConnectionString;
            var datastore = new MySQLDataStore(connectionString);

            var queueName = "mysqltest";
            datastore.InitialiseForQueue(queueName);

            using (var scope = TransactionScopes.ReadCommitted())
            {
                datastore.Publish(queueName, new DurableMessage()
                {
                    Body = "Test",
                    MessageId = Guid.NewGuid(),
                    Queue = queueName
                });

                try
                {
                    using (var connection = new MySqlConnection(connectionString))
                    {
                        connection.Query("SELECT * FROM `queue_messages_mysqltest` FOR UPDATE; SELECT SLEEP(5)", commandTimeout: 1);
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("Timeout occured as expected");
                }
            }

            using (var connection = new MySqlConnection(connectionString))
            {
                var result = connection.Query<int>("SELECT id FROM `queue_messages_mysqltest`");
                Assert.IsFalse(result.Any());
            }
        }

This is a pretty big deal and has been in limbo for some time. PLEASE can someone put some effort into fixing it.