Server-Side Parallel Hst Component Preprocessing
Introduction
HST provides a way to achieve parallel programming at the HstComponent level by the #prepareBeforeRender(HstRequest, HstResponse) method in the HstComponent interface. Because the #prepareBeforeRender(HstRequest, HstResponse) method is always to be invoked before #doBeforeRender(HstRequest, HstResponse) by the HST container, it is possible to have a preparation step in the #prepareBeforeRender(HstRequest, HstResponse) method of HstComponents, such as submitting a concurrent task to the Executor and get the results of each execution later in a #doBeforeRender(HstRequest, HstResponse) method. This makes it easier to achieve parallel programming with HstComponents.
Problems in Context
Suppose there are multiple HstComponents (C1, C2, ..., Cn), in a page, that need to read data from the remote backends such as Microservices through REST APIs. So, each component can be implemented to invoke remote backend services in #doBeforeRender(HstRequest, HstResponse) methods.
In this scenario, if we add up the response time of the concurrent task execution in the #doBeforeRender(HstRequest, HstResponse) method call on each HstComponent be (R1, R2, ..., Rn), then the response time of the whole page will be the sum of (R1, R2, ..., Rn) plus slack (for the rest such as rendering templates) because the HST container invokes #doBeforeRender(HstRequest, HstResponse) method in a sequential way, not in a parallel way.
In most cases, this wouldn't be a problem because data retrievals in #doBeforeRender(HstRequest, HstResponse) method should be fast enough from JCR (thanks to its internal caches and indexes) or some other local data stores, and multiple request handlings are actually being processed in parallel by the servlet container anyway.
However, sometimes it could be very useful if you can execute invocations on the backend in parallel to take advantage of various modern parallel computing powers for specific use cases.
HST provides a way to achieve this kind of parallel programming.
Parallel Programming with HstComponents
There could be various ways to achieve parallel programming in Java, but in this article let's take a popular concurrency programming model just for simplicity in the following assumptions:
- You could have an Executor (java.util.concurrent.Executor) service which is responsible for concurrent task executions. Executors are typically implemented with thread pools.
- You create an executable task which is type of either java.lang.Runnable or java.util.concurrent.Callable, depending on whether or not you need to get the return of the execution. When java.util.concurrent.Callable is used, you can get the return of the execution through java.util.Future#get(), for instance.
To support this kind of parallel programming, the HstComponent interface has the #prepareBeforeRender(HstRequest, HstResponse) method. The #prepareBeforeRender(HstRequest, HstResponse) method will always be invoked before #doBeforeRender(HstRequest, HstResponse) by HST container. So, it is possible to have a preparation step in the #prepareBeforeRender(HstRequest, HstResponse) method of HstComponents, such as submitting a concurrent task to the Executor and get the results of each execution later in #doBeforeRender(HstRequest, HstResponse) method.
In summary, when you want to implement this kind of parallel programming with HstComponents, you can do the following just as one of typical examples:
- Establish an Executor service. e.g, java.util.concurrent.Executor, org.springframework.core.task.AsyncTaskExecutor, etc.
- Create a task and submit it to the Executor in #prepareBeforeRender(HstRequest, HstResponse) method. If the task is a java.util.Future object, you may store it into an HstRequest attribute.
- If you want to get the result of the concurrent execution task submission from the #prepareBeforeRender(HstRequest, HstResponse) method, then you may retrieve the result in #doBeforeRender(HstRequest, HstResponse) method. If it is a java.util.Future object, you may retrieve it from the HstRequest attribute and invoke the java.util.Future#get() method. You may use the results from the concurrent execution afterward. e.g, in templates.
In this scenario, the response time of the page would be about the maximum of (R1, R2, ..., Rn) plus slack because of the concurrent HstComponent designs as explained above.
Let's see this scenario in the following diagram:
Actually the idea is very simple. Only with #doBeforeRender(HstRequest, HstResponse) method in each HstComponent, it is hard or impossible to implement parallel execution tasks in HstComponent level. But now you may submit parallel execution tasks in #prepareBeforeRender(HstRequest, HstResponse) methods which are invoked in an earlier processing cycle of HST Container than #doBeforeRender(HstRequest, HstResponse) method invocations, and so the parallel execution tasks will be started in parallel by the Executor service which probably has a concurrent execution thread pool in most cases. Afterward, you may retrieve each return value (if needed) from the parall execution task in #doBeforeRender(HstRequest, HstResponse) method in each HstComponent in order to fill in the model attributes to be used by templates in the end.
As an example scenario, you can submit java.util.concurrent.Future instances (as holders of the parallel execution tasks) to the Executor in #prepareBeforeRender(HstRequest, HstResponse) method in HstComponents, so the Executor service can execute those tasks in parallel. Finally, you can get the results through java.util.concurrent.Future#get() method in #doBeforeRender(HstRequest, HstResponse) method, waiting for the specific concurrent task to be completed.
Simple Example in TestSuite
There is a very minimal example usnig #prepareBeforeRender(HstRequest, HstResponse) method implementations in the testsuite project.
After building and running the testsuite project locally, visit http://localhost:8080/site/ and click on "prepareBeforeRender Example" menu on the left menu. This example page shows a component (org.hippoecm.hst.demo.components.PrepareCallersParent) which contains 4 child components (type of org.hippoecm.hst.demo.components.PrepareCaller). PrepareCaller simply creates an asynchronous job and submit to task executor in #prepareBeforeRender(HstRequest, HstResponse) method, and get the results in #doBeforeRender(HstRequest, HstResponse) method. So, 4 jobs are being executed in parallel. As a result, the page shows less time laps than the sum of each child component's execution laps. See the report in the page for validation as well.