📜 ⬆️ ⬇️

Forcing to caching: fasten L2 Apache Ignite cache to Activiti

It often happens that there is a good library, but something is missing in it, some pearl buttons. So it’s to me with Activiti , a rather popular business process engine with BPMN 2.0 support, valuable for its Java nativeness. Without going into details of the internal structure of this open source product, it is quite obvious that in his work he uses a variety of data: metadata of business process definitions, instance data and historical data. For their storage, Activiti uses a DBMS, allowing you to choose from DB2, H2, Oracle, MySQL, MS SQL and PostgreSQL. This engine is very good, and is used not only for small crafts. Perhaps the question of support for caching database accesses in this product arose not only from me. At least once he asked the developers , who responded to it in the sense that the metadata is cached, but for the rest of the data there is not much point in this and it is not easy. In principle, it is possible to agree about the absence of a big sense - the data of a specific instance or its historical data with a small probability can be reused. But the scenario, when this still happens, is also possible. For example, if we have a cluster of Activiti servers with a common base. In general, a person with an inquisitive mind may well want to have a decent cache in Activiti. For example, use Apache Ignite for this .

Under a cat an example of the decision of this problem, the code is laid out on GitHub .

Thinking of the task


What do we have for this? First of all, the developer-guaranteed cache of process definitions stored in java.util.HashMap, which cannot be called an enterprise solution. For access to the database, Activiti uses the Mybatis library, which, of course, caching supports. For its operation, Mybatis uses xml configurations, and Activiti has a lot of these xml, and they contain definitions of queries of approximately the following form:

<select id="selectJob" parameterType="string" resultMap="jobResultMap"> select * from ${prefix}ACT_RU_JOB where ID_ = #{id, jdbcType=VARCHAR} </select> 

The links below provide a habostat on how to cross Apache Ignite with Mybatis. From it, it becomes clear that if the select tag was set to useCache = "true" , and the cache type was specified ...
')
 <cache type="org.mybatis.caches.ignite.IgniteCacheAdapter" /> 

... that would be almost enough. It also indicates the micro-library org.mybatis.caches: mybatis-ignite in which there are exactly 2 classes and no specifics of Mybatis. That is, quite a common solution.

Although Activiti lives on GitHub and you can fork it, make changes to Mybatis configs and enjoy caching, I suggest not to go this way. This condemns us to maintaining our own version of a rather rather big project created for the sake of making foolish changes. But Activiti supports Spring Boot and this opens up new perspectives. For the experiment, the latter was taken at the time of writing the 4th beta of Activiti version 6.0.

Decision


Sql queries in Mybatis are described by the org.apache.ibatis.mapping.MappedStatement class, which, as it is not difficult to guess, has an isUseCache method. MappedStatement objects are returned by the org.apache.ibatis.session.Configuration class, which has a getMappedStatement method. And the configuration is created in the org.activiti.spring.SpringProcessEngineConfiguration class, which is injected during the Spring Boot autoconfiguration process. Thus, it is necessary to somehow influence the result returned by the MappedStatement class. Unfortunately, there are no very simple ways to do this, and I haven’t found anything better than to instruct everything using the cglib library, which comes to us with the spring. The algorithm is briefly like this: override the Spring Boot autoconfiguration for the SpringProcessEngineConfiguration object, which manages the Activiti configuration, replacing the object with its instrumented version, which returns the instrumented Configuration object, which returns the new MappedStatement objects (unfortunately, this is the final class, you cannot instruct it with cglib) who think they should use the cache. And yes, the new Configuration object knows about the existence of Apache Ignite. It may sound complicated, but in fact everything is transparent (just in case the link on the cglib guide is attached).

The final code will be
 @Configuration @ConditionalOnClass(name = "javax.persistence.EntityManagerFactory") @EnableConfigurationProperties(ActivitiProperties.class) public class CachedJpaConfiguration extends JpaProcessEngineAutoConfiguration.JpaConfiguration { @Bean @ConditionalOnMissingBean public SpringProcessEngineConfiguration springProcessEngineConfiguration( DataSource dataSource, EntityManagerFactory entityManagerFactory, PlatformTransactionManager transactionManager, SpringAsyncExecutor springAsyncExecutor) throws IOException { return getCachedConfig(super.springProcessEngineConfiguration (dataSource, entityManagerFactory, transactionManager, springAsyncExecutor)); } private SpringProcessEngineConfiguration getCachedConfig(final SpringProcessEngineConfiguration parentConfig) { Enhancer enhancer = new Enhancer(); CallbackHelper callbackHelper = new CallbackHelper(SpringProcessEngineConfiguration.class, new Class[0]) { @Override protected Object getCallback(Method method) { if (method.getName().equals("initMybatisConfiguration")) { return (MethodInterceptor) (obj, method1, args, proxy) -> getCachedConfiguration( (org.apache.ibatis.session.Configuration) proxy.invokeSuper(obj, args)); } else { return NoOp.INSTANCE; } } }; enhancer.setSuperclass(SpringProcessEngineConfiguration.class); enhancer.setCallbackFilter(callbackHelper); enhancer.setCallbacks(callbackHelper.getCallbacks()); SpringProcessEngineConfiguration result = (SpringProcessEngineConfiguration) enhancer.create(); result.setDataSource(parentConfig.getDataSource()); result.setTransactionManager(parentConfig.getTransactionManager()); result.setDatabaseSchemaUpdate("create-drop"); return result; } private org.apache.ibatis.session.Configuration getCachedConfiguration(org.apache.ibatis.session.Configuration configuration) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(org.apache.ibatis.session.Configuration.class); enhancer.setCallback(new CachedConfigurationHandler(configuration)); return (org.apache.ibatis.session.Configuration) enhancer.create(); } private class CachedConfigurationHandler implements InvocationHandler { private org.apache.ibatis.session.Configuration configuration; CachedConfigurationHandler(org.apache.ibatis.session.Configuration configuration) { this.configuration = configuration; this.configuration.addCache(IgniteCacheAdapter.INSTANCE); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object originalResult = method.invoke(configuration, args); if (method.getName().equals("getMappedStatement")) { return getCachedMappedStatement((MappedStatement) originalResult); } return originalResult; } } private MappedStatement getCachedMappedStatement(MappedStatement mappedStatement) { return new MappedStatement .Builder(mappedStatement.getConfiguration(), mappedStatement.getId(), mappedStatement.getSqlSource(), mappedStatement.getSqlCommandType()) .databaseId(mappedStatement.getDatabaseId()) .resource(mappedStatement.getResource()) .fetchSize(mappedStatement.getFetchSize()) .timeout(mappedStatement.getTimeout()) .statementType(mappedStatement.getStatementType()) .resultSetType(mappedStatement.getResultSetType()) .parameterMap(mappedStatement.getParameterMap()) .resultMaps(mappedStatement.getResultMaps()) .cache(IgniteCacheAdapter.INSTANCE) .useCache(true) .build(); } } 


Pay attention to the line:

 result.setDatabaseSchemaUpdate("create-drop"); 

Here we have provided the automatic creation of Activiti tables. Do not do this in production.

Now you need to connect Ignite. I will not describe its installation and configuration here, the version was used 1.7.0. In the simplest version that I used, it is enough just to download and unpack it. Its configuration in the application can be done in two ways: via xml, since Ignite is a Spring application, or by Java code. I chose the second option:

The simplest config for Ignite in Java
  IgniteConfiguration igniteCfg = new IgniteConfiguration(); igniteCfg.setGridName("testGrid"); igniteCfg.setClientMode(true); igniteCfg.setIgniteHome("<IGNITE_HOME>"); CacheConfiguration config = new CacheConfiguration(); config.setName("myBatisCache"); config.setCacheMode(CacheMode.LOCAL); config.setStatisticsEnabled(true); config.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC); igniteCfg.setCacheConfiguration(config); TcpDiscoverySpi tcpDiscoverySpi = new TcpDiscoverySpi(); TcpDiscoveryJdbcIpFinder jdbcIpFinder = new TcpDiscoveryJdbcIpFinder(); jdbcIpFinder.setDataSource(dataSource); tcpDiscoverySpi.setIpFinder(jdbcIpFinder); tcpDiscoverySpi.setLocalAddress("localhost"); igniteCfg.setDiscoverySpi(tcpDiscoverySpi); TcpCommunicationSpi tcpCommunicationSpi = new TcpCommunicationSpi(); tcpCommunicationSpi.setLocalAddress("localhost"); igniteCfg.setCommunicationSpi(tcpCommunicationSpi); 


The IgniteCacheAdapter class, in which this configuration lies, is based on a simplified version of the class from the org.mybatis.caches library: mybatis-ignite. Actually, that's all, our requests are cached. Pay attention to the specified path to the runtime Ignite, here it is necessary to substitute your own.

results


You can test the application using the calls of the REST services described in the guide [2], there is a simple business process for reviewing the summary. By running several times, you can see the statistics, the collection of which was enabled with the config.setStatisticsEnabled (true) command:

 Ignition.ignite("testGrid").getOrCreateCache("myBatisCache").metrics(); 

In the debug you can see these metrics, in particular, the number of reads from the cache and the number of misses. After 2 process launches 16 readings and 16 misses. That is, the cache never hit.

findings


Specifically, in the considered example, as it turned out, L2 cache is not needed. But it was a very simple and not indicative example. Perhaps in a more complex topology and with a different nature of the load, with several users, the picture will be different. As they say, we will look for ...

The article also showed the possibility of not very rude intervention in a large library for a significant change in its behavior.

Links


Source: https://habr.com/ru/post/310170/


All Articles