From bc1e758c743a25a03da8d5d86e38e6887ecfe673 Mon Sep 17 00:00:00 2001 From: Arnold Galovics Date: Sun, 12 Mar 2023 19:14:00 +0100 Subject: [PATCH] FINERACT-1724: Making global configuration caching transaction bound --- .../cache/CacheApiConstants.java | 2 +- .../command/UpdateCacheCommandHandler.java | 6 +- .../RuntimeDelegatingCacheManager.java | 84 ++++++++----------- .../domain/ConfigurationDomainServiceJpa.java | 33 ++------ .../GlobalConfigurationRepositoryWrapper.java | 9 ++ .../core/config/FineractProperties.java | 7 ++ .../core/config/{ => cache}/CacheConfig.java | 19 ++++- .../SpecifiedCacheSupportingCacheManager.java | 82 ++++++++++++++++++ .../cache/TransactionBoundCacheManager.java | 52 ++++++++++++ .../jpa/DiagnosticsEntityManager.java | 61 ++++++++++++++ .../jpa/StatementLoggingCustomizer.java | 37 ++++++++ .../StatementLoggingCustomizerCondition.java | 30 +++++++ .../ExtendedJpaTransactionManager.java | 22 ++++- .../TransactionLifecycleCallback.java | 28 +++++++ .../jobs/ScheduledJobRunnerConfig.java | 6 +- .../src/main/resources/application.properties | 2 + .../resources/application-test.properties | 2 + 17 files changed, 400 insertions(+), 82 deletions(-) rename fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/{ => cache}/CacheConfig.java (78%) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/SpecifiedCacheSupportingCacheManager.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/TransactionBoundCacheManager.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/DiagnosticsEntityManager.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/StatementLoggingCustomizer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/StatementLoggingCustomizerCondition.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/TransactionLifecycleCallback.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/CacheApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/CacheApiConstants.java index 24e2468ccc0..a56acf7868e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/CacheApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/CacheApiConstants.java @@ -25,6 +25,6 @@ private CacheApiConstants() { } public static final String RESOURCE_NAME = "CACHE"; - public static final String cacheTypeParameter = "cacheType"; + public static final String CACHE_TYPE_PARAMETER = "cacheType"; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/command/UpdateCacheCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/command/UpdateCacheCommandHandler.java index 26858cd7fa4..99d90afd2a3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/command/UpdateCacheCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/command/UpdateCacheCommandHandler.java @@ -48,7 +48,7 @@ public class UpdateCacheCommandHandler implements NewCommandSourceHandler { private final CacheWritePlatformService cacheService; - private static final Set REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(CacheApiConstants.cacheTypeParameter)); + private static final Set REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(CacheApiConstants.CACHE_TYPE_PARAMETER)); @Autowired public UpdateCacheCommandHandler(final CacheWritePlatformService cacheService) { @@ -72,8 +72,8 @@ public CommandProcessingResult processCommand(final JsonCommand command) { final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(CacheApiConstants.RESOURCE_NAME.toLowerCase()); - final int cacheTypeEnum = command.integerValueSansLocaleOfParameterNamed(CacheApiConstants.cacheTypeParameter); - baseDataValidator.reset().parameter(CacheApiConstants.cacheTypeParameter).value(Integer.valueOf(cacheTypeEnum)).notNull() + final int cacheTypeEnum = command.integerValueSansLocaleOfParameterNamed(CacheApiConstants.CACHE_TYPE_PARAMETER); + baseDataValidator.reset().parameter(CacheApiConstants.CACHE_TYPE_PARAMETER).value(Integer.valueOf(cacheTypeEnum)).notNull() .isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)); if (!dataValidationErrors.isEmpty()) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/service/RuntimeDelegatingCacheManager.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/service/RuntimeDelegatingCacheManager.java index 74225c10389..a15ae0130b2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/service/RuntimeDelegatingCacheManager.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/service/RuntimeDelegatingCacheManager.java @@ -22,17 +22,17 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.cache.CacheApiConstants; import org.apache.fineract.infrastructure.cache.CacheEnumerations; import org.apache.fineract.infrastructure.cache.data.CacheData; import org.apache.fineract.infrastructure.cache.domain.CacheType; import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.cache.jcache.JCacheCacheManager; import org.springframework.cache.support.NoOpCacheManager; import org.springframework.stereotype.Component; @@ -43,49 +43,43 @@ * database on startup and allow user to switch implementation through UI/API */ @Component(value = "runtimeDelegatingCacheManager") -public class RuntimeDelegatingCacheManager implements CacheManager { - - private static final Logger LOG = LoggerFactory.getLogger(RuntimeDelegatingCacheManager.class); - - private final CacheManager cacheManager; - private final CacheManager noOpCacheManager = new NoOpCacheManager(); +@RequiredArgsConstructor +@Slf4j +public class RuntimeDelegatingCacheManager implements CacheManager, InitializingBean { + + @Qualifier("ehCacheManager") + private final CacheManager ehCacheManager; + @Qualifier("defaultCacheManager") + private final CacheManager defaultCacheManager; private CacheManager currentCacheManager; - @Autowired - public RuntimeDelegatingCacheManager(final JCacheCacheManager cacheManager) { - this.cacheManager = cacheManager; - this.currentCacheManager = this.noOpCacheManager; + @Override + public void afterPropertiesSet() throws Exception { + currentCacheManager = defaultCacheManager; } @Override public Cache getCache(final String name) { - return this.currentCacheManager.getCache(name); + return currentCacheManager.getCache(name); } @Override public Collection getCacheNames() { - return this.currentCacheManager.getCacheNames(); + return currentCacheManager.getCacheNames(); } public Collection retrieveAll() { - final boolean noCacheEnabled = this.currentCacheManager instanceof NoOpCacheManager; - final boolean ehcacheEnabled = this.currentCacheManager instanceof JCacheCacheManager; - - // final boolean distributedCacheEnabled = false; + final boolean noCacheEnabled = currentCacheManager == defaultCacheManager; + final boolean ehCacheEnabled = currentCacheManager == ehCacheManager; final EnumOptionData noCacheType = CacheEnumerations.cacheType(CacheType.NO_CACHE); final EnumOptionData singleNodeCacheType = CacheEnumerations.cacheType(CacheType.SINGLE_NODE); - // final EnumOptionData multiNodeCacheType = - // CacheEnumerations.cacheType(CacheType.MULTI_NODE); final CacheData noCache = CacheData.instance(noCacheType, noCacheEnabled); - final CacheData singleNodeCache = CacheData.instance(singleNodeCacheType, ehcacheEnabled); - // final CacheData distributedCache = - // CacheData.instance(multiNodeCacheType, distributedCacheEnabled); + final CacheData singleNodeCache = CacheData.instance(singleNodeCacheType, ehCacheEnabled); - final Collection caches = Arrays.asList(noCache, singleNodeCache); - return caches; + return Arrays.asList(noCache, singleNodeCache); } public Map switchToCache(final boolean ehcacheEnabled, final CacheType toCacheType) { @@ -93,42 +87,38 @@ public Map switchToCache(final boolean ehcacheEnabled, final Cac final Map changes = new HashMap<>(); final boolean noCacheEnabled = !ehcacheEnabled; - final boolean distributedCacheEnabled = !ehcacheEnabled; switch (toCacheType) { - case INVALID: - break; - case NO_CACHE: + case INVALID -> { + log.warn("Invalid cache type used"); + } + case NO_CACHE -> { if (!noCacheEnabled) { - changes.put(CacheApiConstants.cacheTypeParameter, toCacheType.getValue()); + changes.put(CacheApiConstants.CACHE_TYPE_PARAMETER, toCacheType.getValue()); } - this.currentCacheManager = this.noOpCacheManager; - break; - case SINGLE_NODE: + currentCacheManager = defaultCacheManager; + } + case SINGLE_NODE -> { if (!ehcacheEnabled) { - changes.put(CacheApiConstants.cacheTypeParameter, toCacheType.getValue()); + changes.put(CacheApiConstants.CACHE_TYPE_PARAMETER, toCacheType.getValue()); clearEhCache(); } - this.currentCacheManager = this.cacheManager; + currentCacheManager = ehCacheManager; - if (this.currentCacheManager.getCacheNames().size() == 0) { - LOG.error("No caches configured for activated CacheManager {}", this.currentCacheManager); - } - break; - case MULTI_NODE: - if (!distributedCacheEnabled) { - changes.put(CacheApiConstants.cacheTypeParameter, toCacheType.getValue()); + if (currentCacheManager.getCacheNames().size() == 0) { + log.error("No caches configured for activated CacheManager {}", currentCacheManager); } - break; + } + case MULTI_NODE -> throw new UnsupportedOperationException("Multi node cache is not supported"); } return changes; } private void clearEhCache() { - Iterable cacheNames = cacheManager.getCacheNames(); + Iterable cacheNames = ehCacheManager.getCacheNames(); for (String cacheName : cacheNames) { - cacheManager.getCache(cacheName).clear(); + ehCacheManager.getCache(cacheName).clear(); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 5088b3c3828..a278c1c5ca2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -19,23 +19,22 @@ package org.apache.fineract.infrastructure.configuration.domain; import java.time.LocalDate; -import java.util.HashMap; -import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.cache.domain.CacheType; import org.apache.fineract.infrastructure.cache.domain.PlatformCache; import org.apache.fineract.infrastructure.cache.domain.PlatformCacheRepository; import org.apache.fineract.infrastructure.configuration.data.GlobalConfigurationPropertyData; -import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.useradministration.domain.Permission; import org.apache.fineract.useradministration.domain.PermissionRepository; import org.apache.fineract.useradministration.exception.PermissionNotFoundException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service +@RequiredArgsConstructor public class ConfigurationDomainServiceJpa implements ConfigurationDomainService { public static final String ENABLE_BUSINESS_DATE = "enable_business_date"; @@ -55,15 +54,6 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService private final PermissionRepository permissionRepository; private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; private final PlatformCacheRepository cacheTypeRepository; - private static Map configurations = new HashMap<>(); - - @Autowired - public ConfigurationDomainServiceJpa(final PermissionRepository permissionRepository, - final GlobalConfigurationRepositoryWrapper globalConfigurationRepository, final PlatformCacheRepository cacheTypeRepository) { - this.permissionRepository = permissionRepository; - this.globalConfigurationRepository = globalConfigurationRepository; - this.cacheTypeRepository = cacheTypeRepository; - } @Override public boolean isMakerCheckerEnabledForTask(final String taskPermissionCode) { @@ -321,9 +311,7 @@ public Long getDailyTPTLimit() { @Override public void removeGlobalConfigurationPropertyDataFromCache(final String propertyName) { - String identifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier(); - String key = identifier + "_" + propertyName; - configurations.remove(key); + globalConfigurationRepository.removeFromCache(propertyName); } @Override @@ -389,15 +377,8 @@ public Long retrieveRelaxingDaysConfigForPivotDate() { return property.getValue(); } - @Cacheable(value = "configByName", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#propertyName)") - public GlobalConfigurationPropertyData getGlobalConfigurationPropertyData(final String propertyName) { - String identifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier(); - String key = identifier + "_" + propertyName; - if (!configurations.containsKey(key)) { - GlobalConfigurationProperty configuration = this.globalConfigurationRepository.findOneByNameWithNotFoundDetection(propertyName); - configurations.put(key, configuration.toData()); - } - return configurations.get(key); + private GlobalConfigurationPropertyData getGlobalConfigurationPropertyData(final String propertyName) { + return globalConfigurationRepository.findOneByNameWithNotFoundDetection(propertyName).toData(); } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/GlobalConfigurationRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/GlobalConfigurationRepositoryWrapper.java index 7276bcd2276..3d91820f72d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/GlobalConfigurationRepositoryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/GlobalConfigurationRepositoryWrapper.java @@ -18,8 +18,11 @@ */ package org.apache.fineract.infrastructure.configuration.domain; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.configuration.exception.GlobalConfigurationPropertyNotFoundException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; /** @@ -28,6 +31,7 @@ *

*/ @Service +@Slf4j public class GlobalConfigurationRepositoryWrapper { private final GlobalConfigurationRepository repository; @@ -37,6 +41,7 @@ public GlobalConfigurationRepositoryWrapper(final GlobalConfigurationRepository this.repository = repository; } + @Cacheable(value = "configByName", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#propertyName)") public GlobalConfigurationProperty findOneByNameWithNotFoundDetection(final String propertyName) { final GlobalConfigurationProperty property = this.repository.findOneByName(propertyName); if (property == null) { @@ -61,4 +66,8 @@ public void delete(final GlobalConfigurationProperty globalConfigurationProperty this.repository.delete(globalConfigurationProperty); } + @CacheEvict(value = "configByName", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#propertyName)") + public void removeFromCache(String propertyName) { + log.debug("Cache entry evicted {}", propertyName); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java index c9c88fb6120..2e5fc9d2a55 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java @@ -53,6 +53,7 @@ public class FineractProperties { private FineractJobProperties job; private FineractTemplateProperties template; + private FineractJpaProperties jpa; @Getter @Setter @@ -248,4 +249,10 @@ public static class FineractTemplateProperties { private List regexWhitelist; } + @Getter + @Setter + public static class FineractJpaProperties { + + private boolean statementLoggingEnabled; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/CacheConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/CacheConfig.java similarity index 78% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/CacheConfig.java rename to fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/CacheConfig.java index e6117fa68c5..e4202ae3479 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/CacheConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/CacheConfig.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.fineract.infrastructure.core.config; +package org.apache.fineract.infrastructure.core.config.cache; import java.time.Duration; import javax.cache.CacheManager; @@ -28,20 +28,32 @@ import org.ehcache.config.builders.ResourcePoolsBuilder; import org.ehcache.jsr107.Eh107Configuration; import org.springframework.cache.jcache.JCacheCacheManager; +import org.springframework.cache.support.NoOpCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CacheConfig { + public static final String CONFIG_BY_NAME_CACHE_NAME = "configByName"; + + @Bean + public TransactionBoundCacheManager defaultCacheManager(JCacheCacheManager ehCacheManager) { + SpecifiedCacheSupportingCacheManager cacheManager = new SpecifiedCacheSupportingCacheManager(); + cacheManager.setNoOpCacheManager(new NoOpCacheManager()); + cacheManager.setDelegateCacheManager(ehCacheManager); + cacheManager.setSupportedCaches(CONFIG_BY_NAME_CACHE_NAME); + return new TransactionBoundCacheManager(cacheManager); + } + @Bean public JCacheCacheManager ehCacheManager() { JCacheCacheManager jCacheCacheManager = new JCacheCacheManager(); - jCacheCacheManager.setCacheManager(getCustomCacheManager()); + jCacheCacheManager.setCacheManager(getInternalEhCacheManager()); return jCacheCacheManager; } - private CacheManager getCustomCacheManager() { + private CacheManager getInternalEhCacheManager() { CachingProvider provider = Caching.getCachingProvider(); CacheManager cacheManager = provider.getCacheManager(); @@ -61,6 +73,7 @@ private CacheManager getCustomCacheManager() { cacheManager.createCache("codes", defaultTemplate); cacheManager.createCache("hooks", defaultTemplate); cacheManager.createCache("tfConfig", defaultTemplate); + cacheManager.createCache(CONFIG_BY_NAME_CACHE_NAME, defaultTemplate); javax.cache.configuration.Configuration accessTokenTemplate = Eh107Configuration.fromEhcacheCacheConfiguration( CacheConfigurationBuilder.newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(10000)) diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/SpecifiedCacheSupportingCacheManager.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/SpecifiedCacheSupportingCacheManager.java new file mode 100644 index 00000000000..2baf7c0b7ec --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/SpecifiedCacheSupportingCacheManager.java @@ -0,0 +1,82 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.config.cache; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.jcache.JCacheCacheManager; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.util.Assert; + +@RequiredArgsConstructor +public class SpecifiedCacheSupportingCacheManager implements CacheManager, InitializingBean { + + private JCacheCacheManager delegateCacheManager; + private NoOpCacheManager noOpCacheManager; + + private final Set supportedCacheNames = new LinkedHashSet<>(16); + + @Override + public void afterPropertiesSet() throws Exception { + Assert.notNull(delegateCacheManager, "cacheManager cannot be null"); + Assert.notNull(noOpCacheManager, "delegate cannot be null"); + Assert.notEmpty(supportedCacheNames, "supportedCacheNames must not be empty"); + delegateCacheManager.afterPropertiesSet(); + } + + @Override + public Cache getCache(String name) { + if (supportedCacheNames.contains(name)) { + Cache cache = delegateCacheManager.getCache(name); + if (cache != null) { + return cache; + } else { + return noOpCacheManager.getCache(name); + } + } else { + return noOpCacheManager.getCache(name); + } + } + + @Override + public Collection getCacheNames() { + synchronized (supportedCacheNames) { + return Collections.unmodifiableSet(supportedCacheNames); + } + } + + public void setDelegateCacheManager(JCacheCacheManager delegateCacheManager) { + this.delegateCacheManager = delegateCacheManager; + } + + public void setNoOpCacheManager(NoOpCacheManager noOpCacheManager) { + this.noOpCacheManager = noOpCacheManager; + } + + public void setSupportedCaches(String... cacheNames) { + supportedCacheNames.addAll(Arrays.asList(cacheNames)); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/TransactionBoundCacheManager.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/TransactionBoundCacheManager.java new file mode 100644 index 00000000000..5a70b4e1af9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/cache/TransactionBoundCacheManager.java @@ -0,0 +1,52 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.config.cache; + +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.persistence.TransactionLifecycleCallback; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +@RequiredArgsConstructor +public class TransactionBoundCacheManager implements TransactionLifecycleCallback, CacheManager { + + private final CacheManager delegate; + + @Override + public void afterCompletion() { + Collection cacheNames = delegate.getCacheNames(); + cacheNames.forEach(c -> { + Cache cache = delegate.getCache(c); + if (cache != null) { + cache.clear(); + } + }); + } + + @Override + public Cache getCache(String name) { + return delegate.getCache(name); + } + + @Override + public Collection getCacheNames() { + return delegate.getCacheNames(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/DiagnosticsEntityManager.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/DiagnosticsEntityManager.java new file mode 100644 index 00000000000..e99dca53d65 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/DiagnosticsEntityManager.java @@ -0,0 +1,61 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.diagnostics.jpa; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import javax.persistence.EntityManagerFactory; +import org.eclipse.persistence.internal.jpa.EntityManagerImpl; +import org.eclipse.persistence.sessions.changesets.UnitOfWorkChangeSet; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Profile; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.stereotype.Component; + +/** + * This class can be used for IntelliJ debugging purposes to access the current transaction bound EntityManager + * instance.
+ *
+ * With Alt + F8 you can run evaluations in IntelliJ and this class makes it easier to access the EntityManager and to + * see what kind of changes are pending within the Persistence Context.
+ *
+ * To enable this, run Fineract with the diagnostics profile. + */ +@Profile("diagnostics") +@Component +public class DiagnosticsEntityManager implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + public static EntityManagerImpl getCurrentEntityManager() { + EntityManagerFactory emf = applicationContext.getBean(EntityManagerFactory.class); + return (EntityManagerImpl) EntityManagerFactoryUtils.getTransactionalEntityManager(emf); + } + + public static UnitOfWorkChangeSet getCurrentChanges() { + return DiagnosticsEntityManager.getCurrentEntityManager().getUnitOfWork().getCurrentChanges(); + } + + @SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD") + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + DiagnosticsEntityManager.applicationContext = applicationContext; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/StatementLoggingCustomizer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/StatementLoggingCustomizer.java new file mode 100644 index 00000000000..ca35f7ae3e0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/StatementLoggingCustomizer.java @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.diagnostics.jpa; + +import java.util.Map; +import org.apache.fineract.infrastructure.core.config.jpa.EntityManagerFactoryCustomizer; +import org.eclipse.persistence.config.PersistenceUnitProperties; +import org.eclipse.persistence.logging.SessionLog; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Component; + +@Component +@Conditional(StatementLoggingCustomizerCondition.class) +public class StatementLoggingCustomizer implements EntityManagerFactoryCustomizer { + + @Override + public Map additionalVendorProperties() { + return Map.of("eclipselink.logging.level.sql", SessionLog.FINE_LABEL, PersistenceUnitProperties.LOGGING_PARAMETERS, + Boolean.TRUE.toString()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/StatementLoggingCustomizerCondition.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/StatementLoggingCustomizerCondition.java new file mode 100644 index 00000000000..e4514f1e0c6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/diagnostics/jpa/StatementLoggingCustomizerCondition.java @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.diagnostics.jpa; + +import org.apache.fineract.infrastructure.core.condition.PropertiesCondition; +import org.apache.fineract.infrastructure.core.config.FineractProperties; + +public class StatementLoggingCustomizerCondition extends PropertiesCondition { + + @Override + protected boolean matches(FineractProperties properties) { + return properties.getJpa().isStatementLoggingEnabled(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java index 1355da65f9c..233fc73bb2e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.infrastructure.core.persistence; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; import javax.persistence.EntityManager; import javax.persistence.FlushModeType; import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport; @@ -29,6 +32,8 @@ public class ExtendedJpaTransactionManager extends JpaTransactionManager { + private final List lifecycleCallbacks = new CopyOnWriteArrayList<>(); + public ExtendedJpaTransactionManager() { setValidateExistingTransaction(true); } @@ -42,7 +47,7 @@ protected void doBegin(Object transaction, TransactionDefinition definition) { entityManager.setFlushMode(FlushModeType.COMMIT); } } - + invokeLifecycleCallbacks(TransactionLifecycleCallback::afterBegin); } @Override @@ -54,6 +59,13 @@ protected void doCommit(DefaultTransactionStatus status) { } } super.doCommit(status); + invokeLifecycleCallbacks(TransactionLifecycleCallback::afterCommit); + } + + @Override + protected void doCleanupAfterCompletion(Object transaction) { + super.doCleanupAfterCompletion(transaction); + invokeLifecycleCallbacks(TransactionLifecycleCallback::afterCompletion); } private boolean isReadOnlyTx(Object transaction) { @@ -68,4 +80,12 @@ private EntityManager getCurrentEntityManager() { } return null; } + + private void invokeLifecycleCallbacks(Consumer f) { + lifecycleCallbacks.forEach(f::accept); + } + + public void setLifecycleCallbacks(List lifecycleCallbacks) { + this.lifecycleCallbacks.addAll(lifecycleCallbacks); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/TransactionLifecycleCallback.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/TransactionLifecycleCallback.java new file mode 100644 index 00000000000..fa33ec8a9f1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/TransactionLifecycleCallback.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.persistence; + +public interface TransactionLifecycleCallback { + + default void afterBegin() {} + + default void afterCommit() {} + + default void afterCompletion() {} +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/ScheduledJobRunnerConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/ScheduledJobRunnerConfig.java index 7c560535d6b..4b1c74a0b8a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/ScheduledJobRunnerConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/ScheduledJobRunnerConfig.java @@ -18,7 +18,9 @@ */ package org.apache.fineract.infrastructure.jobs; +import java.util.List; import org.apache.fineract.infrastructure.core.persistence.ExtendedJpaTransactionManager; +import org.apache.fineract.infrastructure.core.persistence.TransactionLifecycleCallback; import org.apache.fineract.infrastructure.core.service.database.RoutingDataSource; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.annotation.BatchConfigurer; @@ -39,8 +41,10 @@ public class ScheduledJobRunnerConfig { @Bean - public PlatformTransactionManager transactionManager(ObjectProvider transactionManagerCustomizers) { + public PlatformTransactionManager transactionManager(ObjectProvider transactionManagerCustomizers, + List callbacks) { ExtendedJpaTransactionManager transactionManager = new ExtendedJpaTransactionManager(); + transactionManager.setLifecycleCallbacks(callbacks); transactionManager.setValidateExistingTransaction(true); transactionManagerCustomizers.ifAvailable(customizers -> customizers.customize(transactionManager)); return transactionManager; diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 11677c9dfeb..f1b158ea348 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -96,6 +96,8 @@ fineract.template.regex-whitelist=${FINERACT_TEMPLATE_REGEX_WHITELIST:} fineract.report.export.s3.bucket=${FINERACT_REPORT_EXPORT_S3_BUCKET_NAME:} fineract.report.export.s3.enabled=${FINERACT_REPORT_EXPORT_S3_ENABLED:false} +fineract.jpa.statementLoggingEnabled=${FINERACT_STATEMENT_LOGGING_ENABLED:false} + # Logging pattern for the console logging.pattern.console=${CONSOLE_LOG_PATTERN:%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(%replace([%X{correlationId}]){'\\[\\]', ''}) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}} diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties index f33d80beadf..459034b08a3 100644 --- a/fineract-provider/src/test/resources/application-test.properties +++ b/fineract-provider/src/test/resources/application-test.properties @@ -75,6 +75,8 @@ fineract.content.s3.secretKey= fineract.report.export.s3.bucket=${FINERACT_REPORT_EXPORT_S3_BUCKET_NAME:} fineract.report.export.s3.enabled=${FINERACT_REPORT_EXPORT_S3_ENABLED:false} +fineract.jpa.statementLoggingEnabled=${FINERACT_STATEMENT_LOGGING_ENABLED:false} + management.health.jms.enabled=false # FINERACT 1296