FINERACT-2389: Fix for rounding mode not being initialized whenever a new tenant is added to the system without a restart
This commit is contained in:
committed by
Arnold Gálovics
parent
63c220e5d2
commit
5079203d57
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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.configuration.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.apache.fineract.infrastructure.configuration.data.GlobalConfigurationPropertyData;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
|
||||
@Entity
|
||||
@Table(name = "c_configuration")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
public class GlobalConfigurationProperty extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@Column(name = "name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "enabled", nullable = false)
|
||||
private boolean enabled;
|
||||
|
||||
@Column(name = "value", nullable = true)
|
||||
private Long value;
|
||||
|
||||
@Column(name = "date_value", nullable = true)
|
||||
private LocalDate dateValue;
|
||||
|
||||
@Column(name = "string_value", nullable = true)
|
||||
private String stringValue;
|
||||
|
||||
@Column(name = "description", nullable = true)
|
||||
private String description;
|
||||
|
||||
@Column(name = "is_trap_door", nullable = false)
|
||||
private boolean isTrapDoor;
|
||||
|
||||
public static GlobalConfigurationProperty newSurveyConfiguration(final String name) {
|
||||
return new GlobalConfigurationProperty().setName(name);
|
||||
}
|
||||
|
||||
public GlobalConfigurationPropertyData toData() {
|
||||
return new GlobalConfigurationPropertyData().setName(getName()).setEnabled(isEnabled()).setValue(getValue())
|
||||
.setDateValue(getDateValue()).setStringValue(getStringValue()).setId(this.getId()).setDescription(this.description)
|
||||
.setTrapDoor(this.isTrapDoor);
|
||||
}
|
||||
}
|
||||
+28
@@ -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.configuration.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface GlobalConfigurationRepository
|
||||
extends JpaRepository<GlobalConfigurationProperty, Long>, JpaSpecificationExecutor<GlobalConfigurationProperty> {
|
||||
|
||||
GlobalConfigurationProperty findOneByName(String name);
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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.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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Wrapper for {@link GlobalConfigurationRepository} that adds NULL checking and Error handling capabilities
|
||||
* </p>
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class GlobalConfigurationRepositoryWrapper {
|
||||
|
||||
private final GlobalConfigurationRepository repository;
|
||||
|
||||
@Autowired
|
||||
public GlobalConfigurationRepositoryWrapper(final GlobalConfigurationRepository repository) {
|
||||
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) {
|
||||
throw new GlobalConfigurationPropertyNotFoundException(propertyName);
|
||||
}
|
||||
return property;
|
||||
}
|
||||
|
||||
public GlobalConfigurationProperty findOneWithNotFoundDetection(final Long configId) {
|
||||
return this.repository.findById(configId).orElseThrow(() -> new GlobalConfigurationPropertyNotFoundException(configId));
|
||||
}
|
||||
|
||||
public void save(final GlobalConfigurationProperty globalConfigurationProperty) {
|
||||
this.repository.save(globalConfigurationProperty);
|
||||
}
|
||||
|
||||
public void saveAndFlush(final GlobalConfigurationProperty globalConfigurationProperty) {
|
||||
this.repository.saveAndFlush(globalConfigurationProperty);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.configuration.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
/**
|
||||
* A {@link RuntimeException} thrown when global configuration properties are not found.
|
||||
*/
|
||||
public class GlobalConfigurationPropertyNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public GlobalConfigurationPropertyNotFoundException(final String propertyName) {
|
||||
super("error.msg.configuration.property.invalid", "Configuration property `" + propertyName + "` does not exist", propertyName);
|
||||
}
|
||||
|
||||
public GlobalConfigurationPropertyNotFoundException(final Long configId) {
|
||||
super("error.msg.configuration.id.invalid", "Configuration identifier `" + configId + "` does not exist", configId);
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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.configuration.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationProperty;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper;
|
||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Service to initialize MoneyHelper configurations for multi-tenant environments. This service bridges the gap between
|
||||
* global configuration and MoneyHelper's tenant-specific caching.
|
||||
*
|
||||
* Note: MoneyHelper rounding mode is immutable once initialized to maintain data integrity. Updates require application
|
||||
* restart to take effect.
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class MoneyHelperInitializationService {
|
||||
|
||||
// TODO: this is preventing the circular dependency...
|
||||
@Lazy
|
||||
@Autowired
|
||||
private GlobalConfigurationRepositoryWrapper globalConfigurationRepository;
|
||||
|
||||
/**
|
||||
* Initialize MoneyHelper for a specific tenant. This method should be called during tenant setup and whenever
|
||||
* rounding mode configuration changes.
|
||||
*
|
||||
* @param tenant
|
||||
* the tenant to initialize
|
||||
*/
|
||||
public void initializeTenantRoundingMode(FineractPlatformTenant tenant) {
|
||||
if (tenant == null) {
|
||||
throw new IllegalArgumentException("Tenant cannot be null");
|
||||
}
|
||||
|
||||
String tenantIdentifier = tenant.getTenantIdentifier();
|
||||
|
||||
FineractPlatformTenant originalTenant = ThreadLocalContextUtil.getTenant();
|
||||
try {
|
||||
// Set tenant context to read configuration
|
||||
ThreadLocalContextUtil.setTenant(tenant);
|
||||
|
||||
// Get rounding mode from configuration with fallback to default
|
||||
int roundingModeValue = getRoundingModeFromConfiguration();
|
||||
|
||||
// Initialize MoneyHelper for this tenant
|
||||
MoneyHelper.initializeTenantRoundingMode(tenantIdentifier, roundingModeValue);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to initialize MoneyHelper for tenant '{}'", tenantIdentifier, e);
|
||||
throw new RuntimeException("Failed to initialize MoneyHelper for tenant: " + tenantIdentifier, e);
|
||||
} finally {
|
||||
ThreadLocalContextUtil.setTenant(originalTenant);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if MoneyHelper is initialized for a tenant.
|
||||
*
|
||||
* @param tenantIdentifier
|
||||
* the tenant identifier
|
||||
* @return true if initialized, false otherwise
|
||||
*/
|
||||
public boolean isTenantInitialized(String tenantIdentifier) {
|
||||
return MoneyHelper.isTenantInitialized(tenantIdentifier);
|
||||
}
|
||||
|
||||
public boolean isTenantInitialized(FineractPlatformTenant tenant) {
|
||||
return isTenantInitialized(tenant.getTenantIdentifier());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rounding mode from configuration with fallback to default.
|
||||
*
|
||||
* @return the rounding mode value
|
||||
*/
|
||||
private int getRoundingModeFromConfiguration() {
|
||||
GlobalConfigurationProperty roundingModeProperty = globalConfigurationRepository
|
||||
.findOneByNameWithNotFoundDetection(GlobalConfigurationConstants.ROUNDING_MODE);
|
||||
return roundingModeProperty.getValue().intValue();
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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.configuration.service;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||
import org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Service to initialize MoneyHelper for all tenants during application startup. This service runs after the application
|
||||
* is fully started to ensure all database migrations and tenant configurations are complete.
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@DependsOn({ "tenantDetailsService", "moneyHelperInitializationService", "tenantDatabaseUpgradeService" })
|
||||
public class MoneyHelperStartupInitializationService implements InitializingBean {
|
||||
|
||||
private final TenantDetailsService tenantDetailsService;
|
||||
private final MoneyHelperInitializationService moneyHelperInitializationService;
|
||||
|
||||
/**
|
||||
* Initialize MoneyHelper for all tenants after the application is ready. This method runs after
|
||||
* ApplicationReadyEvent to ensure all database migrations and tenant configurations are complete.
|
||||
*
|
||||
* If it fails (for any reason), it will fail the application startup!
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
log.info("Starting MoneyHelper initialization for all tenants...");
|
||||
|
||||
List<FineractPlatformTenant> tenants = tenantDetailsService.findAllTenants();
|
||||
if (tenants.isEmpty()) {
|
||||
log.warn("No tenants found during MoneyHelper initialization");
|
||||
return;
|
||||
}
|
||||
|
||||
for (FineractPlatformTenant tenant : tenants) {
|
||||
String tenantIdentifier = tenant.getTenantIdentifier();
|
||||
// Check if already initialized (in case of restart scenarios)
|
||||
if (moneyHelperInitializationService.isTenantInitialized(tenantIdentifier)) {
|
||||
log.debug("MoneyHelper already initialized for tenant: {}", tenantIdentifier);
|
||||
continue;
|
||||
}
|
||||
// Initialize MoneyHelper for this tenant
|
||||
moneyHelperInitializationService.initializeTenantRoundingMode(tenant);
|
||||
}
|
||||
log.info("MoneyHelper initialization completed");
|
||||
}
|
||||
}
|
||||
+22
@@ -18,14 +18,17 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.core.service.database;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import javax.sql.DataSource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.configuration.service.MoneyHelperInitializationService;
|
||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
@@ -53,6 +56,9 @@ public class TomcatJdbcDataSourcePerTenantService implements RoutingDataSourceSe
|
||||
|
||||
private final DataSourcePerTenantServiceFactory dataSourcePerTenantServiceFactory;
|
||||
|
||||
private final MoneyHelperInitializationService moneyHelperInitializationService;
|
||||
private final Set<Long> tenantMoneyInitializingSet = Sets.newConcurrentHashSet();
|
||||
|
||||
@Override
|
||||
public DataSource retrieveDataSource() {
|
||||
// default to tenant database datasource
|
||||
@@ -66,7 +72,23 @@ public class TomcatJdbcDataSourcePerTenantService implements RoutingDataSourceSe
|
||||
// appropriate datasource for that tenant.
|
||||
actualDataSource = TENANT_TO_DATA_SOURCE_MAP.computeIfAbsent(tenantConnectionKey,
|
||||
(key) -> dataSourcePerTenantServiceFactory.createNewDataSourceFor(tenant, tenantConnection));
|
||||
}
|
||||
|
||||
// TODO: This is definitely not the optimal place to initialize the rounding modes
|
||||
// Preferably nothing should use a statically referenced context and the initialization
|
||||
// should happen within the rounding mode retrieval
|
||||
if (tenant != null) {
|
||||
Long connectionId = tenant.getConnection().getConnectionId();
|
||||
if (!tenantMoneyInitializingSet.contains(connectionId) && !moneyHelperInitializationService.isTenantInitialized(tenant)) {
|
||||
// Double check to prevent visibility and race-condition issues
|
||||
synchronized (tenantMoneyInitializingSet) {
|
||||
if (!tenantMoneyInitializingSet.contains(connectionId)) {
|
||||
tenantMoneyInitializingSet.add(connectionId);
|
||||
moneyHelperInitializationService.initializeTenantRoundingMode(tenant);
|
||||
tenantMoneyInitializingSet.remove(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actualDataSource;
|
||||
|
||||
Reference in New Issue
Block a user