none
EF .Net 4.5. Каким образом можно корректно очистить кэш материализаторов для динамических типов? RRS feed

  • Вопрос

  • Добрый день!

    Возникла следующая проблема:

    Через ADO.Net Entity Framework выполняется динамически формируемый LINQ-запрос. Структура объекта на выходе заранее неизвестна (запрос формирует пользователь на основе метаданных). Тип для результирующего списка формируется в Dynamic Assembly, которая создается с опцией AssemblyBuilderAccess.RunAndCollect.

    Таким образом, получаем запрос вида:

    objectContext.CreateObjectSet<TEntity>().Select(e => new MyDynamicType() {Property1 = e.p1, ...........}).ToList()

    Обнаружилась неприятная ситуация - после исполнения такого запроса с материализацией в динамический тип сборка никогда более не выгружается из памяти, т.е. - типичный Memory Leak. Ситуация усугубляется тем, что это происходит на сервере приложений, на котором пользователи могут исполнять много таких разнообразных запросов, после каждого из которых в памяти остается динамическая сборка.

    Анализ при помощи Memory Profiler показал, что ссылки на динамический тип накапливаются во внутреннем кэше материализаторов EF:

    _getCanonicalInitializerMetadataMemoizer -> _resultCache

    Пока что выходим из ситуации при помощи следующего кода:

    		private void ClearItemsCSpace(ObjectContext objCtx )
    		{
    			var items = objCtx.MetadataWorkspace.GetItemCollection(DataSpace.CSpace);
    			var memoizer = items.GetType().GetField("_getCanonicalInitializerMetadataMemoizer", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(items);
    			var dict = memoizer.GetType().GetField("_resultCache", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(memoizer) as IDictionary;
    			var rwLock = memoizer.GetType().GetField("_lock", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(memoizer) as ReaderWriterLockSlim;
    
    			if (rwLock.TryEnterWriteLock(20))
    			{
    				var genArgs = memoizer.GetType().GetGenericArguments();
    				var clrTypeField = genArgs[0].GetField("ClrType", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
    				try
    				{
    					var toRemove = new ArrayList();
    					foreach (var item in dict.Keys)
    					{
    						var type = (Type)(clrTypeField.GetValue(item));
    						if (type.Assembly.GetName().Name.StartsWith("Katharsis.TempAssembly."))
    							toRemove.Add(item);
    					}
    
    					foreach (var key in toRemove)
    					{
    						dict.Remove(key);
    					}
    				}
    				finally
    				{
    					rwLock.ExitWriteLock();
    				}
    			}
    		}
    

    , т.е. вручную, через Reflection, вычищаем из кэша наши динамические типы. После исполнения этого кода сборка корректно выгружается через GC.Collect().

    Такой подход, хоть и решает проблему утечки памяти, представляется весьма сомнительным, ибо в очередной версии EF может прекратить работать.

    Просим подсказать корректный способ очистки кэша материализаторов для динамических типов.

    10 января 2013 г. 7:43

Ответы

Все ответы

  • Вместо того чтобы использовать рефлексию, лучше применить деревья выражений. Но и они в конечном счёте транслируются в SQL. Так что, получается двойной расход. Если это для Вас критично, то пишите слой отображения самостоятельно с использованием ADO.NET, без EF. У Вас подход неверный заранее. Но если всё же настаиваете на свой подход, то позже постараюсь более детально посмотреть.
    10 января 2013 г. 8:17
    Модератор
  • Спасибо за оперативный ответ!

    Для построения запроса и используются деревья выражений. Здесь Reflection и не используется. В конечном итоге получается запрос LINQ вида:

    objectContext.CreateObjectSet<TEntity>().Select(e => new MyDynamicType() {Property1 = e.p1, Property2 = e.NavigationPropertyX.PropertyY, ...........}).ToList()

    На самом деле, реальный запрос, как правило, еще более сложный, так как в нем возможно соединение некоторого количества ObjectSet через Join, имеются условия Where (так же динамически задаваемые пользователем) и сортировка OrderBy ... ThenBy. Также в запрос автоматически добавляются условия, связанные с ограничениями доступа пользователя (тоже сформулированные в виде выражений LINQ). Проблема заключается в том, что материализация результатов происходит в динамический тип, который для каждого запроса разный. Заранее определить тип на выходе запроса невозможно, поскольку сам запрос (вместе с условиями, объединениями, сортировками и т.п.) формируется пользователем в интерфейсе, построенном на основе метаданных доменной модели.

    Entity Framework накапливает делегаты материализации в кэше, который (в отличие от кэша планов запросов) никогда не очищается. В результате получается утечка памяти. Её и хотелось бы избежать. Вот здесь и приходится использовать Reflection, поскольку другого способа устранить утечку памяти нет.

    Предлагаемый Вами вариант прямого использования ADO.NET представляется бесперспективным, поскольку в этом случае теряются преимущества объектного подхода, предоставляемые EF, такие как навигационные свойства, наследование и т.п. Цель же данной подсистемы - предоставить пользователю инструмент для выборки объектных данных, работающий в рамках понятной ему доменной модели.


    10 января 2013 г. 9:45
  • "Предлагаемый Вами вариант прямого использования ADO.NET представляется бесперспективным, поскольку в этом случае теряются преимущества объектного подхода, предоставляемые EF, такие как навигационные свойства, наследование и т.п." - Вы меня наверное немного не так поняли. Я не говорю использовать ADO.NET в "голом виде". Оставьте объектную модель и сущностные классы, и там где критично генерируйте SQL запросы самостоятельно, т.е. функцию EF Вы частично берёте на себя. Запросы идут в обход EF. Если приложение правильно организовано, то какая разница данные подаёт EF или собственный слой отображения, написанный вами. А контролировать свой код намного легче, чем пытаться "сдвинуть гору к Магомету".
    10 января 2013 г. 10:05
    Модератор
  • Генерация SQL - кода для запросов к объектной модели - это задача ORM. В нашей объектной модели используются различные иерархии наследования (TPH, TPT и их комбинации). У объектов есть навигационные свойства (как единичные объекты, так и коллекции). Имеются Model-Defined functions, а также процессоры бизнес-логики, ориентированные на обработку деревьев выражений LINQ. Чтобы все это было возможно использовать в запросах, мы и используем Entity Framework. Вы предлагаете самостоятельно разработать ОРМ уровня EF ради устранения утечки памяти, или я не совсем правильно Вас понимаю?

    PS: возможно, острота вопроса будет снята с выходом EF 6.0 с открытым исходным кодом.

    10 января 2013 г. 10:45
  • Привет

    Лучше всего вам будет создать баг-репорт на codeplex в проекте EntityFramework, чтобы его пофиксили в следующей версии.

    Также код самого EF уже доступен на том же codeplex - Entity Framework

    А свое решение вы уже можете проверить на альфа-версиях для 6-ой версии EF - EntityFramework 6.0.0-alpha2



    Для связи [mail]

    • Помечено в качестве ответа Abolmasov Dmitry 16 января 2013 г. 6:57
    11 января 2013 г. 13:10