Проблема с MySqlConnector 2.3.x
SQLQueryHolder<R> m_holder;
Dictionary<T, PreparedStatement> m_queries = new();
Dictionary<T, SQLResult> _results = new();
public bool Execute<T>(MySqlBase<T> mySqlBase)
{
if (m_holder == null)
return false;
// execute all queries in the holder and pass the results
foreach (var pair in m_holder.m_queries)
m_holder.SetResult(pair.Key, mySqlBase.Query(pair.Value));
return m_result.TrySetResult(m_holder);
}
public void SetResult(T index, SQLResult result)
{
_results[index] = result;
}
public SQLResult Query(PreparedStatement stmt)
{
try
{
MySqlConnection Connection = _connectionInfo.GetConnection();
Connection.Open();
MySqlCommand cmd = Connection.CreateCommand();
cmd.CommandText = stmt.CommandText;
foreach (var parameter in stmt.Parameters)
cmd.Parameters.AddWithValue("@" + parameter.Key, parameter.Value);
return new SQLResult(cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection));
}
catch (MySqlException ex)
{
HandleMySQLException(ex, stmt.CommandText, stmt.Parameters);
return new SQLResult();
}
}
public class SQLResult
{
MySqlDataReader _reader;
public SQLResult() { }
public SQLResult(MySqlDataReader reader)
{
_reader = reader;
NextRow();
}
~SQLResult()
{
_reader = null;
}
public T Read<T>(int column)
{
if (_reader.IsDBNull(column))
return default;
var columnType = _reader.GetFieldType(column);
if (columnType == typeof(T))
return _reader.GetFieldValue<T>(column);
switch (Type.GetTypeCode(columnType))
{
case TypeCode.SByte:
{
var value = _reader.GetSByte(column);
return Unsafe.As<sbyte, T>(ref value);
}
case TypeCode.Byte:
{
var value = _reader.GetByte(column);
return Unsafe.As<byte, T>(ref value);
}
case TypeCode.Int16:
{
var value = _reader.GetInt16(column);
return Unsafe.As<short, T>(ref value);
}
case TypeCode.UInt16:
{
var value = _reader.GetUInt16(column);
return Unsafe.As<ushort, T>(ref value);
}
case TypeCode.Int32:
{
var value = _reader.GetInt32(column);
return Unsafe.As<int, T>(ref value);
}
case TypeCode.UInt32:
{
var value = _reader.GetUInt32(column);
return Unsafe.As<uint, T>(ref value);
}
case TypeCode.Int64:
{
var value = _reader.GetInt64(column);
return Unsafe.As<long, T>(ref value);
}
case TypeCode.UInt64:
{
var value = _reader.GetUInt64(column);
return Unsafe.As<ulong, T>(ref value);
}
case TypeCode.Single:
{
var value = _reader.GetFloat(column);
return Unsafe.As<float, T>(ref value);
}
case TypeCode.Double:
{
var value = _reader.GetDouble(column);
return Unsafe.As<double, T>(ref value);
}
}
return default;
}
public T[] ReadValues<T>(int startIndex, int numColumns)
{
T[] values = new T[numColumns];
for (var c = 0; c < numColumns; ++c)
values[c] = Read<T>(startIndex + c);
return values;
}
public bool IsNull(int column)
{
return _reader.IsDBNull(column);
}
public int GetFieldCount() { return _reader.FieldCount; }
public bool IsEmpty()
{
if (_reader == null)
return true;
return _reader.IsClosed || !_reader.HasRows;
}
public SQLFields GetFields()
{
object[] values = new object[_reader.FieldCount];
_reader.GetValues(values);
return new SQLFields(values);
}
public bool NextRow()
{
if (_reader == null)
return false;
if (_reader.Read())
return true;
_reader.Close();
return false;
}
}
Случилось это после того как вышла новая версия MySql Connector 2.3.x Один участник проекта сообщил, что там были изменения с кэшированием MySqlDataReader, но я вообще ничего не понял на что он намекал.
Я вообще не понимаю, что происходит закулисами, и что я могу сделать чтобы решить эту проблему. Хоть наведите на что-то, пожалуйста.
Дебаггер показывает, что содержимое моей коллекции ответов меняется после вызова вот этого метода (как оказалось это происходит случайным образом, просто в тот момент произошло сдесь):
/// <summary>
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the <see cref="TaskStatus.RanToCompletion"/> state.
/// </summary>
/// <param name="result">The result value to bind to this <see cref="Task{TResult}"/>.</param>
/// <returns>True if the operation was successful; otherwise, false.</returns>
/// <remarks>
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
/// <see cref="TaskStatus.RanToCompletion"/>,
/// <see cref="TaskStatus.Faulted"/>, or
/// <see cref="TaskStatus.Canceled"/>.
/// </remarks>
public bool TrySetResult(TResult result)
{
bool rval = _task.TrySetResult(result); //after this point i get wrong results
if (!rval)
{
_task.SpinUntilCompleted();
}
return rval;
}
При этом я где-то прочитал (на хабрахабе) что это типа нормальная практика вызывать этот метод. Но нигде точно не могу найти что происходит внутри этого метода (даже на MSND)
UPD: Нашел, ничего он такого не делает, кроме того, что ждет пока асинхронная операция не подтвердит, что она уже вернула результат и им можно пользоваться. Вот скажите, если в моей программе практически не используются никакие асинхронные операции напрямую, только вот через коннектор, который делает что-то внутри для общения с сервером базы - каким образом из-за моего кода может появиться паралельный поток который вмешивается и изменяет уже полученные данные из базы?
Ответы (1 шт):
Забейте на код, там ничего не понятно всеравно. В общем проблема оказалась в том, что ребята сделали оптимизацию аля https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-connection-pooling.
После этого означает, что соединение и ридер не уничтожаются после закрытия, а работают дальше на благо другого запроса к той же базе.
Незнаю кто так придумал (долго копаться нужно) чтобы обвертка ридера работала именно так:
public class SQLResult
{
MySqlDataReader _reader;
public SQLResult() { }
public SQLResult(MySqlDataReader reader)
{
_reader = reader;
NextRow();
}
public bool NextRow()
{
if (_reader == null)
return false;
if (_reader.Read())
return true;
_reader.Close();
return false;
}
}
Получается что NextRow(); выполнялся при создании обвертки ридера без проверки возвращенной переменной. А когда дело доходило до считывания данных - данный ридер уже принадлежал другому запросу и в нем были чужие данные.
Сложность выявления проблемы была в том, что походу пустой ридер автоматически считается библиотекой уже никому не нужным и передается другому запросу вместе с соединением, потому как никто его явно не закрывал.(А пустой он был потому что пустая таблица, тоесть это возникало только с пустыми таблицами, у которых после первого чтения по задумке авторов никто уже ничего считывать не будет, так как стандартный API предполагает чтение данных именно с проверки на первое чтение) Кто бы до такого допер? А при проверке соединения - оно было в порядке, потому что было передано другому запросу.
В общем я добавил проверку и все заработало
public SQLResult(MySqlDataReader reader)
{
_reader = reader;
if (!NextRow())
{
Dispose();
}
}
public bool NextRow()
{
return _reader.Read()
}
public void Dispose()
{
if (_reader != null)
{
_reader.Close();
_reader = null;
}
}