У меня есть таблица dbo.Cache на сервере SQL с двумя столбцами:
Я планирую хранить большие строки в столбце Value (> 30 МБ) и запрашивать их в нескольких потоках.
Итак, проблема в том, что когда я выполняю более 9 запросов параллельно, он начинает генерировать исключение System.InvalidOperationException : Invalid attempt to call CheckDataIsReady when reader is closed
:
[Fact]
public void TestMemory()
{
const string key = "myKey1";
//re-creating record with Value = large 30mb string
using (var db = new MyDbContext())
{
var existingRecord = db.CachedValues.FirstOrDefault(e => e.Key == key);
if (existingRecord!=null)
{
db.Remove(existingRecord);
db.SaveChanges();
}
var myHugeString = new string('*',30*1024*1024);
db.CachedValues.Add(new CachedValue() {Key = key, Value = myHugeString});
db.SaveChanges();
}
//Try to load this record in parallel threads, creating new dbContext
const int threads = 10;
Parallel.For(1, threads, new ParallelOptions(){MaxDegreeOfParallelism = threads}, (i) =>
{
using (var db = new MyDbContext())
{
var entity = db.CachedValues.FirstOrDefault(c => c.Key == key);
}
});
}
Пытался запускать GC.Collect(); GC.WaitForFullGCComplete();
до / после каждого чтения db - не помогло
Пытался имитировать это поведение на нижнем уровне, напрямую считывая данные через sqlDataReader.ExecuteReader(CommandBehavior.SequentialAccess)
- выкидывает OutOfMemoryException
Да, я сделал - все та же проблема.
Итак, после расследования я понял, что это просто проблема OutOfMemory, потому что после 8-го параллельного запроса, выделяющего 30 МБ * 2 (так как это unicode char) объем памяти, выделенный .Net, фактически превышает 1,2 ГБ в моем приложении, что достаточно для моей рабочей станции. net runtime, чтобы начать подавляться из-за нехватки памяти (https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals#ephemeral-generations-and-segments).
Итак, сказав, что я ничего не могу придумать, просто повторив операцию чтения (mem allocate), пока она не добьется успеха с принудительным сбором GC в блоке catch.
Retry.Action(() =>
{
using (var db = new MyDbContext())
{
var entity = db.CachedValues.FirstOrDefault(c => c.Key == key);
}
}).OnException((OutOfMemoryException exception) =>
{
GC.Collect();
GC.WaitForFullGCComplete();
return true;
})
.OnException((InvalidOperationException exception) =>
{
if (exception.Message != "Invalid attempt to call CheckDataIsReady when reader is closed.")
{
return false;
}
GC.Collect();
GC.WaitForFullGCComplete();
return true;
})
.Run();
Пожалуйста, дайте мне знать, если вы знаете лучшее решение для этого
Ммм ... черт ... вообще-то не лучший вариант. Может быть, открыть вопрос на github NHibernate и спросить совета. Мне кажется, что OutOfMemoryException следует выбросить из фреймворка NHibernate вместо InvalidOperationException. Но в целом с OOM очень сложно справиться.
Не могу с этим согласиться, выбрасывая какое-то первое последующее исключение вместо корневого OutOfMemory - это определенно ошибка / недоработка. Не стесняйтесь создавать билет для репозитория Entity Framework Core
Вы пробовали использовать MultipleActiveResultSets = True в строке подключения?