Dispatcher Cache Invalidation - Programmatically


AEM Dispatcher :

The Dispatcher is Adobe Experience Manager's caching and/or load balancing tool that helps realize a fast and dynamic Web authoring environment. 

In a load balancing role, the Dispatcher distributes user requests (load) across different AEM instances (renders).

For caching, the Dispatcher works as part of an HTTP server, such as Apache, with the aim of storing (or "caching") as much of the static website content as possible and accessing the website's layout engine as infrequently as possible. 

For caching, the Dispatcher module uses the Web server's ability to serve static content. The Dispatcher places the cached documents in the document root of the Web server.

Dispatcher Caching:

The Dispatcher uses the web server's ability to serve static content. The Dispatcher stores cached documents in the web server’s document root. The Dispatcher has two primary methods for updating the cache content when changes are made to the website.

  • Content Updates remove the pages that have changed, as well as files that are directly associated with them.
  • Auto-Invalidation automatically invalidates those parts of the cache that may be out of date after an update. For example, it effectively flags relevant pages as being out of date, without deleting anything.
Although most of the times AEM customers rely on the Flush Agents which is best suited for , but there maybe some scenario like below in which we need to programmatically flush/clear the dispatcher cache. 

1. Your site is using MSM extensively.

2. Your site is built on AEM React SPA , which leverages JSON response for each page and hence parent page JSON might not update when you create, edit or update subpages/child pages.

3. Glitches in Flush Agent which might disrupt the production content deployment.

So to avoid such situations and not to depend entirely on the Flush Agents, we can develop a code which on click of a button or creation of a page can flush the cache automatically.

Problem :

I had the similar scenario as of mentioned in point no 2 above. The site which I was developing was an AEM React SPA (Single Page Application) and it was a multilingual site making use of MSM. Every page had a JSON response. 

The whole flow was working normally when I realized that the parent pages JSON (/content/project/ca or /content/project/us) was cached and didn't update when the whole content tree is replicated/activated. I tried flushing the dispatcher cache by normal means but still the parent JSON was stuck at its old content. That's when I developed this code which on replication action and on publish instance could flush the cache automatically.

Solution:

Flushing dispatcher cache programmatically makes use of a Replication Preprocessor.
A Replication Preprocessor is a service that is called directly before the replication is started.
All the checks are performed at this state. 

Code:

A Replication Preprocessor Impl Class :

This class implements Preprocessor interface and overrides the void preprocess method.
Inside that method you can add your own definition like here I am calling another service impl class which has the main logic only when run modes is PUBLISH and replication action is ACTIVATE.

@Component(service = Preprocessor.class, immediate = true, name = "Project Replication Preprocessor") public class ReplicationPreprocessorImpl implements Preprocessor { public static final Logger LOGGER = LoggerFactory.getLogger(ReplicationPreprocessorImpl.class); @Reference private FlushParentPageJsonService flushParentPageJsonService; @Reference private SlingSettingsService slingSettingsService; /** * Override preprocess method of Preprocessor which prepare a replication only * if run mode is PUBLISH and replication action is ACTIVATE * * @param ReplicationAction the action * @param ReplicationOptions the replication options * @throws ReplicationException */ @Override public void preprocess(ReplicationAction action, ReplicationOptions options) throws ReplicationException { ReplicationActionType replicationType = action.getType(); LOGGER.debug("******replicationType**********{}", replicationType); LOGGER.debug("******run modes are**********{}", slingSettingsService.getRunModes()); if (slingSettingsService.getRunModes().contains(CommonConstants.PUBLISH) && replicationType.equals(ReplicationActionType.ACTIVATE)) { flushParentPageJsonService.invalidatePageJson(action.getPath()); } } }

Flush Parent Page Json Service Impl Class :

This is a service impl class for getting the dispatcher url through the flush agent and invalidating the parent JSON based on the resource page which is being activated/deactivated.

@Component(service = FlushParentPageJsonService.class) public class FlushParentPageJsonImpl implements FlushParentPageJsonService { public static final Logger LOGGER = LoggerFactory.getLogger(FlushParentPageJsonImpl.class); @Reference private ReadWriteService readWriteService; private int locationLevel = 5; private String dispatcherCacheUrl; /** * Override invalidatePageJson method of FlushParentPageJsonService * * @param String the pageUrl */ @Override public void invalidatePageJson(String pageUrl) { try { ResourceResolver resourceResolver = readWriteService.getReadService(); Resource publishReplicationAgentResource = resourceResolver .getResource(ReplicationConstants.REPLICATION_AGENT_PATH_PUBLISH); if (publishReplicationAgentResource != null) { getResourceNode(publishReplicationAgentResource, pageUrl); } } catch (LoginException e) { LOGGER.error("login exception:{} ", e); } } /** * Method to get content resource node of the publish replication agent. * * @param publishReplicationAgentResource , Resource Resolver object to get the resource properties * @param pageUrl , Resource page which is being * activated/deactivated * @return void */ private void getResourceNode(Resource publishReplicationAgentResource, String pageUrl) { Page publishReplicationAgentPage = publishReplicationAgentResource.adaptTo(Page.class); try { if (publishReplicationAgentPage != null) { Iterator <Page> publishAgentPageIterator = publishReplicationAgentPage.listChildren(); while (publishAgentPageIterator.hasNext()) { Page publishReplicationAgentsChildPage = publishAgentPageIterator.next(); Resource contentResource = publishReplicationAgentsChildPage.getContentResource(); Node contentResourceNode = contentResource.adaptTo(Node.class); if (contentResourceNode != null && ReplicationUtils.checkProperty(contentResourceNode)) { dispatcherCacheUrl = contentResourceNode.getProperty("transportUri").getValue().getString(); LOGGER.debug("getResourceNode :: dispatcherCacheUrl is {}", dispatcherCacheUrl); getTransportUri(pageUrl, dispatcherCacheUrl); } } } } catch (RepositoryException e) { LOGGER.error("getResourceNode :: RepositoryException is {}", e); } } /** * Method to get Transport Uri from the publish replication agent jcr:content * property. * * @param pageUrl , Resource page which is being activated/deactivated * @param dispatcherCacheUrl , The dispatcher cache url * @return void */ public void getTransportUri(String pageUrl, String dispatcherCacheUrl) { try { Resource pageUrlResource = readWriteService.getReadService().getResource(pageUrl); LOGGER.debug("getTransportUri :: Resource path:{}", pageUrl); if (pageUrlResource != null && pageUrlResource.adaptTo(Page.class) != null) { getParentModelJsonCheck(pageUrl, dispatcherCacheUrl); } else { LOGGER.debug("getTransportUri :: Resource is either NULL or not a content page for resource path: {}", pageUrl); } } catch (LoginException e) { LOGGER.error("getTransportUri :: LoginException is {}", e); } } /** * Method to check invalidation of parent JSON. * * @param pageUrl , Resource page which is being activated/deactivated * @param dispatcherCacheUrl , Dispatcher cache url page retrieved from publish replication agent * @return Boolean */ private void getParentModelJsonCheck(String pageUrl, String dispatcherCacheUrl) { String[] contentPathSplit = pageUrl.split("\\/"); if (contentPathSplit.length >= locationLevel) { StringBuilder parentPageJsonBuilder = new StringBuilder(); String parentPagePath = Arrays.stream(contentPathSplit).limit(locationLevel) .collect(Collectors.joining(CommonConstants.SLASH)); LOGGER.debug("getParentModelJsonCheck :: Parent Page Path {}", parentPagePath); if (parentPagePath.contains("/ca") || parentPagePath.contains("/us")) { parentPageJsonBuilder.append(parentPagePath).append(CommonConstants.DOT) .append(ReplicationConstants.MODEL).append(CommonConstants.DOT) .append(ReplicationConstants.JSON); HttpGet request = new HttpGet(dispatcherCacheUrl); try { HttpClientBuilder builder = HttpClientBuilder.create(); RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(10000) .setSocketTimeout(10000).build(); builder.setDefaultRequestConfig(requestConfig); HttpClient client = builder.build(); request.addHeader(ReplicationConstants.CQ_ACTION_HEADER, ReplicationConstants.ACTIVATE); request.addHeader(ReplicationConstants.CQ_HANDLE_HEADER, parentPageJsonBuilder.toString()); request.addHeader(ReplicationConstants.CQ_PATH, parentPageJsonBuilder.toString()); request.addHeader(ReplicationConstants.HOST_PROPERTY, ReplicationConstants.HOST_VALUE); HttpResponse response = client.execute(request); int statusCode = response.getStatusLine().getStatusCode(); LOGGER.debug("getParentModelJsonCheck :: status code is {}", statusCode); if (statusCode != HttpStatus.SC_OK) { LOGGER.error("Dispatcher Cache Invalidator returned status-code:{} with summary: {}", statusCode, response.getStatusLine()); } LOGGER.debug("getParentModelJsonCheck :: Invalidating {}", parentPageJsonBuilder); } catch (ClientProtocolException e) { LOGGER.error("Dispatcher Cache Invalidator ClientProtocolException:{} ", e); } catch (IOException e) { LOGGER.error("Dispatcher Cache Invalidator IOException:{} ", e); } finally { request.releaseConnection(); } } } else { LOGGER.debug("getParentModelJsonCheck :: Activated content page: {} doesn't contain country parent Page", pageUrl); } } }

Hope you found it useful
Happy Coding :) !!

Ping me on LinkedIn for more queries or comment down below.

Comments

  1. Hello Saumya! I'm working on AEM SPA with angular and I think I've the same problem as you. Do you have any git code or sample besides those 2 clasess?

    Thanks!

    ReplyDelete

Post a Comment

Popular posts from this blog

AEM as a Cloud Service

AEM Commerce as a Cloud Service