Примус до кешуванню: прикручуємо L2 кеш Apache Ignite до Activiti

Часто так буває, що є хороша бібліотека, а чого в ній не вистачає, якихось перламутрових гудзиків. Так і мені з Activiti, досить популярним движком бізнес-процесів з підтримкою BPMN 2.0, цінних своєї Java-нативностью. Не вдаючись у подробиці внутрішнього устрою цього продукту з відкритим вихідним кодом, досить очевидно, що в своїй роботі він використовує різноманітні дані: метадані визначень бізнес-процесів, дані примірників і історичні дані. Для їх зберігання Activiti використовує СУБД, дозволяючи вибрати з DB2, H2, Oracle, MySQL, MS SQL і PostgreSQL. Цей движок дуже непоганий, і використовується не тільки для маленьких виробів. Можливо, питання про підтримку кешування звернень до БД в цьому продукті виник не тільки у мене. Як мінімум один раз він задавався розробників, які на нього відповіли в тому сенсі, що метадані кешуються, а для решти даних великого сенсу в цьому немає і це не просто. В принципі, про відсутність великого сенсу погодитися можна — дані конкретного екземпляра або його історичні дані з невеликою ймовірністю можуть знадобитися повторно. Але сценарій, коли таке все-таки трапиться, теж можливий. Наприклад, якщо у нас кластер серверів Activiti з загальною базою. Загалом, людина з допитливим розумом цілком може захотіти мати пристойний кеш в Activiti. Наприклад, використовувати в цій якості Apache Ignite.

Під катом приклад вирішення цієї проблеми, код викладений на GitHub.

Обдумування завдання
Що ж у нас для цього є? Перш за все, гарантований розробниками кеш визначень процесів, хранящийся в java.util.HashMap, що не можна назвати enterprise рішенням. Для доступу до бази Activiti використовує бібліотеку Mybatis яка, зрозуміло, підтримує кешування. Для свого функціонування Mybatis використовує xml-конфігурації, і цих xml Activiti є дуже багато і в них присутні визначення запитів приблизно такого вигляду:

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

У посиланнях нижче вказана хабростатья про те, як схрестити Apache Ignite з Mybatis. З неї стає зрозуміло, що якби в тезі select був встановлений ознака useCache=«true», і був вказаний тип кешу…

<cache type="org.mybatis.caches.ignite.IgniteCacheAdapter" />

… то цього було б досить. Там же вказується мікро-бібліотека org.mybatis.caches:mybatis-ignite у якій рівно 2 класу і ніякої специфіки саме Mybatis. Тобто, цілком спільне рішення.

Хоча Activiti живе на GitHub'е і його можна безперешкодно форкнуть, внести зміни в конфіги Mybatis і насолоджуватися кешуванням, пропоную цим шляхом не йти. Це прирікає нас на підтримку власної версії вельми немаленького проекту, створеного задля внесення ерундовых змін. Але Activiti підтримує Spring Boot і це відкриває нові перспективи. Для експерименту взята остання на момент написання статті 4-я бета Activiti версії 6.0.

Рішення
Sql-запити в Mybatis описуються класом org.apache.ibatis.mapping.MappedStatement, у якого, як не важко здогадатися, є метод isUseCache. Об'єкти MappedStatement повертає клас org.apache.ibatis.session.Configuration, у якого є метод getMappedStatement. А конфігурація створюється в класі org.activiti.spring.SpringProcessEngineConfiguration, який инжектится в процесі автоконфигурирования Spring Boot. Таким чином, треба якось вплинути на результат, що повертається класом MappedStatement. На жаль, зовсім простих шляхів для цього немає, і я не знайшов нічого кращого, як проинструментировать все підряд з допомогою бібліотеки cglib, яка потрапляє до нас разом зі спрингом. Алгоритм коротенько такий: перевизначаємо автоконфигурацию Spring Boot для об'єкта SpringProcessEngineConfiguration, який управляє конфігуруванням Activiti, підміняючи об'єкт його инструментированной версією, яка повертає інструментований об'єкт Configuration, який повертає нові об'єкти MappedStatement (на жаль, це фінальний клас, його не можна проинструментировать з допомогою cglib), які думають, що вони повинні використовувати кеш. І так, новий об'єкт Configuration знає про існування Apache Ignite. Можливо, звучить складно, але насправді все прозоро (на всякий випадок посилання гайд по cglib додається).

Остаточний код буде такий
@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();
}
}


Зверніть увагу на рядок:

result.setDatabaseSchemaUpdate("create-drop");

Тут ми забезпечили автоматичне створення таблиць Activiti. Не робіть так продакшені.

Тепер треба підключити Ignite. Його установку і налаштування тут описувати не буду, використовувалася версія 1.7.0. У найпростішому варіанті, який я використовував, його досить просто завантажити і розпакувати. Його конфігурацію в додатку можна виконати двома способами: через xml, так як Ignite це Spring-додаток, або Java кодом. Я вибрав другий варіант:

Найпростіший конфіг для Ignite на 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);


Клас IgniteCacheAdapter, в якому лежить ця конфігурація, заснований на спрощеній по-максимуму версії класу з бібліотеки org.mybatis.caches:mybatis-ignite. Власне це все, наші запити кешуються. Зверніть увагу на вказаний шлях до рантайму Ignite, тут треба підставити свій.

Результати
Протестувати додаток можна за допомогою викликів REST-сервісів, описаних в гайде [2], там є простенький бізнес-процес для розгляду резюме. Позапускав кілька разів, можна подивитися статистику, збір якої був включений командою config.setStatisticsEnabled(true):

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

У дебаге можна подивитися ці метрики, зокрема, кількість читань з кешу і кількість промахів. Після 2 запусків процесу 16 читань і 16 промахів. Тобто в кеш жодного разу не потрапили.

Висновки
Конкретно в розглянутому прикладі, як виявилося, L2 кеш не потрібен. Але це був дуже простий і не показовий приклад. Можливо, більш складної топології і з іншим характером навантаження, з декількома користувачами картина буде інша. Як кажуть, будемо шукати…

Також в статті була показана можливість не дуже грубого втручання у велику бібліотеку для істотної зміни її поведінки.

Посилання
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.