views:

265

answers:

4

Ok this looks like a major fundamental bug in .NET:

Consider the following simple program, which purposely tries to connect to a non-existent database:

class Program
{
    static void Main(string[] args)
    {            

        Thread threadOne = new Thread(GetConnectionOne);
        Thread threadTwo = new Thread(GetConnectionTwo);            
        threadOne.Start();
        threadTwo.Start();

    }



    static void GetConnectionOne()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection("Data Source=.\\wfea;Initial Catalog=zc;Persist Security Info=True;Trusted_Connection=yes;"))
            {
                conn.Open();
            }    
        } catch (Exception e)
        {
            File.AppendAllText("ConnectionOneError.txt", e.Message + "\n" + e.StackTrace + "\n");
        }

    }


    static void GetConnectionTwo()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection("Data Source=.\\wfea;Initial Catalog=zc;Persist Security Info=True;Trusted_Connection=yes;"))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            File.AppendAllText("ConnectionTwoError.txt", e.Message + "\n" + e.StackTrace + "\n");
        }

    }
}

Run this program and set breakpoints on the catch blocks. The DBConnection object will attempt to connect for 15 seconds (on both threads), then it will throw an error. Inspect the exception's stack trace, and the stack trace will have TWO call stacks intermingled, as follows:

at System.Data.ProviderBase.DbConnectionPool.CreateObject(DbConnection owningObject)
at System.Data.ProviderBase.DbConnectionFactory.GetConnection(DbConnection owningConnection)
at System.Data.ProviderBase.DbConnectionClosed.OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory)
at System.Data.ProviderBase.DbConnectionPool.UserCreateRequest(DbConnection owningObject)
at System.Data.ProviderBase.DbConnectionPool.GetConnection(DbConnection owningObject)
at System.Data.SqlClient.SqlConnection.Open()
at ZoCom2Test.Program.GetConnectionOne() in C:\src\trunk\ZTest\Program.cs:line 38
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 ZoCom2Test.Program.GetConnectionTwo() in C:\src\trunk\ZTest\Program.cs:line 54

You may have to try it several times to get this to happen, but I'm getting this to happen right now on my machine. How is this possible? This should be totally impossible at the VM level. It looks like the DBConnection.Open() function is simultaneously throwing the same exception on two threads at once, or something bizarre like that.

+1  A: 

You are getting the same exception thrown. I don't understand why however. Have a look at the output window, notably that exception1 == exception2.

class ThreadingBug
{
    private const string CONNECTION_STRING =
        "Data Source=.\\wfea;Initial Catalog=catalog;Persist Security Info=True;Trusted_Connection=yes;";

    static void Main(string[] args)
    {
        try
        {
            Thread threadOne = new Thread(GetConnectionOne);
            Thread threadTwo = new Thread(GetConnectionTwo);
            threadOne.Start();
            threadTwo.Start();

            threadOne.Join(20000);
            threadTwo.Join(20000);

            Debug.WriteLine("Same?" + (exception1 == exception2));
        }
        catch (Exception e)
        {
            Debug.WriteLine("error main" + e);
        }
    }

    static Exception exception1;

    static void GetConnectionOne()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            Debug.WriteLine("Error Con one" + e);
            exception1 = e;
        }
    }
    static Exception exception2;

    static void GetConnectionTwo()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            Debug.WriteLine("Error Con two" + e);
            exception2 = e;
        }
    }
}

Edit: The below was my original response.

It's very likely your "random" filenames are similar, if not the same, as they will sometimes be called within very close timeframes. Often, when you have a problem that randomly appears, and you have a Random.Next call, it should be the first place you look.

Gregory
Yes, in the sample he posted Random.Next() will produce the same number, but he claims that the output is intermingled even if you write to two different files.
Esteban Araya
I've tried it without the random filename generation. I just used a different filename in each respective catch block. The two files written EACH have TWO intermingled stack traces written in them.
nganju
Yes, I'm seeing the same thing now. Trying to understand the reflected code in .Open. I note that SqlConnection instances are not thread safe, but that shouldn't be a problem here.
Gregory
@Gregory, thanks for your help on this question. One quick favor, can you edit your response and remove the company name from the code snippet? In the db connection string, it's the word that starts with 'z' and ends with 'c'. Thanks!
nganju
+3  A: 

Try this instead, and see what happens:

class ThreadingBug
{
    private const string CONNECTION_STRING =
        "Data Source=.\\wfea;Initial Catalog=catalog;Persist Security Info=True;Trusted_Connection=yes;";

    static void Main(string[] args)
    {
        try
        {
            Thread threadOne = new Thread(GetConnectionOne);
            Thread threadTwo = new Thread(GetConnectionTwo);
            threadOne.Start();
            threadTwo.Start();

            threadOne.Join(2000);
            threadTwo.Join(2000);
        }
        catch (Exception e)
        {
            File.AppendAllText("Main.txt", e.ToString());
        }
    }

    static void GetConnectionOne()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            File.AppendAllText("GetConnectionOne.txt", e.ToString());
        }
    }

    static void GetConnectionTwo()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            File.AppendAllText("GetConnectionTwo.txt", e.ToString());
        }
    }
}

I believe there is a bug here, though it's neither major, nor fundamental. After working to narrow this down (and to do things like removing one thread), it looks like the same instance of the Exception class is thrown by the Connection Pool implementation on both threads (kudos to Gregory for discovering this). This sometimes shows up as a corrupt ("intermingled") stack trace, and sometimes simply as the same stack trace on both threads, even when the code is quite different between the two threads.

Commenting out one of the Thread.Start calls shows an entirely different stack trace, demonstrating that the odd part is in the connection pool implementation - the odd stack traces are being handed out by the connection pool, since both threads use the same connection string and credentials.

I've submitted a Connect issue on this at https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=522506. Everyone should feel free to vote on how important (or unimportant) you feel it is, whether you can reproduce it, or whether you have a workaround. This will help Microsoft prioritize a fix.


Update: The Connect issue has been updated. Microsoft acknowledges it as a bug, and plans to fix it in a future release.

Thanks to nganju, Gregory, and everyone else who participated in solving this problem. It was indeed a bug, and it will be fixed, and it's because of us.

John Saunders
Unusual. I get the same exception in both files, stack trace doesnt seem merged, but is only for the GetConnectionOne() call.
Gregory
This reproduces the bug. I ran your code, and got TWO intermingled exceptions in EACH of the files created.
nganju
They're probably not "intermingled", you probably just don't understand the single exception. Remove one of the AppendAllText lines, delete both files, and try again. I just tried it with a known-good connection string, and got no exceptions. The problem is with your connection string.
John Saunders
Also, try File.WriteAllText instead.
John Saunders
The problem is with a non-working connection string, the exceptions stack traces seem to be merged.
Gregory
Let's see if they're merged if the problem is simplified to remove appending as an issue.
John Saunders
I know the connection string is no good. That's on purpose. The point is that no matter how bad your code behaves, there should never be two stack traces intermingled in one exception. Try it with a bad connection string. I'm not confused about the single stack trace, look at the stack trace I put in the original post and tell me that it doesn't look like two intermingled stack traces. This is the stack trace I get even with your code.
nganju
@nganju: did you think there was a guarantee that `file.AppendAllText` was atomic, or that new Random().Next() would always produce a unique value? Neither is true. Need unique filenames, use Guid.NewGuid().ToString(). Otherwise, interlock writes to the log file. In either case, it's better to use System.Diagnostics features for logging.
John Saunders
John, I did everything you said. Deleted both error files, removed the File.AppendAllText from the second exception handler, recompiled and ran from scratch. Now I get one error file with the exact stack trace I posted in the original post.
nganju
Are you still getting intermingled traces using Exception.ToString()? Even when you remove one File.AppendAllText?
John Saunders
What happens when you remove one Thread.Start?
John Saunders
Yes. Furthermore two other users on here are confirming that they can reproduce this.
nganju
And which .NET Framework version are you running?
John Saunders
Removing one Thread.start() obviously gets rid of the problem. But it doesn't resolve the issue that it's possible to get two intermingled stack traces in one exception.
nganju
I'm using .Net 3.5
nganju
Do you get it when you don't use ex.StackTrace, but instead use ex.ToString(). I just tried it with a deliberately bad connection string in .NET 3.5, and get no intermingling. Please paste the File.AppendAllText (or WriteAllText) code that you use.
John Saunders
Have a look at my answer John. The exceptions are ==, which seems very unusual. It's beyond my understanding now, so hopefully you or someone else can take it to a conclusion (the curiosity is killing me)
Gregory
Yes, I'm using your exact version of the code you just posted, so I'm using ToString(). All I've modified is I removed one of the File.AppendAllText from one exception handler. Check your stack trace very carefully, make sure no single method is repeated twice as there should be no recursion in this particular stack trace.
nganju
I'm now seeing "intermingling". It has nothing to do with threads. The code in the two threads is almost identical. It looks like the C# compiler optimizes it by pulling out the common code. Compare the two stack traces line by line, and I think you'll see. The exceptions are the same because it's literally the same code executing. Turn off optimization and try again.
John Saunders
BTW, did you ever actually remove the Thread.Start? You'd have had to try removing each one individually, but if you had done that, I think you would have seen this had nothing to do with threads.
John Saunders
The "Optimize Code" checkbox in my Properties->Build screen was already unchecked. And it's not literally the same code that's executing, you can see the two different line numbers that each call DBConnection.Open() if you look at my original posted stack trace. Oddly I don't ALWAYS get those two source lines, but I get them 3 out of 4 times. Also, I'm not complaining about two stacks being the same, I'm complaining about ONE exception having two intermingled stacks. I think you'll agree it's hard to read that trace. I doubt Microsoft intended to produce such an unreadable stack trace.
nganju
Although I'll still be surprised to find that this is a somehow "intermingled" stack trace, I have to admit that I'm unable to explain the trace. In particular, I'm betting we're seeing the actual stack traces - and that they are both weird.
John Saunders
BTW, I am agreeing with ChaosPandion now. I think this is a bug in the connection pool. I think that Gregory found the same exception instance because the connection pool code is using a static Exception instance somewhere, and is lacking synchronization.
John Saunders
Also, did you ever try this one thread at a time? I did, and the results were revealing.
John Saunders
Even if the StackTrace was a member variable of the Exception object, then if two threads simultaneously set the value of it, it would end up with the value of the last thread that set it. It would still not be an intermingled mix of both threads' stacks. See my last comment above.
nganju
**sigh** you're assuming the value is _set_ and not just _used_. If `StackTrace` were an `List<StackTraceEntry>` and two threads were manipulating it, that would have this effect. Besides, I often now see the _identical_ stack trace, when it can't possibly be identical. Ive changed my code so the two threads are quite different. There's no way this is due to code optimization (I also looked at the IL to confirm this). I think one thread encounters the exception, and the same exception is returned to the second. At least when I see same trace.
John Saunders
BTW, you should try it one thread at a time and note the difference in the stack traces. It will shake up your assumptions.
John Saunders
Ok I agree with that. Still, you're saying if your db connection code results in an exception, the stack trace in that exception may be from someone else's completely different thread, with a different stack of methods. Even the method from which you caught the exception would not be listed in this call stack. That's confusing, to say the least. I'm calling this a BUG.
nganju
I've submitted a Connect issue on this at https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=522506. Everyone should feel free to vote on how important you feel it is; whether you can reproduce it or whether you have a workaround. This will help Microsoft prioritize a fix.
John Saunders
BTW, did you ever try removing one of the Thread.Start calls?
John Saunders
No idea on the voting issue, sorry.
Marc Gravell
@Marc: ok, I get it. In retrospect, I think I confused this question with another that actually had upvotes.
John Saunders
Hi John,@John Saunders , I've marked this answer as correct because I think it is the most informative. One favor to ask, can you please edit this and remove the company name from the DB connection string? The word that starts with a 'z' and ends with a 'c'. Thanks!
nganju
+3  A: 

OK, I managed to reproduce this (VS2008, FX3.5SP1, dual core) both inside and outside(*) the debugger. And after altering your catch logic a little it even is reliably reproducable. And, like Gregory mentioned, it is the same exception instance thrown in both threads.

This should be totally impossible at the VM level.

Where did you get that idea?

Both threads are trying to connect through the connection pool. I don't know anything about how the Pool works, but I'll take a guess: It is serializing the 2 simultaneous requests. That sounds like being nice to the Server. And then when the attempt fails it has 1 exception and 2 waiting threads.

I too would have expected the CLR or the ConnectionPool to duplicate the exception and prepend 2 separate stacktraces but instead it merges the 2 calling traces.

So I think your bug could very well be feature, status: by design.
Because it is not really an 'intermingled' stacktrace but more of a deliberately Y-shaped one. It does not look accidental.

It would be nice if somebody found a reference for this behaviour though. Right now I'm not sure if this is a CLR or a ConnectionPool 'feature'.

(*) Edit: I think I saw it outside the debugger once, but now I'm unable to reproduce that. So it could be a debugger or a timing issue.

Henk Holterman
I don't think this should be the approved behavior. If I'm on thread 1, why should I get information about how thread 2 got to the same exception that I got to? I've got a whole stack trace with all the method calls about a completely different thread! This seems like it should be outlawed purely from a security perspective.
nganju
I think it's a bug or sub-optimal design of the connection pool. Somehow, it's using the same exception instance on both threads. I think there's only a single exception thrown. In the same way it would have returned the successful connection to the second thread, it's returning the failed exception to the second thread. The two stack traces are identical in my code, because they're the identical exception, as Gregory found.
John Saunders
I promise you I'm getting an intermingled stack trace, not one stack trace or the other. I'll do a screen share with you if you want.
nganju
nganju, we agree on the merged stacktrace. And where is the security risk?
Henk Holterman
I'm not getting it "intermingled" I'm getting it _identical_, and, yes, i consider that a bug. I don't get that it's a security bug, since both traces will be from the same connection pool (maybe same AppDomain). I don't see one thread having any secrets from the other in terms of its stack trace. A Security expert can correct me on that...
John Saunders
Well ok. I'm just glad we're no longer blaming my pitiful understanding of threading, and how hopefully inferior it is to Microsoft's understanding. There's not a bug in the VM, but there is a bug in the framework after all.
nganju
I don't see this as a bug at all. The stack trace has to be merged like this to show you exactly the state the code was in when the problem occurred.
tster
There are two threads, so there should be two stack traces, no?
John Saunders
John, "there should be two" - that's the question now. Should there be two distinct exception for 1 failure?
Henk Holterman
Two different stack traces discovered the failure. Perhaps there should be the sort of "server exception" one sees in Remoting. One from the thread up to the connection pool; one from the Connection pool showing what failed.
John Saunders
I cannot believe that the two stack traces would merge by anything other than a corrupt read.
ChaosPandion
Chaos, it's not just 2 stacktraces, both threads uses the _same_ exception instance.
Henk Holterman
+3  A: 
ChaosPandion
Where is that line located? I think you're right, and that it's using a static Exception instance.
John Saunders
If the connection pool has a pointer to an exception, then there may be a race condition where one exception gets pointed to instead of another. But it doesn't explain how a single exception has two intermingled stack traces inside it.
nganju
It's the same exception instance, and is being operated upon by two threads at the same time, with no synchronization - that explains it completely.
John Saunders
You're saying two threads are setting the stack trace on the same exception instance? Is there a SetStackTrace(string input) method? Even if there was, it would still be an atomic setting of the StackTrace string. The only way two threads could produce an intermingled stack trace is if they were somehow writing to a stream that was going into the StackTrace member variable in the Exception object. Doesn't seem likely.
nganju
@nganju: did you know it's possible to look at the actual .NET code? We don't need to speculate. And pay attention to the fact that Gregory and I saw the identical Exception instance. The same object was thrown from both threads.
John Saunders
Please add some commentary showing why the code you added shows what you say it does.
John Saunders
I cannot do this... because it doesn't do what I thought it did. :)
ChaosPandion
I think it is time to take off my coding hat and go to sleep.
ChaosPandion
Goodnight, but you're on the hook for tomorrow. ;-)
John Saunders
Chaos, how do you get from a static method to a reace condition so quickly? MS's guideline is that all static methods should be thread-safe.
Henk Holterman
Never believe everything you are told. (keyword *should*)
ChaosPandion