Strange Exception When Using Caching

Topics: General, Troubleshooting
Feb 17, 2011 at 7:51 PM

I am attempting to use the following code to cache a list of content items: 

<!-- p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 9.5px Consolas} span.s1 {color: #1440fc} span.s2 {color: #3da3bc} span.s3 {color: #b12320} -->

private IEnumerable<ContentItem> GetCachedItemsForTopic(int topicId)
        {
            return _cacheManager.Get("suggested_" + topicId.ToString(), ctx =>
            {
                ctx.Monitor(_clock.When(TimeSpan.FromMinutes(20)));
                return GetRecentItemsForTopic(topicId);
            });
        }

private IEnumerable<ContentItem> GetRecentItemsForTopic(int topicId)
        {
            var settings = Services.WorkContext.CurrentSite.As<SuggestionSettingsPart>();

            return Services.ContentManager
                .Query<TopicsPart>(VersionOptions.Latest, settings.ContentTypes.Split(','))
                .Where<TopicsPartRecord>(t => t.PrimaryTopic == topicId)
                    .Join<CommonPartRecord>()
                    .OrderByDescending(c => c.PublishedUtc)
                .Slice(0, 6)
                .Select(c => c.ContentItem).ToList();

        }

This works great when I strip out the caching code, or on the first request with the caching code in place. But any subsequent request throws the following exception:

[TransactionAbortedException: The transaction has aborted.]
   System.Transactions.TransactionStateAborted.BeginCommit(InternalTransaction tx, Boolean asyncCommit, AsyncCallback asyncCallback, Object asyncState) +11
   System.Transactions.CommittableTransaction.Commit() +184
   System.Transactions.TransactionScope.InternalDispose() +402
   System.Transactions.TransactionScope.Dispose() +1450
   Orchard.Data.TransactionManager.System.IDisposable.Dispose() in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\Data\TransactionManager.cs:43
   Autofac.Core.Disposer.Dispose(Boolean disposing) +79
   Autofac.Util.Disposable.Dispose() +46
   Autofac.Core.Lifetime.LifetimeScope.Dispose(Boolean disposing) +21
   Autofac.Util.Disposable.Dispose() +46
   Orchard.Environment.<>c__DisplayClass2.<.ctor>b__0() in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\Environment\WorkContextAccessor.cs:75
   Orchard.Environment.HttpContextScopeImplementation.System.IDisposable.Dispose() in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\Environment\WorkContextAccessor.cs:80
   Orchard.Mvc.Routes.HttpAsyncHandler.EndProcessRequest(IAsyncResult result) in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\Mvc\Routes\ShellRoute.cs:158
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +8841105
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +184

If I attach a debugger I can see the following output which is the source of the aborted transaction:

Orchard.ContentManagement.DefaultContentManager: ERROR - GenericADOException thrown from IContentHandler by Orchard.Core.Routable.Handlers.RoutePartHandlerBase
NHibernate.Exceptions.GenericADOException: could not load an entity: [Orchard.Core.Routable.Models.RoutePartRecord#580][SQL: SELECT routepartr0_.Id as Id14_0_, routepartr0_.Title as Title14_0_, routepartr0_.Slug as Slug14_0_, routepartr0_.Path as Path14_0_, routepartr0_.ContentItemRecord_id as ContentI5_14_0_ FROM Routable_RoutePartRecord routepartr0_ WHERE routepartr0_.Id=?] ---> System.Transactions.TransactionException: The operation is not valid for the state of the transaction.
   at System.Transactions.TransactionState.EnlistVolatile(InternalTransaction tx, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions, Transaction atomicTransaction)
   at System.Transactions.Transaction.EnlistVolatile(IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions)
   at NHibernate.Transaction.AdoNetWithDistrubtedTransactionFactory.EnlistInDistributedTransactionIfNeeded(ISessionImplementor session)
   at NHibernate.Impl.AbstractSessionImpl.EnlistInAmbientTransactionIfNeeded()
   at NHibernate.Impl.AbstractSessionImpl.CheckAndUpdateSessionStatus()
   at NHibernate.Impl.SessionImpl.get_Batcher()
   at NHibernate.Loader.Loader.GetResultSet(IDbCommand st, Boolean autoDiscoverTypes, Boolean callable, RowSelection selection, ISessionImplementor session)
   at NHibernate.Loader.Loader.DoQuery(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies)
   at NHibernate.Loader.Loader.DoQueryAndInitializeNonLazyCollections(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies)
   at NHibernate.Loader.Loader.LoadEntity(ISessionImplementor session, Object id, IType identifierType, Object optionalObject, String optionalEntityName, Object optionalIdentifier, IEntityPersister persister)
   --- End of inner exception stack trace ---
   at NHibernate.Loader.Loader.LoadEntity(ISessionImplementor session, Object id, IType identifierType, Object optionalObject, String optionalEntityName, Object optionalIdentifier, IEntityPersister persister)
   at NHibernate.Loader.Entity.AbstractEntityLoader.Load(ISessionImplementor session, Object id, Object optionalObject, Object optionalId)
   at NHibernate.Loader.Entity.AbstractEntityLoader.Load(Object id, Object optionalObject, ISessionImplementor session)
   at NHibernate.Persister.Entity.AbstractEntityPersister.Load(Object id, Object optionalObject, LockMode lockMode, ISessionImplementor session)
   at NHibernate.Event.Default.DefaultLoadEventListener.LoadFromDatasource(LoadEvent event, IEntityPersister persister, EntityKey keyToLoad, LoadType options)
   at NHibernate.Event.Default.DefaultLoadEventListener.DoLoad(LoadEvent event, IEntityPersister persister, EntityKey keyToLoad, LoadType options)
   at NHibernate.Event.Default.DefaultLoadEventListener.Load(LoadEvent event, IEntityPersister persister, EntityKey keyToLoad, LoadType options)
   at NHibernate.Event.Default.DefaultLoadEventListener.ProxyOrLoad(LoadEvent event, IEntityPersister persister, EntityKey keyToLoad, LoadType options)
   at NHibernate.Event.Default.DefaultLoadEventListener.OnLoad(LoadEvent event, LoadType loadType)
   at NHibernate.Impl.SessionImpl.FireLoad(LoadEvent event, LoadType loadType)
   at NHibernate.Impl.SessionImpl.Get(String entityName, Object id)
   at NHibernate.Impl.SessionImpl.Get(Type entityClass, Object id)
   at NHibernate.Impl.SessionImpl.Get[T](Object id)
   at Orchard.Data.Repository`1.Get(Int32 id) in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\Data\Repository.cs:line 87
   at Orchard.Data.Repository`1.Orchard.Data.IRepository<T>.Get(Int32 id) in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\Data\Repository.cs:line 56
   at Orchard.ContentManagement.Handlers.StorageVersionFilter`1.GetRecordCore(ContentItemVersionRecord versionRecord) in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\ContentManagement\Handlers\StorageVersionFilter.cs:line 11
   at Orchard.ContentManagement.Handlers.StorageFilter`1.<>c__DisplayClass1.<Loading>b__0(TRecord prior) in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\ContentManagement\Handlers\StorageFilter.cs:line 54
   at Orchard.ContentManagement.Utilities.LazyField`1.GetValue() in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\ContentManagement\Utilities\LazyField.cs:line 24
   at Orchard.ContentManagement.Utilities.LazyField`1.get_Value() in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\ContentManagement\Utilities\LazyField.cs:line 10
   at Orchard.ContentManagement.ContentPart`1.get_Record() in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\ContentManagement\ContentPart.cs:line 58
   at Orchard.Core.Routable.Models.RoutePart.get_Title() in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard.Web\Core\Routable\Models\RoutePart.cs:line 7
   at Orchard.Core.Routable.Handlers.RoutePartHandlerBase.GetContentItemMetadata(GetContentItemMetadataContext context) in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard.Web\Core\Routable\Handlers\RoutePartHandler.cs:line 135
   at Orchard.ContentManagement.DefaultContentManager.<>c__DisplayClass40.<GetItemMetadata>b__3f(IContentHandler handler) in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\ContentManagement\DefaultContentManager.cs:line 355
   at Orchard.InvokeExtensions.Invoke[TEvents](IEnumerable`1 events, Action`1 dispatch, ILogger logger) in C:\Users\chris\Desktop\Orchard.Source.1.0.20\src\Orchard\InvokeExtensions.cs:line 19

Am I doing something wrong? Is this a bug in caching?

Coordinator
Feb 17, 2011 at 8:52 PM

Louis figured it out in about 3 minutes :) Here's what he said:

"A transaction has a strict boundary around a unit of work. That is either a single web request, or a background task.

Any objects which are “database hot” are zombied at the end of the unit of work. That includes any ORM record, and by extension any ContentPart and ContentItem. 

The fix is to cache the values you need, or a POCO model of the values, instead of the ContentItem. You could do that with a c=>new MyPoco{Title=c.Title} in the Select."

Feb 17, 2011 at 8:57 PM

Thanks for the quick response, but ouch... That's going to make caching cumbersome to work with, especially with results with mixed content types. :(

Well, at least I know now. :)

Thanks Bertrand (and Louis)

Coordinator
Feb 17, 2011 at 9:06 PM

Well, not really. Think about it this way: in ASP.NET, you wouldn't cache a DataReader or any object that really is dependent on a connection to the database. What you cache is a copy of the data, one that does not depend on any expensive resources. Same thing here.

Plus, with the magic of the Select method, this comes for cheap.

Feb 17, 2011 at 9:08 PM

It does, but it means I have to know about all the data from all of the possible content parts of all of the possible content types that i'm projecting onto a POCO.

What would be sweet is if there was a way to project a POCO from a content type that was part-aware.

Coordinator
Feb 17, 2011 at 9:10 PM

Maybe you are not caching at the right level. First, do you know that you need to cache that data? Second, can you cache at another scale where there is less uncertainty about the data's shape?

Feb 17, 2011 at 9:14 PM

That's possible. What I'm trying to do is create a Suggested Articles module based on a user's browsing history and actions taken on the site. I want to keep a cache of a few items from each topic globally to be able to show to the user so that I'm not making n database requests per pageview (where n = number of topics the user has ever interacted with).

 

I suppose with the content types that I know about right now, all I really need is a title and a URL. But what if in the future I use this to suggest a video to a user. Then I need to know that I need a thumbnail url cached as well.

Coordinator
Feb 17, 2011 at 9:15 PM

You might want to consider doing output caching, or caching of the view model/shapes. This way you can re-use structures that you'd have to build anyways.

Feb 17, 2011 at 9:17 PM

I'm not sure I can cache at either of those levels because of the per-user nature. :-\

Coordinator
Feb 17, 2011 at 9:19 PM

Why not? It's just a matter of putting a user token in the cache key. And again, if this is per-user, do you *know* that there are substantial benefits in caching?

Feb 17, 2011 at 9:22 PM

When I say user, I mean non-logged-in people as well. Anyone that has ever hit the site. Since this is a public facing site with several 10's of thousands of uniques per day I don't think it would be wise to keep a per-user cache in memory.

The benefit in caching comes before it becomes per-user, where I can just keep a handful of records representing contentitems in memory so the user doesn't have to hit the database to get a personalized suggested article list.

Coordinator
Feb 17, 2011 at 9:27 PM

Ah, I see. Yeah, content item is pretty lazy for obvious reasons, which is what kills this, but I totally see your scenario. What would be great would be a way to clone the items or make them fetch everything then sever their ties to the database. That's tricky to do though because you don't know in advance what potentially costly relations to follow.

For the moment you'll have to come up with a structure that captures what you know about the objects, but I'll take that feedback to the team.

Feb 17, 2011 at 9:31 PM

Thanks. :)

What would be slick is to be able to go c.GetPOCO( ctx=>ctx.WithPart<RoutePart>().WithPart<CommonPart>().WithPart<MyPart>()). 

Developer
Feb 19, 2011 at 3:21 PM

Hi!

Have you thought about using ContentHandler and handling OnPublishing (and possibly other, as OnRemoving too) event on your TopicsPart? This could be the place where you'd build and update a list of items.

You could create a collection of POCOs, which store the relevant information, cache them and update the cache if OnPublished event gets hit. Then, for every user you could query this cached collection instead of the database to get the personalized data. So the database queries won't be necessary at all, as the data would be updated only when content changes!:)

Cached collection could be made available to drivers via a custom service implementing IDependency.

Cheers!

Feb 21, 2011 at 5:26 PM

Bertrand, so you had mentioned output caching/shape caching... Is there a way to cache the result of a shape? That could be very useful.

Coordinator
Feb 21, 2011 at 7:53 PM

Well, output caching I haven't investigated yet how you could do that at the shape level.

To get back to the original problem, with the transaction thing when using cache, there is a workaround, which is to get a reference to an IWorkContextAccessor and then ask it to resolve your dependency, instead of injecting the dependency directly. You can look for examples of usage in the source code.

Feb 21, 2011 at 7:57 PM

Ok, I'll look into that.

I would be very interested in a way to cache at a shape-level. I think we're going to need it. I spent the weekend mulling that particular problem and I can't think of any way our site will work without having shape-level caching for certain shapes a la caching a partial in MVC.

 

pszmyd - Thanks for that advice too, good idea.

Feb 23, 2011 at 8:36 PM

To follow-up on this for anyone who has a similar challenge: What I ended up doing was caching the ContentItemMetadata of each item, since thats all I needed in order to generate a link to each item for my specific case.