The content of this wiki page is not finalized yet and will get changed frequently |
Mentor | Gurpreet Luthra (Unlicensed) |
---|---|
Assigned Contributor | Anuruddha Premalal |
Mifosx is built with the multitenancy concept in which a single Mifosx deployment can server several organizations. It is very important to consider performance optimization in this multitenant environment. The main focus of this project would be to implement a distributed caching system to cache most frequently accessed data.
Since MifosX is a stateless application, adding caching can contribute in a greater degree to improve the API request processing performance. However the tricky part of this project is addressing the multitenant nature.
Spring framework caching abstraction will used on top of the data-service methods which needs to be cached. And JDK concurrent has map will used as the initial caching storage, this can be easily replaced by a distributed caching storage system in future.
Git hub development branch : Link |
Test the caching functionality by implementing it on few selected methods.
We can easily enable tenant aware caching by annotating the target method with @cachable
@Cacheable(value = "users", key = "T(org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil).getTenant().gettenantIdentifier().concat(#username)") public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException, DataAccessException { //method body } |
I have subdivide the cache based on the tenant name by adding tenant name to the parameters of the target method.
As stated in the summary, I have used a concurrent hash map to store the cached results.
<cache:annotation-driven /> <!-- generic cache manager --> <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property name="caches"> <set> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="clients" /> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="users" /> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="tenants" /> </set> </property> </bean> |
Spring insight is a great tool to monitor java web applications. I've used this tool to measure the performance of MifosX platform before and after adding caching.
Below are the steps to deploy MifosX on a spring insight instance.
Download tc Server Developer edition from the VMware Download Center.
Create a directory for tc Server, for example:
/home/tcserver
Unpack the tc Server archive into the directory created in the previous step, such that the tc Server home directory is a sub- directory of it, for example:
/home/tcserver/vfabric-tc-developer-2.x.x.x
.
Create a tc Server instance where Insight will run:
In a terminal window, change to the tc Server home directory.
Create a tc Server instance with tcruntime-instance.sh
or tcruntime-instance.bat
, using the insight
template.
command: ./tcruntime-instance.sh create Insight
-t insigh
Start the new tc Server instance using the tcruntime-ctl.sh
or tcruntime-ctl.bat
command.(This command should execute from .../insight/bin/)
On Unix, to start an instance named "Insight":
./tcruntime-ctl.sh Insight start
On Windows, install a Windows service for the instance before you start it the first time (After that, you can control the service from the Windows services control panel.) To install an instance named "Insight" as a Windows service, and then start it :
tcruntime-ctl.bat Insight install
tcruntime-ctl.bat Insight start
Deploy MifosX application to tc Server by copying the WAR file to the webapps
directory of the tc runtime instance. update conf/server.xml
file to support MifosX application.
(How to create Amazon public AIM from scratch). Below is a sample server.xml file.
<?xml version="1.0"?> <Server port="${base.shutdown.port}" shutdown="SHUTDOWN"> <Listener className="org.apache.catalina.core.JasperListener"/> <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/> <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/> <Listener className="com.springsource.tcserver.serviceability.deploy.TcContainerDeployer"/> <Listener accessFile="${catalina.base}/conf/jmxremote.access" authenticate="true" bind="127.0.0.1" className="com.springsource.tcserver.serviceability.rmi.JmxSocketListener" passwordFile="${catalina.base}/conf/jmxremote.password" port="${base.jmx.port}" useSSL="false"/> <GlobalNamingResources> <Resource auth="Container" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" name="UserDatabase" pathname="conf/tomcat-users.xml" type="org.apache.catalina.UserDatabase"/> <Resource type="javax.sql.DataSource" name="jdbc/mifosplatform-tenants" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/mifosplatform-tenants" username="root" password="mysql" initialSize="3" maxActive="10" maxIdle="6" minIdle="3" validationQuery="SELECT 1" testOnBorrow="true" testOnReturn="true" testWhileIdle="true" timeBetweenEvictionRunsMillis="30000" minEvictableIdleTimeMillis="60000" logAbandoned="true" suspectTimeout="60" /> </GlobalNamingResources> <Service name="Catalina"> <Executor maxThreads="300" minSpareThreads="50" name="tomcatThreadPool" namePrefix="tomcat-http--"/> <Engine defaultHost="localhost" name="Catalina"> <Realm className="org.apache.catalina.realm.LockOutRealm"> <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> </Realm> <Host appBase="webapps" autoDeploy="true" deployOnStartup="true" deployXML="true" name="localhost" unpackWARs="true"> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" pattern="%h %l %u %t "%r" %s %b" prefix="localhost_access_log." suffix=".txt"/> </Host> </Engine> <Connector protocol="org.apache.coyote.http11.Http11Protocol" port="8443" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" keystoreFile="C:\res\mykey" keystorePass="mifostest" clientAuth="false" sslProtocol="TLS" URIEncoding="UTF-8" compression="force" compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css"/> <Connector URIEncoding="UTF-8" connectionTimeout="20000" port="${insight.http.port}" protocol="HTTP/1.1"/> </Service> <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/> </Server> |
Access the Spring Insight Dashboard by opening http://localhost:8080/insight
in your browser.
There were total of 27 API end point classes which were observed in-order to choose the best candidates for caching.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Service End point | Cached method | T1 | T2 | T3 | Notes |
---|---|---|---|---|---|
Organization related | |||||
GET/mifosng-provider/api/v1/offices | OfficeReadPlatformServiceImpl.retrieveAllOffices [OfficeAPI] | 40.8 | 21.8 | 12.1 | Most frequently used end point. (Used in 6 places of the reference UI application) |
GET/mifosng-provider/api/v1/offices/template | OfficeReadPlatformServiceImpl.retrieveAllOfficesForDropDown [OfficeAPI] | 29.4 | 16.3 | 9.4 | This method is used in 13 places of the MifosX platform |
GET/mifosng-provider/api/v1/clients/template | OfficeReadPlatformServiceImpl.retrieveAllOfficesForDropDown [OfficeAPI] is the only data base call (which is already cached) invoked in ClientReadPlatformServiceImpl.retrieveTemplate [Clients API] no need of caching Clients API retrieveTemplate() | 32.4 | 25.8 | 14.9 | Creation of a new client is one of the most frequently used feature of MifosX |
GET/mifosng-provider/api/v1/clients/{clientId} | ClientReadPlatformServiceImpl.retrieveOne [Clients API] *(Suffering with issue 3 mentioned in challenges section) | This method is used in 8 places of the MifosX platform | |||
GET/mifosng-provider/api/v1/groups/{groupId} | GroupReadPlatformServiceImpl.retrieveOne [Groups API] *(Unable to cache - issue 3 ) | Used in 8 places of MifosX | |||
GET/mifosng-provider/api/v1/loans/template | LoanProductReadPlatformService.retrieveAllLoanProductsForLookup [Loans API] LoanReadPlatformService.retrieveLoanProductDetailsTemplate [Loans API] LoanReadPlatformService.retrieveClientDetailsTemplate [Loans API] LoanReadPlatformService.retrieveCalendars [Loans API] GroupReadPlatformService.retrieveOne [Groups] * (issue 3) LoanReadPlatformService.retrieveGroupDetailsTemplate [Loans API] LoanReadPlatformService.retrieveGroupAndMembersDetailsTemplate StaffReadPlatformService.retrieveAllLoanOfficersInOfficeById [Staff API] StaffReadPlatformService.retrieveAllLoanOfficersInOfficeAndItsParentOfficeHierarchy [Staff API] | 43.8 | 25.6 | 15.5 | Most frequently used feature |
GET/mifosng-provider/api/v1/savingsaccounts/template | SavingsAccountReadPlatformService.retrievetemplate [Savings accounts API] *(issue 1 ,3) | 25.8 | |||
GET/mifosng-provider/api/v1/accountingrules | AccountingRuleReadPlatformService.retrieveAllAccountingRules [Accounting rules API] *(self invocation methods - issue 2) | ||||
GET/mifosng-provider/api/v1/charges | ChargeReadPlatformService.retrieveAllCharges [Charges API] | 27.4 | 19.6 | 10.2 | |
GET/mifosng-provider/api/v1/funds | FundReadPlatformService.retrieveAllFunds [Funds API] | 31.6 | 21.7 | 10.8 | This method is used in 4 places of the MifosX platform |
GET/mifosng-provider/api/v1/staff | StaffReadPlatformService.retrieveAll [Staff API] *(issue 2) | ||||
System related | |||||
GET/mifosng-provider/api/v1/codes | CodeReadPlatformService.retrieveAll [Codes API] | 34.2 | 26.4 | 12.7 | |
GET/mifosng-provider/api/v1/codes/{codeId}/codevalues | CodeValueReadPlatformService.retrieveAllCodeValues [Codes API] | 30.2 | 25.9 | 14.1 | |
GET/mifosng-provider/api/v1/datatables | ReadWriteNonCoreDataService.retrieveDatatableNames [Datatables API] | 45.2 | 25.5 | 13.2 | |
GET/mifosng-provider/api/v1/reports | ReadReportingService.retrieveReportList [Reports API] *(issue 2) |
T1 - Avg. response time before caching (ms)
T2 - Avg. response time after caching only the end point (ms)
T3 - Avg. response time after caching + caching the security filter (ms)
|
Method : Above T1,T2,T3 values were obtained by averaging 5 API request processing times which were obtained using spring insight made via Firefox REST client.
Action | Associated API requests |
---|---|
Add new client | GET/mifosng-provider/api/v1/clients/template |
Add new loan application | GET/mifosng-provider/api/v1/loans/template |
Enter repayment |
Cache eviction is really an important consideration, it is not enough to cache results but it is also necessary to evict the stale or unused data from the cache. Spring abstraction allows us to use another annotation to achieve this goal.
All the update and delete methods of the above identified cacheable methods of the data service layer must be cached. It is obvious that the delete or the update operation on a particular entity must trigger cache eviction.
Enabling cache eviction on updateOffice :
@Transactional @Override @Caching(evict = { @CacheEvict(value = "offices", key = "T(org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil).getTenant().gettenantIdentifier().concat(#officeId)"), @CacheEvict(value = "office_template", key = "T(org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil).getTenant().gettenantIdentifier().concat(#officeId)") }) public CommandProcessingResult updateOffice(final Long officeId, final JsonCommand command) { //method body } |
The target cache eviction methods are on XXXWritePlatformServiceJpaRepositoryImpl classes.
Returned values depends not only with the method arguments but also with the logged in user.
/* * Although this method is eligible for caching we cannot add caching for this method * ,as the return value also depends on the logged in user. * */ // @Cacheable(value = "clients", key = // "T(org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getName().concat(#clientId)") public ClientData retrieveOne(final Long clientId) { try { AppUser currentUser = context.authenticatedUser(); String hierarchy = currentUser.getOffice().getHierarchy(); String hierarchySearchString = hierarchy + "%"; String sql = "select " + this.clientMapper.schema() + " where o.hierarchy like ? and c.id = ?"; ClientData clientData = this.jdbcTemplate.queryForObject(sql, this.clientMapper, new Object[] { hierarchySearchString, clientId }); String clientGroupsSql = "select " + this.clientGroupsMapper.parentGroupsSchema(); Collection<GroupGeneralData> parentGroups = this.jdbcTemplate.query(clientGroupsSql, this.clientGroupsMapper, new Object[] { clientId }); return ClientData.setParentGroups(clientData, parentGroups); } catch (EmptyResultDataAccessException e) { throw new ClientNotFoundException(clientId); } } |
So far I've been using the default cachemanager provided by spring cache module. It is most suitable for small non distributed environments. When the systems grows JDK inmemory hash map will not be sufficient to server the demand. There are couple of alternatives available off the shelf for example Ehcahce, Memcached, Spring Gemfire. I've tried both ehcache and memcached they all works pretty well with spring cache abstraction thus there are couple of challenges remain with integration. More details about these framework integration can be found here.