Тайм-аут EF 5 при обновлении; Возможно из-за блокировки

У меня есть инструмент командной строки для выполнения массового импорта/экспорта записей данных через Entity Framework в базу данных приложения. Инструмент хорошо работает для вставки новых записей в базу данных, но я столкнулся с ошибкой тайм-аута при попытке обновить существующие записи, которые, похоже, связаны с блокировкой EF.

Я прочитал некоторые из многие другие сообщения о Entity Framework и взаимоблокировках, но ни один из ответов не кажется применить к этой ситуации. Я попытался обернуть свой код импорта в TransactionScope, а также выполнить команду SET TRANSACTION ISOLATION LEVEL SQL, но ни один из них не решает тайм-аут.

Тайм-аут происходит независимо от того, сколько сущностей обновляется за один вызов SaveChanges. В приведенном ниже примере кода я задал для размера пакета значения от 1 до 500, всегда выдавая одно и то же исключение.

Вот сокращенная версия кода обновления, за которой следуют сведения об исключении и снимок экрана монитора активности SQL Server.

Я использую объекты Entity Framework 5 DbContext, которые инициализируются из моделей EDMX (Model First).

using(var readContext = new MySourceEntities())
using(var readWriteContext = new MyTargetEntities())
{
    var query = "SELECT ...";  // Determine which records to update
    var keys  = readContext.Database.SQLQuery<int>(query);

    // Group the update into batches to improve performance. Batch()
    // extension method from MoreLINQ
    foreach (var batch in keys.Batch(BATCH_SIZE))
    {
        var sourceRecords = readContext
            .AsNoTracking()
            .Where(x => batch.Contains(x.SharedId))
            .ToList();

        var targetRecords = readWriteContext
            .Where(x => batch.Contains(x.SharedId))
            .ToLookup(x => x.SharedId);

        foreach (var record in sourceRecords)
        {
            // Enforce a constraint on having only a single match
            var target = targetRecords[record.SharedId].Single();

            target.Field = record.Field;
        }

        readWriteContext.SaveChanges();  // <--- Timeout happens here
    }
}

Я запускаю это из приложения командной строки, и конкретная трассировка стека, которую оно выдает, выглядит следующим образом:

An error occurred while updating the entries. See the inner exception for details.
   at System.Data.Entity.Internal.InternalContext.SaveChanges()
   at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
   at System.Data.Entity.DbContext.SaveChanges()
   at <snip>

An error occurred while updating the entries. See the inner exception for details.
   at System.Data.Mapping.Update.Internal.UpdateTranslator.Update(IEntityStateManager stateManager, IEntityAdapter adapter)
   at System.Data.EntityClient.EntityAdapter.Update(IEntityStateManager entityCache)
   at System.Data.Objects.ObjectContext.SaveChanges(SaveOptions options)
   at System.Data.Entity.Internal.InternalContext.SaveChanges()

Timeout expired.  The timeout period elapsed prior to completion of the operation or the server is not responding. The statement has been terminated.
   at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
   at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   at System.Data.Mapping.Update.Internal.DynamicUpdateCommand.Execute(UpdateTranslator translator, EntityConnection connection, Dictionary`2 identifierValues, List`1 generatedValues)
   at System.Data.Mapping.Update.Internal.UpdateTranslator.Update(IEntityStateManager stateManager, IEntityAdapter adapter)

Пока метод SaveChanges зависает, монитор активности SQL Server показывает следующие запросы, все в состоянии SUSPENDED. Красные запросы относятся к базе данных, на которую нацелен readContext, а синие запросы — к базе данных, на которую нацелен readWriteContext.

Монитор активности SQL Server Management Studio

Кроме того, сами приостановленные запросы не выглядят подозрительными, просто простые команды SELECT и UPDATE. Я могу запускать их вручную без ошибок.

Изменить

Вот подробности выполняемого предложения query, поскольку оно кажется уместным. Запрос выполняет соединения между базами данных, чтобы сопоставить записи с SharedId. Выполнение запроса sys.dm_os_waiting_tasks с этой страницы дает следующую таблицу.

session_id wait_duration_ms wait_type         blocking_session_id resource_description program_name    text
55         15               ASYNC_NETWORK_IO  NULL                NULL                 EntityFramework <cross-db join query>
54         29310            LCK_M_IX          55                  pagelock fileid=1... EntityFramework <update query>

Содержание запроса такое

SELECT DB1.dbo.Table1.SharedId 
FROM DB2.dbo.Table2 INNER JOIN DB1.dbo.Table1.SharedId
ON DB1.dbo.Table1.SharedId = DB2.dbo.Table2.SharedId
WHERE (
    (DB1.dbo.Table1.Field1 <> DB2.dbo.Table2.Field1) OR
    (DB1.dbo.Table1.Field2 <> DB2.dbo.Table2.Field2)
)

Самое удивительное для меня наблюдение заключается в том, что запрос все еще активен. Любые идеи, почему вызов readContext.Database.SQLQuery() не завершит запрос? Похоже, этот тип ожидания обычно указывает на ошибку приложения, но я не уверен, как я запускаю такое поведение.


person Lucas    schedule 06.01.2014    source источник


Ответы (1)


Решение состояло в том, чтобы материализовать результаты SQLQuery() явно, используя метод расширения ToList(). Это позволило полностью использовать результаты перед попыткой UPDATE базовых таблиц.

Удивительно (для меня), что прямая команда SQL не вернула полный результат немедленно, особенно потому, что запрос возвращал примитивный тип, а не объект Entity. После внимательного прочтения документации MSDN , в нем указано, что IEnumerable<T>, возвращаемый методом SQLQuery, «выполнит запрос, когда он будет пронумерован».

Мне нравится, что в EF6 этот метод возвращает объект DbRawSqlQuery<T>, что, вероятно, поможет таким людям, как я, остановиться и внимательнее прочитать документацию в следующий раз.

person Lucas    schedule 07.01.2014