AEM Dispatcher Cache

If your AEM instance is shipped in a virtual appliance (with Author and Publish inside, Dispatcher, initial content and configuration) there is a big probability that you’ve never tried to explore configuration files, cached files, log files etc. of Dispatcher.

If you’d really like to have the whole developing process under control, you should think about Dispatcher and see what it does and how. If you don’t already have it installed on your machine (or in a virtual appliance) this is how you can do it on Mac and on Windows. In the case you use Mac, as I do, you might have to modify read only files in Vi editor during Dispatcher installation. If you don’t know how to do it, check this article. Now when you have Publish and Dispatcher up and running, we can finally remove port from URLs that we used so far while working with Author and Publish instances, and feel the power of cache and other features of Dispatcher.

Feel free to delete all cached files in Dispatcher. In my case that’s folder
/docroot "/Library/WebServer/Documents"

Now if you use AEM 6.3, open the famous page http://localhost/content/we-retail/us/en.html. Page will be loaded with status 200. In “docroot” folder you will see folder structure /content/we-retail/us and file en.html inside it as well as etc folder with static *.js, *.css and image files. If you refresh the page, you’ll see that it’s loaded with status 304 and much faster than before since it was cached during the previous load.

Now if you open the page http://localhost/content/we-retail/us/en/men.html you will notice that a new folder (en) has been created, next to the file en.html, and in that folder there’s file men.html.

Try to open the same page with selectors and you will see that Dispatcher will cache these responses as well in files with selectors. For example:

What about suffixes? They can be cached as well but under the two conditions:

  1. When a suffix is used, the rest of the page URL has to be different compared to the case when a suffix is not used. It practically means that some unique selector has to be there, any selector that will not be used when URL for the same page does not have a suffix
  2. a suffix has to end with ‘.html’

Therefore http://localhost/content/we-retail/us/en/men.cache_it_please.html/this_is_suffix.html or http://localhost/content/we-retail/us/en/men.selector1.cache_it_please.html/this_is_suffix.html

Why unique selector? When you load http://localhost/content/we-retail/us/en/men.html, the content will be cached in file en/men.html. Now if you call the same URL with suffix this_is_suffix.html, Dispatcher has to create folder en/men.html and in that folder, the file this_is_suffix.html will be created. Practically it means that if you intend to call the same URL with and without a suffix, we have to have both file en/men.html and folder en/men.html and that’s not possible. Workaround for this problem is to use an additional selector when a suffix is there, so we will have file en/men.html and folder en/men.withsuffix.html with file this_is_suffix.html in it.

You’ve tried all of this and still you have problems with suffix URLs? In that case check this post or:

  1. go to your apache installation
  2. open file httpd.conf for editing
  3. find lines that look like this
    Options FollowSymLinks MultiViews
    AllowOverride all
    

    for your cache directory

  4. delete MultiViews

For more info read the official documentation.

Everything mentioned above works also for parts of a page (that are usually loaded with AJAX), so try these URLs one by one and check what is happening with Dispatcher cache.

  • http://localhost/content/we-retail/us/en/men/_jcr_content/root/responsivegrid/title.html
  • http://localhost/content/we-retail/us/en/men/_jcr_content/root/responsivegrid/title.myselector.html
  • http://localhost/content/we-retail/us/en/men/_jcr_content/root/responsivegrid/title.myselector.html/this_is_suffix.html This will not work, right? And we know why.
  • http://localhost/content/we-retail/us/en/men/_jcr_content/root/responsivegrid/title.myselector.withsuffix.html/this_is_suffix.html
  • http://localhost/content/we-retail/us/en/men/_jcr_content/root/responsivegrid/title.myselector.withsuffix.html/this_is_also_suffix.html

What about GET parameters? In general, a content for URLs which contain GET parameters is not cached but parameters can be ignored if you configure Dispatcher to do it.

Flushing the cache

This sounds simple. A cache will be flushed when page is re-activated. But what to do if a cached content does not come from JCR, if it’s not related to one specific page? What if a content is loaded from a database or it comes from REST API?

Let’s say we have a page inbox.html and this page shows different content for different users. Username of an user is provided as a selector so we load the page as:

  • /inbox.anna.html
  • /inbox.maria.html
  • /inbox.sofia.html

We will find these files in Dispatcher cache. If we re-activate the page inbox.html, all this content will be invalidated in the cache, but we wouldn’t like that.

We’d like to invalidate the content on demand or on some event, for a particular user only (eg. maria). How to do that?

First of all, to invalidate the cache after re-activation, you need to have a replication agent properly configured and enabled on Publish and this is about how you can do it. Second, you need a bit of code to do the rest for you:

@Component(...)
@Service(value = ActivationService.class)
public class ActivationServiceImpl implements ActivationService {

private static final String SERIALIZATION_TYPE_FLUSH = "flush";
private static final ReplicationOptions DEFAULT_REPLICATION_OPTION = new ReplicationOptions();

@Reference
private AgentManager agentManager;

public void flushThisPage(String pagePath) {
List<Agent> flushAgents = getActiveFlushAgents(agentManager);
for (Agent agent : flushAgents) {
flush(agent, pagePath);
}
}

private List<Agent> getActiveFlushAgents(AgentManager agentManager) {
List<Agent> activeAgents = new ArrayList<>();
for (Agent agent : agentManager.getAgents().values()) {
if (agent.isEnabled() && isFlushQueue(agent)) {
activeAgents.add(agent);
}
}
return activeAgents;
}

private boolean isFlushQueue(Agent agent) {
return SERIALIZATION_TYPE_FLUSH.equals(agent.getConfiguration().getSerializationType());
}

private void flush(Agent agent, String path) {
try {
ReplicationAction action = new ReplicationAction(ReplicationActionType.ACTIVATE, path);
agent.replicate(action, ReplicationContent.VOID, DEFAULT_REPLICATION_OPTION);
}
catch (ReplicationException e) {
LOGGER.error("Failed to activate pages " + agent.getId(), e);
}
}
}

@Component(...)
@Service(value = EventListener.class)
public class CustomJcrListener implements EventListener {
private static final String RUNMODE_AUTHOR = "author";

@Reference
private ActivationService activationService;

@Reference
private SlingSettingsService settingsService;

@Activate
protected void activate(final ComponentContext context) {
if(isAuthor()) {
return;
}
// ...
}

@Override
public void onEvent(EventIterator eventIterator) {
if(eventIterator == null) {
return;
}

activationService.flushThisPage("/content/test/dummy/inbox.maria.html");
}

private boolean isAuthor() {
if (settingsService == null || settingsService.getRunModes() == null) {
return false;
}
return settingsService.getRunModes().contains(RUNMODE_AUTHOR);
}
}