У меня есть инструмент командной строки для выполнения массового импорта/экспорта записей данных через 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
.
Кроме того, сами приостановленные запросы не выглядят подозрительными, просто простые команды 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()
не завершит запрос? Похоже, этот тип ожидания обычно указывает на ошибку приложения, но я не уверен, как я запускаю такое поведение.