Caching: non-cacheable regions


#1

Darren Beck bought this up over in the CFML slack box products slack:

Previously in a legacy app there was a bespoke caching mechanism whereby pages where cached, but prior to the cached content being returned to the user, the content was run through a method which injected/ran dynamic content within it, based on information inside comment tags i.e ‘’, which was then replaced with the dynamic content.

If this kind of capability was needed in a coldbox app , the ability to cache a page, but aspects inside it remaining dynamic. What would the way to do it be?

I thought this would be a really neat approach to solving some of the caching challenges we have with preside. Combine something like this with whole page level caching and UI options around how to cache pages and you could have something really neat. Entire pages cached and, where there is uncacheable content your application can output something like this:

<!--viewlet:path.to.my.viewlet( arg1=10, arg2=true )-->

These would be parsed and viewlets invoked after the rest of the page is rendered, with arguments passed as the args struct.

Does anyone have any further input ideas around how we could approach caching and whether they like this idea in core preside?


Have created this issue: https://presidecms.atlassian.net/browse/PRESIDECMS-962 and a feature branch feature-PRESIDECMS-962_caching-improvements where we can have a play (fork+clone the repo and checkout the branch - see https://docs.presidecms.com/contribguides/buildfromsource.html for guide to maintaining local fork of preside).

Update: work has been merged into 10.9.0 bleeding edge


#2

In Mura the caching principle is: Cache everything except the areas I define as not cacheable (like user data). There’s an own custom Tag for that: http://docs.getmura.com/v6/back-end/caching/cache-o-matic/


#3

in terms of API, we could add a ‘delayed’ argument to renderViewlet():

// handler for non-cacheable widget at /handlers/MyWidget.cfc
private string function index( event, rc, prc, args={}, delayed=false ) {
    if ( !arguments.delayed ) {
        return renderViewlet( event="widgets.mywidget.index", args=args, delayed=true );
    }

    // render viewlet as normal
}

Adding the delayed=true argument to renderViewlet() would make the call not instantiate the viewlet at all. Instead it would just output the special <!--delayedexec:widgets.mywidget.index( somearg=true )--> syntax. Later, when the request resolves all those delayed execution placeholders, it will execute the viewlets and add an extra argument delayed=true to be passed to the viewlet.


Alternatively, we could annotate handler actions to force them always to be delayed executed (having the same effect as above):

/**
 * @delayExecution true
 */
private string function index( event, rc, prc, args={} ) {
    // render viewlet as normal
}

#4

This has just been released into 10.9.0 bleeding edge! If you’re using box to install dependencies, you’ll want [email protected] to get this.

To enable, in your config.cfc:

settings.features.fullPageCaching.enabled = true; // false by default right now

By default, this will cache everything except:

  • Conditional content widgets
  • System page types (just the body of the page type will not be cached, the layout around it will be)
  • Permissions checking for pages with access restrictions
  • Navigational menu items that are shown conditionally (see below)

Each page will potentially have two cached entries - one for logged in users and one for anonymous visitors.

Auto non-cacheable viewlets

To mark a viewlet as not being cacheable, add the @cacheable false annotation to the viewlet’s handler:

/**
 * @cacheable false
 */
private string function myViewlet( ... ) {
// ...
}

Navigation menus

If you are overriding the views for the core navigation viewlets, you may want to add the following lines to your views so that menu items that have conditional access rules are not cached:

<cfloop array="#menuItems#" index="i" item="item">
	<cfif IsTrue( item.hasRestrictions ?: "" )>
		#renderViewlet(
			  event   = "core.navigation.restrictedMenuItem"
			, args    = { menuItem=item, view="/core/navigation/mainNavigation" }
			, delayed = IsTrue( args.delayRestricted ?: true )
		)#
		<cfcontinue />
	</cfif>
	<!-- ... -->

Explicit delayed viewlet render

Add delayed=true to renderViewlet() to explicitly render a viewlet that will not be included in the full page cache (it will get rendered after the rest of the page).

#event.renderViewlet( event="my.event", args=viewletArgs, delayed=true )#

Request context helpers

event.cachePage(); // returns true/false for whether the page is going to be cached
event.cachePage( false ); // instruct the system that this page should not be cached
event.setPageCacheTimeout( 24000 ); // set a non-default cache timeout for the cache

Configuring the cache store

We are using cachebox to configure caches. The cache used for full page caching is named PresidePageCache and looks like this right now:

PresidePageCache = {
	  provider   = "preside.system.coldboxModifications.cachebox.CacheProvider"
	, properties = {
		  objectDefaultTimeout           = 1200
		, objectDefaultLastAccessTimeout = 0
		, useLastAccessTimeouts          = false
		, reapFrequency                  = 20
		, freeMemoryPercentageThreshold  = 0
		, evictionPolicy                 = "LFU"
		, evictCount                     = 200
		, maxObjects                     = 2000
		, objectStore                    = "ConcurrentSoftReferenceStore"
	}
}

You can override this configuration in your application by adding /application/config/Cachebox.cfc and tweaking the setting you want to tweak. For example, to change the maxObject and defaultTimeout:

component extends="preside.system.config.Cachebox" {
	function configure(){
		super.configure( argumentCollection=arguments );

		cacheBox.caches.PresidePageCache.properties.maxObjects           = 50000;
		cacheBox.caches.PresidePageCache.properties.objectDefaultTimeout = 60 * 60; // 1hr
	}
}

Considerations

Obviously, if your site has a login functionality and displays personal information in pages to the logged in user - you need to ensure that these parts of the page are not cached. Use either the renderViewlet( ..., delayed=true ) technique, and/or, mark your personal info/non-cacheable viewlets with @cacheable false. The fact that system page types are not cached by default should help with this also.

If you’re using sticker, you won’t be able to use event.include( "myasset" ) inside a delayed viewlet. This is because the rest of the layout will have already been rendered.


#5

As of 10.9.0-snaphot110, this limitation has been removed. Now, the includes in the cached page and those generated by delayed viewlets are combined and rendered at runtime (with all the usual dependency logic respected)…