3 simple optimisations for AngularJS + Java architectures

base-arch

Nowadays, web app architectures are including an AngularJS layer for user interface and a Spring Boot for REST services. As in the popular JHipster, which is selecting this kind of architecture.

In this post, taking above image architecture as reference, a performance optimization based in 3 simple actions is analysed.

These tests have been performed on a custom lab configured in my own developing laptop by using Docker Compose.

We are starting with a findAll method which is returning all the rows stored on a big database table.

  • Time: 47,47 seconds
  • Size: 21,6 MB

1. Adding database indexes

It happens that JPA abstraction hides that real Java objects are performing SQL sentences on a real database. And this database always need help to improve response times.

Using indexes in JPA annotations

@Entity
@Table(name = "element")
public class ElementEntity {

	@ManyToOne(optional = false)
	@JoinColumn(name = "category", foreignKey = @ForeignKey(name = "FK_CATEGORY_ELEMENT"))
    @Indexed
	private CategoryEntity category;

}

Using indexes in database

CREATE INDEX element_category ON ELEMENT(CATEGORY);

After including indexes, we can see how time is reduced.

  • Time: 40,88 seconds
  • Size: 21,6 MB

2. Using cache in service layer

Many web apps services are accessed mainly in read mode. For these scenarios, using Spring Cache with a third party product like Hazelcast provides faster results, as database layer is not used while cache is active.

Using Spring Cache

@Service
public class ElementServiceImpl {
	
    @Cacheable("elementService.findAll")
    @Transactional
    public List<ElementDto> findAll() {
    }

    @CacheEvict(value = { "elementService.findAll" }, allEntries = true)
    @Transactional
    public void delete(Long id) {
    }

    @CacheEvict(value = { "elementService.findAll" }, allEntries = true)
    @Transactional
    public ElementEntity save(ElementDto elementUpdated) {
    }

}

After this second optimization, it can be seen that time is also lower than before.

  • Time: 13,84 seconds
  • Size: 21,6 MB

3. Compressing JSON responses

Anyway, we can do it better. This kind of architectures are sending big JSON data objects to AngularJS layer, as web interfaces are complex and they need to provide different options to user. In our sample, 21,6 MB are sent to client.

Including an Apache HTTPd directive, we can compress JSON data before sending it.

Declaring compression for JSON responses

AddOutputFilterByType DEFLATE application/json
DeflateCompressionLevel 7
DeflateMemLevel 8
DeflateWindowSize 10

As it can be seen, time is now lower again.

  • Time: 6,49 seconds
  • Size: 4,5 MB

Final words

In our tests, we have optimized a REST service also in time as in network bandwidth.

  • From 47,47 seconds to 6,49 seconds
  • From 21,6 MB to 4,5 MB

We have used just only three simple techniques:

  • Indexing database and JPA layer
  • Caching service layer
  • Compressing web layer

Any other optimization techniques can be used, but don’t forget to use ever the simple ones.

SSO support for Aikau apps

Alfresco Share webapp supports Kerberos SSO since many years ago. In order to enable SSO in a standalone Aikau application some of these SSO Alfresco Share resources have to be copied to the web application.

Starting from the default aikau-sample web app, following steps are including these resources to enable SSO support.

Creating a new Aikau client

From the command line a new client project can be created:

mvn archetype:generate -DarchetypeCatalog=https://artifacts.alfresco.com/nexus/content/groups/public/archetype-catalog.xml \
-DarchetypeGroupId=org.alfresco -DarchetypeArtifactId=aikau-sample-archetype -DarchetypeVersion=RELEASE

A new label has been added to identify the user logged in the web app at home page.

{
    name: "alfresco/html/Label",
       config: {
          label: "Current user: " + user.fullName,
          additionalCssClasses: "bold {additionalCssClasses}"
    }
}

Including Maven dependencies

As SSO filter requires some classes which are part of share artifact, pom.xml has to be modified

<dependency>
    <groupId>org.alfresco</groupId>
    <artifactId>share</artifactId>
    <version>5.1.g</version>
    <classifier>classes</classifier>
</dependency>

Declaring the filter in web application descriptor

Once our SSO classes are available, we adapt our web.xml to filter the requests.

A. Filter definition

<filter>
  <description>Share SSO authentication support filter.</description>
  <filter-name>SSOAuthenticationFilter</filter-name>
  <filter-class>org.alfresco.web.site.servlet.SSOAuthenticationFilter</filter-class>
  <init-param>
     <param-name>endpoint</param-name>
     <param-value>alfresco</param-value>
  </init-param>
</filter>

B. Filter URL patterns

<filter-mapping>
  <filter-name>SSOAuthenticationFilter</filter-name>
  <url-pattern>/page/*</url-pattern>
</filter-mapping>

<filter-mapping>
  <filter-name>SSOAuthenticationFilter</filter-name>
  <url-pattern>/p/*</url-pattern>
</filter-mapping>

<filter-mapping>
  <filter-name>SSOAuthenticationFilter</filter-name>
  <url-pattern>/proxy/*</url-pattern>
</filter-mapping>

C. Spring Listener

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/config/web-application-config.xml</param-value>
</context-param>

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>   

Including Kerberos properties in Surf configuration file

In Alfresco Share, Surf configuration can be included at share-config-custom.xml. This configuration can be copied to surf.xml in our Aikau web app.

<!-- KERBEROS SETTINGS -->
<config evaluator="string-compare" condition="Kerberos" replace="true">
  <Kerberos>
    <kerberos>
       <password>password</password>
       <realm>KEENSOFT.LOCAL</realm>
       <endpoint-spn>http/dms.keensoft.local@KEENSOFT.LOCAL</endpoint-spn>
       <config-entry>ShareHTTP</config-entry>
    </kerberos>
  </Kerberos>
</config>

<config evaluator="string-compare" condition="Remote">
  <remote>
     <endpoint>
        <id>alfresco-noauth</id>
        <name>Alfresco - unauthenticated access</name>
        <description>Access to Alfresco Repository WebScripts that do not require authentication</description>
        <connector-id>alfresco</connector-id>
        <endpoint-url>http://localhost:8080/alfresco/s</endpoint-url>
        <identity>none</identity>
     </endpoint>

     <endpoint>
        <id>alfresco</id>
        <name>Alfresco - user access</name>
        <description>Access to Alfresco Repository WebScripts that require user authentication</description>
        <connector-id>alfresco</connector-id>
        <endpoint-url>http://localhost:8080/alfresco/s</endpoint-url>
        <identity>user</identity>
     </endpoint>

  </remote>
</config>

<config evaluator="string-compare" condition="Remote">
  <remote>

     <connector>
        <id>alfrescoCookie</id>
        <name>Alfresco Connector</name>
        <description>Connects to an Alfresco instance using cookie-based authentication</description>
        <class>org.alfresco.web.site.servlet.SlingshotAlfrescoConnector</class>
     </connector>

     <endpoint>
        <id>alfresco</id>
        <name>Alfresco - user access</name>
        <description>Access to Alfresco Repository WebScripts that require user authentication</description>
        <connector-id>alfrescoCookie</connector-id>
        <endpoint-url>http://localhost:8080/alfresco/wcs</endpoint-url>
        <identity>user</identity>
        <external-auth>true</external-auth>
     </endpoint>

  </remote>
</config>

Caution: Check password, realm, endpoint-spn and config-entry to include your own data.

Fixing a broken link

Every SSO resource has been copied to Aikau web app by now, however a final touch has to be made. AikauLoginController.java is generated by Aikau archetype to work with the client, however the implementation is not compatible with SSO filter. A minor modification for extension is required.

public class AikauLoginController extends org.alfresco.web.site.servlet.SlingshotLoginController {

From now your Aikau web application is accepting SSO Kerberos request for authentication.

Final words

  • The source code for this blog post is available at https://github.com/angelborroy/aikau-kerberos-sso
  • More grained Maven dependencies can be configured, instead of full Share artifact, in order to save some JAR files from packaged WAR
  • Every Alfresco Share SSO resource has been copied without modification, only a minor change has been made to Aikau auto-generated source code

Alfresco: what happens when a folder is moved

Recently we had a use case where a folder including some thousands of hierarchical subfolders had to be moved inside Alfresco to another location.

alfresco-move-folder

Our client tried to move the folder, received a timeout error in Share web client and Alfresco server crashed some hours later by an OutOfMemoryError.

My first thought was that both actions were not linked, as a simple inspection at move-to.post.json.js revealed that the exception was captured but it was never logged. So, I tried again overwriting this web script to include a simple log sentence inside the catch. Surprisingly the behavior was exactly as before. And the exception was not logged.

I guessed that the process was still running in Alfresco repo, but a Share HTTP timeout had been returned. Executing a kill -3 to Alfresco Java process, I found following stack trace for the running thread:

"ajp-apr-8009-exec-8" #84 daemon prio=5 os_prio=0 tid=0x00007fabe8015800 nid=0x5fb7 runnable [0x00007fab9f402000]
   java.lang.Thread.State: RUNNABLE
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
	at java.net.SocketInputStream.read(SocketInputStream.java:170)
	at java.net.SocketInputStream.read(SocketInputStream.java:141)

    ...

	at org.alfresco.repo.domain.node.AbstractNodeDAOImpl.getChildAssocs(AbstractNodeDAOImpl.java:3484)

    ...

    at org.alfresco.repo.rule.ruletrigger.OnMoveNodeRuleTrigger.triggerChildrenRules(OnMoveNodeRuleTrigger.java:84)
	at org.alfresco.repo.rule.ruletrigger.OnMoveNodeRuleTrigger.triggerChildrenRules(OnMoveNodeRuleTrigger.java:86)
	at org.alfresco.repo.rule.ruletrigger.OnMoveNodeRuleTrigger.onMoveNode(OnMoveNodeRuleTrigger.java:69)

    ...

    at org.alfresco.repo.model.filefolder.FileFolderServiceImpl.moveOrCopy(FileFolderServiceImpl.java:1115)
	at org.alfresco.repo.model.filefolder.FileFolderServiceImpl.moveFrom(FileFolderServiceImpl.java:997)

    ...
    
    at org.mozilla.javascript.gen.classpath__alfresco_extension_templates_webscripts_org_alfresco_slingshot_documentlibrary_action_move_to_post_json_js_9._c_runAction_19(classpath*:alfresco/extension/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/move-to.post.json.js:930)

Alfresco was trying to find and trigger rules for every subfolder below targeted one (maybe a million) and all this job was being performed on the same database transaction. So, in the end, Alfresco crashed because of the memory consumption required by this huge transaction.

I decided to try again the operation but disabling globally rules previously (thanks for this, Douglas CR Paes) by using a script in JavaScript Console.

var context = Packages.org.springframework.web.context.ContextLoader.getCurrentWebApplicationContext();
var ruleService = context.getBean('RuleService', Packages.org.alfresco.service.cmr.rule.RuleService);

ruleService.disableRules();
logger.warn("After disabling: " + ruleService.isEnabled());

Once disabled, the operation run softly, so I enabled rules again in JavaScript Console.

var context = Packages.org.springframework.web.context.ContextLoader.getCurrentWebApplicationContext();
var ruleService = context.getBean('RuleService', Packages.org.alfresco.service.cmr.rule.RuleService);

ruleService.enableRules();
logger.warn("After enabling: " + ruleService.isEnabled());

After a while, some AJP timeout errors started to log at Apache HTTP. SOLR, which was installed in another server, had been querying intensively Alfresco. And I discovered that weird Cascade Update aspect.

alfresco-cascade-update

Every time a node is moved, a two properties (cascadeTx and cascadeCRC) aspect is created for that node. Surprisingly, this aspect is never removed. So every folder that once was moved in the system, has the aspect set.

alfresco-search-cascade

The aspect is used by SOLR to re-index the content of a moved folder, as it can be seen at SolrInformationServer class, and it includes a full and granular re-index operation for every children below that moved folder. So, as the operation is not designed again for heavy volumes, Tomcat AJP connector (or even HTTP one) can collapse.

And the worse part is that, if the operation has been successfully executed, Cascade Update aspect is not removed. So it lasts forever as junk in every moved node.

Some everyday operations in Alfresco, like moving a folder, seems to be as easy as an update in a database. However they can provoke serious problems in real systems as they involve different operations that has not been designed for heavy use.

Note As this is an enterprise customer, I have raised a new issue at Alfresco Support. I’ll update this post if I receive further information.