Caching and rendering modules

November 14, 2023

Core Module - Caches

Caches in Jahia 6.6 are coded as a rendering filter. The default filter is the AggregateCacheFilter. This filter caches each module separately and then aggregates all modules on rendering time to deliver the full page to the client.

As an example, imagine a "last news module" that displays the latest five news listings on a site. The aggregate on rendering will ask for the module last news for the rendering of each discrete news, which will be cached separately and the last news module will only contain its own html and references to the displayed news. When subsequent rendering is requested, the last news modules searches for the expected news in the cache. If they are found, it aggregates the content in the output; otherwise it asks only for rendering on the missing part.

What cache framework is Jahia 6.6 using ?

We are using EHCache. You can configure it in the file WEB-INF/classes/ehcache-jahia.xml or WEB-INF/classes/ehcache-jahia_cluster.xml.

Key Generation for the fragment

The key generator implements CacheKeyGenerator interface, the default implementation use "workspace", "language", "node path", "template", "templateType", "acls", "queryString" for building the key. Since DX 7, the cache key is generated by multiple implementations of CacheKeyPartGenerator. The core defines the default key parts, which are usually enough to create a unique key for each fragment. However, if you need to customize the key by adding specific values, a spring bean implementing CacheKeyPartGenerator can be added to any module, and will impact all keys generated for the cache.

Invalidation or expiration ?

Jahia 6.6 is using both modes for its caches, by default a fragment will have its expiration set to 4 hours. If during this time the element is updated or deleted or child node is added/removed the element will be invalidated from the cache on the fly.

Overriding the default expiration ?

You can override the default expiration in two ways. The easiest and more end-user friendly method, is to allow the users to specify the expiration time directly from the end user interface. User need specific permissions to access to this parameter. To enable manual set-up of expiracy delay in the engines, you must apply the mixin type jmix:cache to the targeted object definition.

[jnt:lastNews] > jnt:content, jmix:list, mix:title, jmix:queryContent, jmix:cache
  - maxNews (long) = 10 indexed=no
  - filter (reference, category[autoSelectParent=false])

You can also have an hard-coded expiration on a per template basis in a template properties file. Example from the jnt_banner/html/banner.properties file in default module.

#Make banner non cacheable
cache.expiration = 0

Expiration delays are expressed in seconds. Note that if you have an alternative view on your content, you have to specify a properties file. For instance, the jnt_user/html/user.welcome.propertie file in default module override the cache for the "welcome" view of the user module.

Automatic/Manual management of dependency for an element

Dependencies of an html element define for which nodes updates this element should be flushed. The system tries to handle most of the dependencies by itself. Automatically the system detects implicit dependencies like parent / childs. If an existing child is updated, only his html fragment will be flushed from the cache. If we create or delete a new child, the system will also flush the parent html fragment. So the system handles automatically all standard parent / child relations.

Now if you have some bound components in your page, the system handles it automatically by making those elements dependent of the bound component for the key computing.

The system will also parse your html to find all the links you have in your module html to other nodes (useful for rich text where your editors will have entered links to pages or contents you could not know in advance) and define the corresponding dependencies. This parsing is executed by the CacheUrlDependenciesParserFilter.

So if in your templates you have defined a template for a news object that add a rateable module bound to this news, then the cache will reflect that by caching the rendering of you rateable module per main resource and adding a dependency to the news. This way we avoid displaying the same rateable module for all news, but we have one per news.

You can have some of those dependencies hard-coded using the template properties file.

You can also define directly in your script file (jsps, etc.) the dependencies you want to add to your fragment. As an example, we can look at the comments component that you can bind to any object in Jahia. This comments component works in two parts.

The first part is the display of the form to add a comment; the second part is the display of the comments list. This form on first submission will create a subfolder under the main resource called comments. So on creation of the first comment, the display list will be correctly flushed as the system as automatically created a dependency between the fragment and the main resource. By adding a child under the main resource (comments node), we will flush all html fragments having a dependency to the main resource.

But for subsequent submission of new comments, we do not update anymore the main resource but only the subnode comments, so in our script we have to tell the system to flush this html fragment when the main resource subnode comments is updated

<jcr:node var="comments" path="${boundComponent.path}/comments"/>
<c:if test="${not empty comments}">
    <template:addCacheDependency node="${comments}"/>
    <template:module node="${comments}" />
</c:if>

Here what you have to keep in mind is that if your script load another node than the current node or the bound one then you will have to add a dependency manually.

You can also define dependencies based on some regular expression, this is really useful for html fragment using search queries to display content.

<query:definition var="result"
  statement="select * from [jnt:blogPost] as blogPost  where isdescendantnode(blogPost, ['${renderContext.mainResource.node.path}']) order by blogPost.[jcr:lastModified] desc" limit="20"/>
<c:set target="${moduleMap}" property="editable" value="false" />
<c:set target="${moduleMap}" property="listQuery" value="${result}" />
<template:addCacheDependency flushOnPathMatchingRegexp="${renderContext.mainResource.node.path}/[^/]*/[^/]*"/>

This fragment will be flushed for any change on any nodes down to two sub level of the main resource.

Cache Key Generation

First request :

initialize users acls :

  • query all user acls (jnt:ace nodes with property principal containing something like 'u:*')
  • for each found acls get the associated content path
  • get the roles for this user on this content node
  • build an acl key merging both the username and the role list
  • append the path/aclkey into the map associated with this user

We obtain something like (username,[[/sites/mysite/home/blogs,username_r_owner[viewer|visitor],[path,aclkey]]) for a user having created its blog named /sites/mysite/home/blogs/usernameblog

Initialize groups acls :

  • query all groups acls (jnt:ace nodes whith property principal containing something like 'g:*')
  • for each found acls get the associated content path
  • append the group to the set of groups found for this path

We obtain something like (/,[[users][groups)]) for the root of the repository

When checking the acl key for a user we first search in the map of paths associated for this user. We loop on the path until the root level to see if we find something matching the current resource if yes then return it.

Otherwise we do the same on the groups map, we search first for the set of groups for the associated path moving up the path until root is found if needed.

When the groups set is found, we check the membership of those groups for this user; if a member, we concatenate the groupname with previously found ones.

Then we get the roles list for this user on the related path and we concatenate that with the groups list.

We store this acl key in the user cache for this path so we do not need to do that a second time.

Caching in modes other than live

The AggregateCacheFilter is configured to only work for the live workspace in live mode. From 6.6.1.5 onwards Jahia provides a second filter, which can be used also to cache fragments in the default workspace to be used in other modes (e.g. contribute, edit or wise mode). This is the ExpiringCacheFilter, which only caches fragment, which have overriden the default expiration value. The fragments are thus only flushed on expiration and not by resolving dependencies like it is done in the AggregateCacheFilter.

This cache is not activated out-of-the-box (except for wise mode if the Jahia Wise templates are installed). In order to activate it for contribute mode, you need to define the filter in a Spring configuration file, like this:

<bean id="contributeModeCacheFilter" class="org.jahia.services.render.filter.cache.ExpiringCacheFilter">
    <property name="disabled" value="${disableOutputCache:false}"/>
    <property name="priority" value="16" />
    <property name="description" value="Module content caching filter for contribute mode (caches only content with expiration set)." />
    <property name="cacheProvider" ref="ModuleCacheProvider"/>
    <property name="skipOnTemplateTypes" value="json"/>
    <property name="skipOnConfigurations" value="include,wrapper,option"/>
    <property name="applyOnModes" value="contribute"/>
    <property name="generatorQueue" ref="moduleGeneratorQueue"/>
    <property name="moduleParamsProperties">
        <map key-type="java.lang.String" value-type="java.lang.String">
            <entry key="j:subNodesView" value="subNodesView"/>
        </map>
    </property>
</bean>           

HTML Caches

Caches in Jahia CMS are coded as a rendering filter. The default filter is the AggregateCacheFilter. This filter caches each module separately and then aggregates all modules on rendering time to deliver the full page to the client.

As an example, imagine a "last news module" that display the latest five news updates on a site. The aggregate rendering will ask for the module last news for the rendering of each discrete news, those news items will be cached separately and the last news module will only contain its own html and references to the displayed news. When subsequent rendering is requested, the last news modules searches for the expected news in the cache. If they are found, it aggregates the content in the output; otherwise, it asks to render only the missing part.

What cache framework is Jahia CMS using ?

We are using EHCache. You can configure it in the file WEB-INF/classes/ehcache-jahia.xml or WEB-INF/classes/ehcache-jahia_cluster.xml.

Key Generation for the fragment

The key generator must implement CacheKeyGenerator interface, the default implementation use "workspace", "language", "node path", "template", "templateType", "acls", "queryString" for building the key.

Invalidation or expiration ?

Jahia CMS is using both modes for its caches, by default a fragment will have its expiration set to four (4) hours. If during this time the element is updated or deleted or child node is added / removed, the element will be invalidated from the cache on the fly.

Overriding the default expiration ?

You can override the default expiration in two ways. The easiest and more end-user friendly method is to allow the users to specify the expiration time directly from the end user interface. Users need specific permissions to access this parameter. To enable manual set-up of expiracy delay in the engines, you must apply the mixin type jmix:cache to the targeted object definition.

[jnt:lastNews] > jnt:content, jmix:list, mix:title, jmix:queryContent, jmix:cache
  - maxNews (long) = 10 indexed=no
  - filter (reference, category[autoSelectParent=false])

You can also have an hard-coded expiration on a per template basis in a template properties file. Example from the jnt_banner/html/banner.properties file in default module.

#Make banner non cacheable
cache.expiration = 0

Expiration delays are expressed in seconds. Note that if you have an alternative view on your content, you have to specify a properties file for each view. For instance the jnt_user/html/user.welcome.propertie file in default module override the cache for the "welcome" view of the user module.

Automatic/Manual management of dependency for an element

Dependencies of an html element define for which nodes updates this element should be flushed. The system tries to handle most of the dependencies by itself. Automatically the system detects implicit dependencies like parent / childs. If an existing child is updated, only his html fragment will be flushed from the cache. If we create or delete a new child, the system will also flush the parent html fragment. So the system handles automatically all standard parent / child relations.

Now if you have some bound components in your page, the system handles it automatically by making those elements dependent of the bound component for the key computing.

The system will also parse your html to find all the links you have in your module html to other nodes (useful for rich text where your editors will have entered links to pages or contents you could not know in advance) and define the corresponding dependencies. This parsing is executed by the CacheUrlDependenciesParserFilter.

So if in your templates you have defined a template for a news object that add a rateable module bound to this news, then the cache will reflect that by caching the rendering of you rateable module per main resource and adding a dependency to the news. This way we avoid to display the same rateable module for all news, but we have one per news.

You can have some of those dependencies hard-coded using the template properties file.

You can also define directly in your script file (jsps, etc.) the dependencies you want to add to your fragment. As an example, we can look at the comments component that you can bind to any object in Jahia. This comments components works in two parts.

The first part is the display of the form to add a comment; the second part is the display of the comments list. This form on first submission will create a subfolder under the main resource called comments. So on the creation of the first comment, the display list will be correctly flushed as the system as automatically created a dependency between the fragment and the main resource. By adding a child under the main resource (comments node), we will flush all html fragments having a dependency to the main resource.

But for subsequent submission of new comments, we do not update anymore the main resource but only the subnode comments, so in our script we have to tell the system to flush this html fragment when the main resource subnode comments is updated

<jcr:node var="comments" path="${boundComponent.path}/comments"/>
<c:if test="${not empty comments}">
    <template:addCacheDependency node="${comments}"/>
    <template:module node="${comments}" />
</c:if>

Here what you have to keep in mind is that if your script load another node than the current node or the bound one then you will have to add a dependency manually.

You can also define dependencies based on some regular expression, this is really useful for html fragment using search queries to display content.

<query:definition var="result"
  statement="select * from [jnt:blogPost] as blogPost  where isdescendantnode(blogPost, ['${renderContext.mainResource.node.path}']) order by blogPost.[jcr:lastModified] desc" limit="20"/>
<c:set target="${moduleMap}" property="editable" value="false" />
<c:set target="${moduleMap}" property="listQuery" value="${result}" />
<template:addCacheDependency flushOnPathMatchingRegexp="${renderContext.mainResource.node.path}/[^/]*/[^/]*"/>

This fragment will be flushed for any change on any nodes down to two sub level of the main resource.

Cache Key Generation

First request :

initialize users acls :

  • query all user acls (jnt:ace nodes with property principal containing something like 'u:*')
  • for each found acls get the associated content path
  • get the roles for this user on this content node
  • build an acl key merging both the username and the role list
  • append the path/aclkey into the map associated with this user

We obtain something like (username,[[/sites/mysite/home/blogs,username_r_owner[viewer|visitor],[path,aclkey]]) for a user having created its blog named /sites/mysite/home/blogs/usernameblog

Initialize groups acls :

  • query all groups acls (jnt:ace nodes whith property principal containing something like 'g:*')
  • for each found acls get the associated content path
  • append the group to the set of groups found for this path

We obtain something like (/,[[users][groups)]) for the root of the repository

When checking the acl key for a user we first search in the map of paths associated for this user. We loop on the path until the root level to see if we find something matching the current resource if yes then return it.

Otherwise we do the same on the groups map, we search first for the set of groups for the associated path moving up the path until root is found if needed.

When groups set is found we check the membership of those groups for this user, if member we concatenate the groupname with previously found ones.

Then we get the roles list for this user on the related path and we concatenate that with the groups list.

We store this acl key in the user cache for this path so we do not need to do that a second time.

Caching in modes other than live

The AggregateCacheFilter is configured to only work for the live workspace in live mode. From 6.6.1.5 onwards Jahia provides a second filter, which can be used also to cache fragments in the default workspace to be used in other modes (e.g. contribute, edit or wise mode). This is the ExpiringCacheFilter, which only caches fragment, which have overriden the default expiration value. The fragments are thus only flushed on expiration and not by resolving dependencies like it is done in the AggregateCacheFilter.

This cache is not activated out-of-the-box (except for wise mode if the Jahia Wise templates are installed). In order to activate it for contribute mode, you need to define the filter in a Spring configuration file, like this:

<bean id="contributeModeCacheFilter" class="org.jahia.services.render.filter.cache.ExpiringCacheFilter">
    <property name="disabled" value="${disableOutputCache:false}"/>
    <property name="priority" value="16" />
    <property name="description" value="Module content caching filter for contribute mode (caches only content with expiration set)." />
    <property name="cacheProvider" ref="ModuleCacheProvider"/>
    <property name="skipOnTemplateTypes" value="json"/>
    <property name="skipOnConfigurations" value="include,wrapper,option"/>
    <property name="applyOnModes" value="contribute"/>
    <property name="generatorQueue" ref="moduleGeneratorQueue"/>
    <property name="moduleParamsProperties">
        <map key-type="java.lang.String" value-type="java.lang.String">
            <entry key="j:subNodesView" value="subNodesView"/>
        </map>
    </property>
</bean>