2

Brief description: after parent and one of its child are updated, transaction commited, session closed, next session gets parent from L2 cache, but updated child is loaded from db. Parent is versioned, children are not.

Detailed description: as I mentioned I have parent with child collection. Parent is versioned, but children are not as my application and mappings guarantees all updates are done by saving parent. What I see that when I load parent with children, parent and most of children are loaded from L2 cache, but recently updated child is loaded from database. I did some research in ReadWriteCache details and found that after I saved parent and updated child the following code decides NOT to update cache:

public bool Put(CacheKey key, object value, long txTimestamp, object version, IComparer versionComparator, bool minimalPut)
{
    bool CS$1$0000;
    if (txTimestamp == -9223372036854775808L)
    {
        return false;
    }
    lock (this._lockObject)
    {
        if (log.IsDebugEnabled)
        {
            log.Debug("Caching: " + key);
        }
        try
        {
            this.cache.Lock(key);
            ILockable lockable = (ILockable) this.cache.Get(key);
            if ((lockable == null) || lockable.IsPuttable(txTimestamp, version, versionComparator))
            {
                this.cache.Put(key, new CachedItem(value, this.cache.NextTimestamp(), version));
                if (log.IsDebugEnabled)
                {
                    log.Debug("Cached: " + key);
                }
                return true;
            }
            if (log.IsDebugEnabled)
            {
                if (lockable.IsLock)
                {
                    log.Debug("Item was locked: " + key);
                }
                else
                {
                    log.Debug("Item was already cached: " + key);
                }
            }
            CS$1$0000 = false;
        }
        finally
        {
            this.cache.Unlock(key);
        }
    }
    return CS$1$0000;
}

In my case lockable is not null, but it is not puttable:

public bool IsPuttable(long txTimestamp, object newVersion, IComparer comparator)
{
    // we really could refresh the item if it  
    // is not a lock, but it might be slower
    //return freshTimestamp < txTimestamp
    return version != null && comparator.Compare(version, newVersion) < 0;
}

As you can see from NHibernate source, earlier decision IsPuttable was made by comparing timestampts, but now version comes into play.

Now the question: should I version EVERY entity in my domain model? Before the problem arises I was sure I should version Aggregate Roots only.

I really expect to find another way of solving this problem because my parent-child hierarchy is much deeper (don't ask why :)), let's say it 3 levels deep (actually it's deeper): Root->Child->GrandChild

In scenario when only GrandChild was updated, I expected 2 UPDATE statements: one for GrandChild and another for Root to update its version. But if I version Child also it will cost me extra database updates and roundtrips :( ... BTW, should ping Ayende or somebody from NH - ADO.NET batching for NH works only for same entities, it won't put in the same batch update statements for GrandChild, Child and Root.

Dmitry Naumov
  • 727
  • 7
  • 12

2 Answers2

1

You need to set a version attribute on your child entities as well, not just the aggregate roots. I have a similar use case in my current project and that is the way that works. We manage this by implementing the version in the base class of our entities and the base class of our fluent nhibernate mapping classes.

Nick Ryan
  • 2,662
  • 1
  • 17
  • 24
  • I've mentioned downsides of versioning all entities in deep hierarchies. I also do understand that having deep hierarchies is bad :(. What makes me curious is the commented line in NHibernate sources: they do used timestampts to make decisions but for unknown reason moved this logic to comparing versions. This means that after updating non-versioned entity you will _always_ hit database next time (in next session) getting this entity even by id. IMHO, doesn't sound good. – Dmitry Naumov Jan 11 '12 at 12:45
  • Hi Dmitry, any possibility that your child entity is not cachable? http://stackoverflow.com/questions/1502794/set-up-caching-on-entities-and-relationships-in-fluent-nhibernate – Nick Ryan Jan 11 '12 at 15:01
  • Ryan, as I discovered if you want your updated entity to appear in L2 immediatelly (a "write-through" behaviour) than it should be versioned and you may use dynamic-update, or you _may_ _not_ version it, but should turn dynamic-update off. Multitable span also affects L2 behaviour. – Dmitry Naumov Jan 12 '12 at 10:58
  • Hi Dmitry, good point. At the moment, in our solution we have versioning on, but are using the default for dynamic-update which is false - meaning it's off and it updates all columns in the update statement but does not check the existing values for the columns as it is relying on the version for concurrency. I should actually turn on dynamic-update in our project as it would simplify the update statements. I'm still standing over my answer though, you should use the version attribute on your child entities otherwise your updates are going to be heavier if you want concurrency maintained. – Nick Ryan Jan 12 '12 at 11:52
  • Ryan, I think decision should be made based on scenario. More oftenly you should turn both versioning and dynamic-update on. But if, for example, I update parent entity and all its child entities, I prefer that child updates will be batched in single roundtrip to db. And in this case I'll turn versioning and dynamic update off. – Dmitry Naumov Jan 13 '12 at 07:27
  • Fair enough. We are implementing L2 at the moment against appfabric. If I encounter any gotchas I'll give you a shout. – Nick Ryan Jan 13 '12 at 09:34
1

Sorry, guys, for confusing you. It seems now I've got the point. Let's take a look at class ReadWriteCache (which is actually a strategy, not the cache it self). We're interested in three methods: Put (I mentioned it in my question), AfterInsert and AfterUpdate:

  • AfterInsert decides should we store inserted entity in L2 cache. It's like a "write-through" behaviour. If you use identity generator than it won't be called :(
  • AfterUpdate decides should we store updated entity in L2. In non-concurrent environment it depends on

    public bool IsCacheInvalidationRequired
    {
        get { return HasFormulaProperties || (!IsVersioned && (entityMetamodel.IsDynamicUpdate || TableSpan > 1)); }
    }
    
  • finally let's talk about Put. Put decides should we store loaded from database entity in L2 cache. The following may happen: a) if L2 doesn't have this (by id) entity we always store loaded entity b) if L2 already has this entity then we update it in L2 only if entity is versioned and it's version is newer than existing.

Hope now I really understand when my created or updated entities will appear in L2 cache and why I see extra db select statements. All this rules are applied to ReadWriteCache. May be someone use NonStrictReadWriteCache, but this is not the option for me.

Dmitry Naumov
  • 727
  • 7
  • 12