Click here to monitor SSC
Av rating:
Total votes: 12
Total comments: 3


Robert Chipperfield
Exceptionally Mysterious
11 September 2008

If you are adept in puzzling over the cryptic messages in your stack trace, when a .NET exception is thrown, you may be surprised when they start  talking complete nonsense. Is it really possible that two threads could build the stack trace on the same exception object at the same time?

One of the lovely things about .NET is its exception handling - it allows a method being called to signal that something has gone wrong, and better yet, it provides a stack trace showing exactly where that error occurred. Great for debugging!

Since it's neither practical nor sensible to conduct testing with a debugger attached, a lot of our applications log any exceptions to a file on disk, allowing the developers to look back after a crash has happened, and dis­sect what went wrong. Usually, this works well...


Nigel earnestly ponders the stack trace

The other day, a rather confused Nigel wandered over with an interesting problem: he had a log of an excep­tion where the stack trace showed a very different path through the code than the message that accompanied it. We checked for all the obvious stupidities (over-zealous use of copy and paste), and ruled out the possibility of it being a side-effect of our obfuscator by reproducing the bug in a debug build.

One thing that raised alarm bells was that two exceptions, both with the same message but different stack traces, were happening at almost exactly the same time: race condition alert! We tried to blame it on the logg­ing framework, but to no avail: everything was correctly synchronised as necessary. Back to the drawing board.

In an attempt to reproduce it more quickly, I wrote a little test application:


class FirstTry
{
    
const int c_Threads = 10;
    
const int c_Iterations = 20;

    
static void Main(string[] args)
    
{
        Thread[] threads
= new Thread[c_Threads];
        
for (int i = 0; i < threads.Length; i++)
        
{
            
if (i % 2 == 0)
                
threads[i] = new Thread(new ThreadStart(Method1));
            
else
                
threads[i] = new Thread(new ThreadStart(Method2));
            
threads[i].Start();
        
}
        
for (int i = 0; i < threads.Length; i++)
            
threads[i].Join();
        
Console.ReadLine();
    
}

    
static object s_Lock = new object();

    
static void Method1()
    
{
        
for (int i = 0; i < c_Iterations; i++)
        
{
            
try
            
{
                Method1Callee
();
            
}
            
catch (Exception ex)
            
{
                
lock (s_Lock)
                
{
                    Console.WriteLine
( "---- Exception in Method 1 ---- ");
                    
Console.WriteLine(ex);
                
}
            }
        }
    }

    
static void Method1Callee()
    
{
        
throw new ArgumentException( "M1 ");
    
}

    
static void Method2()
    
{
        
for (int i = 0; i < c_Iterations; i++)
        
{
            
try
            
{
                Method2Callee
();
            
}
            
catch (Exception ex)
            
{
                
lock (s_Lock)
                
{
                    Console.WriteLine
( "---- Exception in Method 2 ---- ");
                    
Console.WriteLine(ex);
                
}
            }
        }
    }

    
static void Method2Callee()
    
{
        
throw new ArgumentException( "M2 ");
    
}
}

This worked exactly as you'd expect: "Exception in M1" was always followed by a stack trace showing Method1Callee() on top, followed by Method1(), and likewise for Method2.

With the strangeness continuing, the next step was to use the same cause of the exception as Nigel was seeing in his application: a failed SqlConnection.Open() call. So, the test application was modified:

static void Method1Callee()
{
    
using (SqlConnection sc = new SqlConnection())
    
{
        SqlConnectionStringBuilder scb
= new SqlConnectionStringBuilder();
        
scb.DataSource = "127.0.0.1:12345";
        
sc.ConnectionString = scb.ConnectionString;
        
sc.Open();
    
}
}

static void Method2Callee()
{
    
using (SqlConnection sc = new SqlConnection())
    
{
        SqlConnectionStringBuilder scb
= new SqlConnectionStringBuilder();
        
scb.DataSource = "127.0.0.1:12345";
        
sc.ConnectionString = scb.ConnectionString;
        
sc.Open();
    
}
}

Here's the sort stack trace we expected to get:

---- Exception in Method 1 ----
System.Data.SqlClient.SqlException: An error has occurred while establishing
a connection to the server.  When connecting to SQL Server 2005, this
failure may be caused by the fact that under the default settings SQL Server
does not allow remote connections. (provider: Named Pipes Provider, error:
40 - Could not open a connection to SQL Server)
   at System.Data.ProviderBase.DbConnectionPool.GetConnection
        (DbConnection owningObject)
   at System.Data.ProviderBase.DbConnectionFactory.GetConnection
        (DbConnection owningConnection)
   at System.Data.ProviderBase.DbConnectionClosed.OpenConnection
        (DbConnection outerConnection, DbConnectionFactory connectionFactory)
   at System.Data.SqlClient.SqlConnection.Open()
   at STExceptionThreading.SecondTry.Method1Callee() in SecondTry.cs:line 58
   at STExceptionThreading.SecondTry.Method1() in SecondTry.cs:line 38

But here's what we actually got, in quite a few cases:

---- Exception in Method 1 ----
System.Data.SqlClient.SqlException: An error has occurred while establishing
a connection to the server.  When connecting to SQL Server 2005, this
failure may be caused by the fact that under the default settings SQL Server
does not allow remote connections. (provider: Named Pipes Provider, error:
40 - Could not open a connection to SQL Server)
   at System.Data.ProviderBase.DbConnectionPool.GetConnection
        (DbConnection owningObject)
   at System.Data.ProviderBase.DbConnectionClosed.OpenConnection
        (DbConnection outerConnection, DbConnectionFactory connectionFactory)
   at STExceptionThreading.SecondTry.Method2() in SecondTry.cs:line 68
   at System.Data.SqlClient.SqlConnection.Open()
   at System.Data.ProviderBase.DbConnectionClosed.OpenConnection
        (DbConnection outerConnection, DbConnectionFactory connectionFactory)
   at System.Data.ProviderBase.DbConnectionFactory.GetConnection
        (DbConnection owningConnection)
   at System.Data.ProviderBase.DbConnectionClosed.OpenConnection
        (DbConnection outerConnection, DbConnectionFactory connectionFactory)
   at STExceptionThreading.SecondTry.Method1() in SecondTry.cs:line 38
   at STExceptionThreading.SecondTry.Method2Callee() in SecondTry.cs:line 88
   at System.Data.SqlClient.SqlConnection.Open()
   at STExceptionThreading.SecondTry.Method2Callee() in SecondTry.cs:line 88
   at System.Data.SqlClient.SqlConnection.Open()
   at System.Data.ProviderBase.DbConnectionClosed.OpenConnection
        (DbConnection outerConnection, DbConnectionFactory connectionFactory)
   at STExceptionThreading.SecondTry.Method2() in SecondTry.cs:line 68
   at System.Data.SqlClient.SqlConnection.Open()
   at STExceptionThreading.SecondTry.Method1Callee() in SecondTry.cs:line 58
   at STExceptionThreading.SecondTry.Method2() in SecondTry.cs:line 68
   at STExceptionThreading.SecondTry.Method2() in SecondTry.cs:line 68
   at STExceptionThreading.SecondTry.Method1() in SecondTry.cs:line 38

Surely not? Method2 never calls itself, and certainly not Method1. And SqlConnection.Open calling back into Method2? Madness!

And in any case, why was this example showing the behaviour, but my initial application that was throwing ArgumentExceptions not? Time to drag Andrew, master of all that is deep and dark in the .NET Framework (well, one of developers of ANTS Profiler 4, which kind of requires that kind of knowledge!)


... master of all that is deep and dark in the .NET Framework...

Much head scratching later, he suggested that the same stack trace was getting filled in on multiple threads simultaneously, resulting in the garbage we were seeing. But the exception's only being used on one thread, we said!

.NET said otherwise. We added some code to the catch block that stored the exception object in a list of "previously seen exceptions", and then did a reference comparison (Object.ReferenceEquals()) of the newly thrown exception with all the ones we'd seen before. And then we saw it...

The exceptions being thrown in SqlConnection.Open() were being re-used between multiple calls. At this point, everything fell into place. Of course the code that builds up the stack trace string isn't thread safe - why would it need to be? What we were seeing was the result of two threads concurrently building the stack trace on the same exception object, leading to an interesting mix of the two real stack traces.

I'm no expert in how the SQL Native Client library works, or whether it's some artefact of some unmanaged interop somewhere, but this just seems wrong to me. If it's deliberate re-use of exception objects, it seems like plain bad programming practice. If it's an artefact of some COM calls somewhere, well, that raises the question of where else this might happen. I go away from this somewhat less confident that my stack traces are telling the truth in the future!



This article has been viewed 4180 times.
Robert Chipperfield

Author profile: Robert Chipperfield

Robert is a software engineer at Red Gate, where he's worked since September 2006 on a wide range of products, including SQL Doc, SQL Data Compare, SQL Log Rescue, SQL Multi Script, and ANTS Profiler. He is currently working on the new Exchange Server Archiver tool, which should be heading towards release at the start of 2009. Outside of work, he enjoys amateur radio, electronics, and of course the usual assortment of computer-related technologies, from hardware all the way through to high-level software

Search for other articles by Robert Chipperfield

Rate this article:   Avg rating: from a total of 12 votes.


Poor

OK

Good

Great

Must read
 
Have Your Say
Do you have an opinion on this article? Then add your comment below:
You must be logged in to post to this forum

Click here to log in.


Subject: argh!
Posted by: Nigel Morse (view profile)
Posted on: Friday, September 12, 2008 at 7:02 AM
Message: This was decidedly weird. I was even trying to go through the obfuscated code trying to see if something had happened there. Not what you want 2 days before code freeze!

Subject: "Nigel earnestly ponders the stack trace"
Posted by: jason.cook (view profile)
Posted on: Friday, September 12, 2008 at 8:03 AM
Message: I wish he had been pondering the stack trace... I might have won the game then!

(For those that have realised there isn't a stack trace in Nigel's photo, he was in the middle of a game of Lost Cities during our lunch break)

Subject: Confirming this problem
Posted by: ccady (view profile)
Posted on: Thursday, October 08, 2009 at 1:44 PM
Message: I was tracing through the logs of one of our high-volume applications and found this exact problem. Two SqlConnections attempting to be created at the same time, both timing out, and on one of the threads, the stack trace looked fine, and on the other, the stack trace was completely wrong -- there were three entries for the same line of code (the same undocumented System.Data.ProviderBase.DbConnectionClosed.OpenConnection
method you see above), and another which was obviously not in that call at all.

Thank you, Robert, for your analysis. I'm not sure what can be done, but at least now there is a big comment in the code which says that this stack trace might be rubbish, and a link back to this article.

 






recommended site pinvoke

PInvoke.net is a user-driven wiki which provides .NET developers with native method signatures, so they don't have to spend time writing them from scratch.




TortoiseSVN and Subversion Cookbook Part 3: In, Out, and Around
 Subversion doesn't have to be difficult, especially if you have Michael Sorens's guide at hand. After... Read more...

Feature Usage Reporting in Early Access Programs
 After doing Web development, you can get very used to the luxury of having basic information about your... Read more...

Feature Usage Reporting in Early Access Programs
 After doing Web development, you can get very used to the luxury of having basic information about your... Read more...

TLS/SSL and .NET Framework 4.0
 The Secure Socket Layer is now essential for the secure exchange of digital data, and is most generally... Read more...

SmartAssembly: Eating Our Own Dogfood
 Quite often at Red Gate, we are some of our own most enthusiastic software-users. SmartAssembly is a... Read more...

A Complete URL Rewriting Solution for ASP.NET 2.0
 Ever wondered whether it's possible to create neater URLS, free of bulky Query String parameters?... Read more...

Visual Studio Setup - projects and custom actions
 This article describes the kinds of custom actions that can be used in your Visual Studio setup project. Read more...

.NET Application Architecture: the Data Access Layer
 Find out how to design a robust data access layer for your .NET applications. Read more...

Web Parts in ASP.NET 2.0
 Most Web Parts implementations allow users to create a single portal page where they can personalize... Read more...

Configuring Forms Authentication in SharePoint 2007
 Damon Armstrong provides a step-by-step guide to the processes, quirks and pitfalls of setting up... Read more...

Over 400,000 Microsoft professionals subscribe to the Simple-Talk technical journal. Join today, it's fast, simple, free and secure.

Join Simple Talk