Forced caching: fasten L2 cache Apache Ignite to Activiti

It often happens that there is a good library, but something is missing in it, some kind of mother-of-pearl buttons. So me with Activiti , a fairly popular business process engine with support for BPMN 2.0, valuable for its Java nativeness. Without going into the details of the internal structure of this open source product, it is quite obvious that in its work it uses a variety of data: metadata for business process definitions, instance data, and historical data. Activiti uses a DBMS to store them, 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 supporting caching of database calls in this product arose not only for me. At least once heit was asked to developers who answered it in the sense that metadata is cached, but for the rest of the data there isn’t much sense in this and it’s not easy. In principle, one can agree about the absence of a lot of sense - the data of a particular instance or its historical data with a small probability may 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 as this .

Under kat, an example of solving this problem, the code is posted on GitHub .

Task thinking


What do we have for this? First of all, the process definition cache guaranteed by the developers, stored in java.util.HashMap, which cannot be called an enterprise solution. Activiti uses the Mybatis library to access the database, which, of course, supports caching. Mybatis uses xml-configurations for its functioning, and there are a lot of these xml in Activiti and they contain definitions of requests of approximately this type:


The links below show habrostat on how to cross Apache Ignite with Mybatis. From it it becomes clear that if the tag useCache = "true" were set in the select tag , and the cache type was specified ...


... then that would be almost enough. The micro-library org.mybatis.caches is also indicated there: mybatis-ignite in which there are exactly 2 classes and no specifics specifically Mybatis. That is, a completely general solution.

Although Activiti lives on GitHub and can be forked non-selectively, make changes to Mybatis configs and enjoy caching, I suggest not to go this way. This dooms us to maintaining our own version of a rather big project, created for the sake of making nonsense changes. But Activiti supports Spring Boot and this opens up new perspectives. For the experiment, the last one at the time of writing was the 4th beta of Activiti version 6.0.

Decision


Sql queries in Mybatis are described by the class org.apache.ibatis.mapping.MappedStatement, which, as you might 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 class org.activiti.spring.SpringProcessEngineConfiguration, which is injected during the autoconfiguration of Spring Boot. Thus, you need to somehow influence the result returned by the MappedStatement class. Unfortunately, there are no easy ways for this, and I did not find anything better than to instruct everything with the cglib library, which gets to us along with the spring. The algorithm is briefly like this: we redefine the Spring Boot auto-configuration for the SpringProcessEngineConfiguration object, which controls the activation of Activiti, replacing the object with its instrumented version, which returns an instrumented Configuration object that returns new MappedStatement objects (unfortunately, this is the final class, it cannot be instructed with cglib), who think that they should use the cache. And yes, the new Configuration object is aware of the existence of Apache Ignite. It may sound complicated, but in fact everything is transparent (just in case, the cglib guide link is attached).

The final code will be like this
@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 quite simple to download and unzip it. There are two ways to configure it in the application: via xml, since Ignite is a Spring application, or 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("");
        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 the maximum simplified version of the class from the org.mybatis.caches library: mybatis-ignite. That's all, our requests are cached. Pay attention to the specified path to the Ignite runtime, here you need to substitute your own.

results


You can test the application using the REST services calls described in the guide [2], there is a simple business process for reviewing a resume. By running it several times, you can see the statistics, the collection of which was enabled by 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 starts of the process, 16 readings and 16 misses. That is, they never hit the cache.

conclusions


Specifically, in the considered example, as it turned out, the 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 search ...

Also, the article showed the possibility of not very rude interference in a large library for a significant change in its behavior.

References



Also popular now: