6

Note: I've went through millions of questions when the issue is not disposing the reader/connection properly, or when the error is because of badly handled lazy loading. I believe that this issue is a different one, and probably related to MySQL's .NET connector.

I'm using MySQL server (5.6) database extensively through its .NET connector (6.8.3). All tables are created with MyISAM engine for performance reasons. I have only one process with one thread (update: in fact, it's not true, see below) accessing the DB sequentially, so there is no need for transactions and concurrency.

Today, after many hours of processing the following piece of code:

public IEnumerable<VectorTransition> FindWithSourceVector(double[] sourceVector)
{
    var sqlConnection = this.connectionPool.Take();

    this.selectWithSourceVectorCommand.Connection = sqlConnection;

    this.selectWithSourceVectorCommand.Parameters["@epsilon"].Value
        = this.epsilonEstimator.Epsilon.Min() / 10;

    for (int d = 0; d < this.dimensionality; ++d)
    {
        this.selectWithSourceVectorCommand.Parameters["@source_" + d.ToString()]
        .Value = sourceVector[d];
    }

    // *** the following line (201) throws the exception presented below
    using (var reader = this.selectWithSourceVectorCommand.ExecuteReader())
    {
        while (reader.Read())
        {
            yield return ReaderToVectorTransition(reader);
        }
    }

    this.connectionPool.Putback(sqlConnection);
}

threw the following exception:

MySqlException: There is already an open DataReader associated with this Connection which must be closed first.

Here is the relevant part of the stack trace:

at MySql.Data.MySqlClient.ExceptionInterceptor.Throw(Exception exception) at MySql.Data.MySqlClient.MySqlConnection.Throw(Exception ex) at MySql.Data.MySqlClient.MySqlCommand.CheckState() at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior) at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader() at implementation.VectorTransitionsMySqlTable.d__27.MoveNext() in C:\Users\bartoszp...\implementation\VectorTransitionsMySqlTable.cs:line 201

at System.Linq.Enumerable.d__3a1.MoveNext() at System.Linq.Buffer1..ctor(IEnumerable1 source) at System.Linq.Enumerable.ToArray[TSource](IEnumerable1 source) at implementation.VectorTransitionService.Add(VectorTransition vectorTransition) in C:\Users\bartoszp...\implementation\VectorTransitionService.cs:line 38

at Program.Go[T](Environment`2 p, Space parentSpace, EpsilonEstimator epsilonEstimator, ThresholdEstimator thresholdEstimator, TransitionTransformer transitionTransformer, AmbiguityCalculator ac, VectorTransitionsTableFactory vttf, AxesTableFactory atf, NeighbourhoodsTableFactory ntf, AmbiguitySamplesTableFactory astf, AmbiguitySampleMatchesTableFactory asmtf, MySqlConnectionPool connectionPool, Boolean rejectDuplicates, Boolean addNew) in C:\Users\bartoszp...\Program.cs:line 323

The connectionPool.Take returns the first connection that satisfies the following predicate:

private bool IsAvailable(MySqlConnection connection)
{
    var result = false;

    try
    {
        if (connection != null
            && connection.State == System.Data.ConnectionState.Open)
        {
            result = connection.Ping();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("Ping exception: " + e.Message);
    }

    return result && connection.State == System.Data.ConnectionState.Open;
}

(This is related to my previous question, when I resolved a different, but similar issue: MySQL fatal error during information_schema query (software caused connection abort))

The FindWithSourceVector method is called by the following piece of code:

var existing
    = this.vectorTransitionsTable
        .FindWithSourceVector(vectorTransition.SourceVector)
        .Take(2)
        .ToArray();

(I need to find at most two duplicate vectors) - this is the VectorTransitionService.cs:line 38 part of the stack trace.

Now the most interesting part: when the debugger stopped execution after the exception occured, I've investigated the sqlConnection object to find, that it doesn't have a reader associated with it (picture below)!

enter image description here

Why is this happening (apparently at "random" - this method was being called almost every minute for the last ~20h)? Can I avoid that (in ways other then guess-adding some sleeps when Ping throws an exception and praying it'll help)?


Additional information regarding the implementation of the connection pool:

Get is intended for methods that call only simple queries and are not using readers, so the returned connection can be used in a re-entrant way. It is not used directly in this example (because of the reader involved):

public MySqlConnection Get()
{
    var result = this.connections.FirstOrDefault(IsAvailable);

    if (result == null)
    {
        Reconnect();

        result = this.connections.FirstOrDefault(IsAvailable);
    }

    return result;
}

The Reconnect method just iterates though the whole array and recreates and opens the connections.

Take uses Get but also removes the returned connection from the list of available connections so in case of some methods that during their usage of a reader call other methods that also need a connection, it will not be shared. This is also not the case here, as the FindSourceVector method is simple (doesn't call other methods that use the DB). However, the Take is used for the sake of convention - if there is a reader, use Take:

public MySqlConnection Take()
{
    var result = this.Get();

    var index = Array.IndexOf(this.connections, result);

    this.connections[index] = null;

    return result;
}

Putback just puts a connection to the first empty spot, or just forgets about it if the connection pool is full:

public void Putback(MySqlConnection mySqlConnection)
{
    int index = Array.IndexOf(this.connections, null);

    if (index >= 0)
    {
        this.connections[index] = mySqlConnection;
    }
    else if (mySqlConnection != null)
    {
        mySqlConnection.Close();
        mySqlConnection.Dispose();
    }
}
Community
  • 1
  • 1
BartoszKP
  • 34,786
  • 15
  • 102
  • 130
  • You haven't shown what you're doing with the return value of the method. Given the way that iterator blocks are executed, there are definitely ways you can end up not closing the reader. – Jon Skeet Aug 04 '14 at 22:11
  • Is this multiple threads? Perhaps a timer and the process took longer than the interval time? Seems like some sort of race condition. – TyCobb Aug 04 '14 at 22:17
  • @TyCobb No, as I've stated it's only one thread and no other process is using this database at the moment. Thanks for looking into this, it's very important to me :) – BartoszKP Aug 04 '14 at 22:19
  • It looks like you wrote your own connection pool class. The .NET sql provider for MySQL [supports automatic connection pooling by default](http://dev.mysql.com/doc/connector-net/en/connector-net-programming-connection-pooling.html), you just need to use `using` statements around your `MySqlConnection` objects and ADO.NET will re-use connections automatically if it detects the same connection string was used before the timeout closed the underlying connection. – Scott Chamberlain Aug 04 '14 at 22:22
  • @ScottChamberlain I've tried this before, and unfortunately had also lots of issues with it. It was few weeks ago so of course I can't guarantee that it wasn't something wrong in my code, but since then my own connection pool worked well (and this is why in the picture you can see the `pooling=False` part) - until today... But if other approaches won't help I will also try going back to the built-in connection pooling, thanks. – BartoszKP Aug 04 '14 at 22:30
  • 4
    @BartoszKP: Even if my answer works, you should *absolutely* go back to the normal connection pooling. Put it this way: millions of programs use the built-in connection pool every day. It's far more likely that it was a bug in your code than a bug in the connection pool code, so you adding *more* code is just likely to add more bugs... and it looks like that's the case here. – Jon Skeet Aug 04 '14 at 22:32
  • @TyCobb Thanks, your intuition was also right. In fact, there was a race condition because of the only method I didn't add, and forgot that it was using threads (`Reconnect`) ... – BartoszKP Aug 12 '14 at 11:59

2 Answers2

3

I suspect this is the problem, at the end of the method:

this.connectionPool.Putback(sqlConnection);

You're only taking two elements from the iterator - so you never complete the while loop unless there's actually only one value returned from the reader. Now you're using LINQ, which will automatically be calling Dispose() on the iterator, so your using statement will still be disposing of the reader - but you're not putting the connection back in the pool. If you do that in a finally block, I think you'll be okay:

var sqlConnection = this.connectionPool.Take();
try
{
    // Other stuff here...

    using (var reader = this.selectWithSourceVectorCommand.ExecuteReader())
    {
        while (reader.Read())
        {
            yield return ReaderToVectorTransition(reader);
        }
    }
}
finally
{
    this.connectionPool.Putback(sqlConnection);
}

Or ideally, if your connection pool is your own implementation, make Take return something which implements IDisposable and returns the connection back to the pool when it's done.

Here's a short but complete program to demonstrate what's going on, without any actual databases involved:

using System;
using System.Collections.Generic;
using System.Linq;

class DummyReader : IDisposable
{
    private readonly int limit;
    private int count = -1;
    public int Count { get { return count; } }

    public DummyReader(int limit)
    {
        this.limit = limit;
    }

    public bool Read()
    {
        count++;
        return count < limit;
    }

    public void Dispose()
    {
        Console.WriteLine("DummyReader.Dispose()");
    }
}

class Test
{    
    static IEnumerable<int> FindValues(int valuesInReader)
    {
        Console.WriteLine("Take from the pool");

        using (var reader = new DummyReader(valuesInReader))
        {
            while (reader.Read())
            {
                yield return reader.Count;
            }
        }
        Console.WriteLine("Put back in the pool");
    }

    static void Main()
    {
        var data = FindValues(2).Take(2).ToArray();
        Console.WriteLine(string.Join(",", data));
    }
}

As written - modelling the situation with the reader only finding two values - the output is:

Take from the pool
DummyReader.Dispose()
0,1

Note that the reader is disposed, but we never get as far as returning anything from the pool. If you change Main to model the situation where the reader only has one value, like this:

var data = FindValues(1).Take(2).ToArray();

Then we get all the way through the while loop, so the output changes:

Take from the pool
DummyReader.Dispose()
Put back in the pool
0

I suggest you copy my program and experiment with it. Make sure you understand everything about what's going on... then you can apply it to your own code. You might want to read my article on iterator block implementation details too.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • @BartoszKP: Yes, the `using` statement has a `finally` statement - so it's calling `reader.Dispose()`, but that's different. And yes, the code following the last `yield` *won't* be executed in your case, if there are more than 2 results - the `Take` method will dispose of the iterator without asking for the third result, so there's no reason for the code after the `while` loop to execute. The reader will only be closed because it's effectively in a `finally` block. – Jon Skeet Aug 04 '14 at 22:30
  • @BartoszKP: Yes, the reader is being disposed - but I don't believe that's the problem. I believe the bug is actually that you're not returning the connection to your pool. I suspect that's then showing a bug in your connection pool - and entirely possibly, a bug in the diagnostic message coming from the `MySqlException`. It's hard to tell without seeing more of your connection pool code, but I suggest you add diagnostics there about what you're returning etc. – Jon Skeet Aug 04 '14 at 22:45
  • @BartoszKP: I suggest that rather than adding that to this question, you take a little time to diagnose what's going on with that, and ask a *new* question with the connection pool code if necessary. Although as I mentioned before, I'd scrap it if I were you :) – Jon Skeet Aug 04 '14 at 22:48
  • @BartoszKP: Do you ever have multiple of those iterator blocks "open" at a time? For example, calling one method within another? If so, that *could* potentially explain it, with the way that your `Reconnect` method works. But really I'd need to step through it line by line to be certain. – Jon Skeet Aug 04 '14 at 23:08
  • Well... I'm a bit embarrassed :) It turns out that while this doesn't provide the answer it's of course not your fault, because of the crucial detail missing in the question, which I forgot about. Your intuitions about the problem being probably in the pool's implementation was totally right, so I accept this answer, and will add mine with a particular explanation. However this whole thread seems unlikely to be helpful to anyone in the future. Should I delete it? What do you think? – BartoszKP Aug 12 '14 at 11:50
  • 1
    @BartoszKP: You can't delete questions with upvoted answers. I don't think it'll do any harm. I'm glad you found the problem though :) – Jon Skeet Aug 12 '14 at 12:07
0

TyCobb and Jon Skeet have correctly guessed, that the problem was the pool implementation and multi-threading. I forgot that actually I did start some tiny Tasks in the Reconnect method. The first connection was created and opened synchronously but all other where opened asynchronously.

The idea was that because I only need one connection at time, there others can reconnect in different threads. However, because I didn't always put the connection back (as explained in Jon's answer) reconnecting was happening quite frequently, and because the system was quite loaded these reconnection threads weren't fast enough, which eventually led to race conditions. The fix is to reconnect in a more simple and straightforward manner:

private void Reconnect()
{
    for (int i = 0; i < connections.Length; ++i)
    {
        if (!IsAvailable(this.connections[i]))
        {
            this.ReconnectAt(i);
        }
    }
}

private void ReconnectAt(int index)
{
    try
    {
        this.connections[index] = new MySqlConnection(this.connectionString);
        this.connections[index].Open();
    }
    catch (MySqlException mse)
    {
        Console.WriteLine("Reconnect error: " + mse.Message);
        this.connections[index] = null;
    }
}
Community
  • 1
  • 1
BartoszKP
  • 34,786
  • 15
  • 102
  • 130