Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
63a005d
Merge branch 'release' of github.com:appsmithorg/appsmith into release
NilanshBansal May 6, 2024
60dd0f9
Merge branch 'release' of github.com:appsmithorg/appsmith into release
NilanshBansal May 8, 2024
9b144b6
initial setup
NilanshBansal May 9, 2024
9f37234
feat: added tenant configuration to redis cache (#33083)
NilanshBansal May 9, 2024
e889d83
code optimised to replace usage of tenantRepository to cached function
NilanshBansal May 9, 2024
b2c301e
fixed cyclic dependency
NilanshBansal May 9, 2024
883b05a
Update app/server/appsmith-server/src/main/java/com/appsmith/server/s…
NilanshBansal May 10, 2024
064d0b1
cache evicted after db update to avoid inconsistency
NilanshBansal May 10, 2024
0702551
Merge remote-tracking branch 'origin/feature/issue-33083/tenant-confi…
NilanshBansal May 10, 2024
9543f94
fixed tests
NilanshBansal May 13, 2024
958f5e2
added implements serializable for tenant configuration
NilanshBansal May 13, 2024
d469047
addressed review comments and other optimisations
NilanshBansal May 13, 2024
2d1115d
updated cacheName and added notDeleted criteria
NilanshBansal May 14, 2024
bdae9ad
Merge branch 'release' of github.com:appsmithorg/appsmith into featur…
NilanshBansal May 14, 2024
4d1deed
cache evicted from update func
NilanshBansal May 14, 2024
da5af58
moved cache invalidation to repository layer
NilanshBansal May 14, 2024
1910c61
Revert "moved cache invalidation to repository layer"
NilanshBansal May 14, 2024
371f2d1
optimisations
NilanshBansal May 14, 2024
bb21eae
code rabbit suggestion
NilanshBansal May 14, 2024
306e77f
Merge branch 'release' of github.com:appsmithorg/appsmith into featur…
NilanshBansal May 14, 2024
d11d404
fix: handled cache deserialization errors (#33504)
NilanshBansal May 22, 2024
0d22cd0
added test for checking deserialization errors
NilanshBansal May 23, 2024
c0b0cf1
added comments
NilanshBansal May 24, 2024
a1da5d6
initial setup
NilanshBansal May 9, 2024
2508b86
feat: added tenant configuration to redis cache (#33083)
NilanshBansal May 9, 2024
140ee6a
code optimised to replace usage of tenantRepository to cached function
NilanshBansal May 9, 2024
6cf7f0e
fixed cyclic dependency
NilanshBansal May 9, 2024
446dfa2
cache evicted after db update to avoid inconsistency
NilanshBansal May 10, 2024
d3ec0d2
Update app/server/appsmith-server/src/main/java/com/appsmith/server/s…
NilanshBansal May 10, 2024
aeafeb7
fixed tests
NilanshBansal May 13, 2024
55e70ac
added implements serializable for tenant configuration
NilanshBansal May 13, 2024
1fa0df1
addressed review comments and other optimisations
NilanshBansal May 13, 2024
fb73a78
updated cacheName and added notDeleted criteria
NilanshBansal May 14, 2024
a13045f
cache evicted from update func
NilanshBansal May 14, 2024
cc9456c
moved cache invalidation to repository layer
NilanshBansal May 14, 2024
169b126
Revert "moved cache invalidation to repository layer"
NilanshBansal May 14, 2024
4c444f3
optimisations
NilanshBansal May 14, 2024
c76bb19
code rabbit suggestion
NilanshBansal May 14, 2024
e247b85
fix: handled cache deserialization errors (#33504)
NilanshBansal May 22, 2024
bdd8f13
added test for checking deserialization errors
NilanshBansal May 23, 2024
15f54d6
added comments
NilanshBansal May 24, 2024
e3718cd
Merge branch 'feature/issue-33083/tenant-config-to-redis' of github.c…
NilanshBansal May 27, 2024
7b4bf8d
removed cache eviction after each test
NilanshBansal May 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;

import java.io.Serializable;

@Getter
@Setter
@ToString
@NoArgsConstructor
@Document
@FieldNameConstants
public class Tenant extends BaseDomain {
public class Tenant extends BaseDomain implements Serializable {

@Unique String slug;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
import lombok.Data;
import org.apache.commons.lang3.ObjectUtils;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Data
public class TenantConfigurationCE {
public class TenantConfigurationCE implements Serializable {

private String googleMapsKey;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.appsmith.server.repositories.ce;

import com.appsmith.server.domains.Tenant;
import com.appsmith.server.domains.User;
import reactor.core.publisher.Mono;

Expand All @@ -18,4 +19,8 @@ public interface CacheableRepositoryHelperCE {
Mono<String> getDefaultTenantId();

Mono<String> getInstanceAdminPermissionGroupId();

Mono<Tenant> fetchDefaultTenant(String tenantId);

Mono<Void> evictCachedTenant(String tenantId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.appsmith.server.domains.Config;
import com.appsmith.server.domains.PermissionGroup;
import com.appsmith.server.domains.Tenant;
import com.appsmith.server.domains.TenantConfiguration;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.exceptions.AppsmithError;
Expand Down Expand Up @@ -166,4 +167,33 @@ public Mono<String> getInstanceAdminPermissionGroupId() {
.doOnSuccess(permissionGroupId ->
inMemoryCacheableRepositoryHelper.setInstanceAdminPermissionGroupId(permissionGroupId));
}

/**
* Returns the default tenant from the cache if present.
* If not present in cache, then it fetches the default tenant from the database and adds to redis.
* @param tenantId
* @return
*/
@Cache(cacheName = "tenant", key = "{#tenantId}")
@Override
public Mono<Tenant> fetchDefaultTenant(String tenantId) {
BridgeQuery<Tenant> defaultTenantCriteria = Bridge.equal(Tenant.Fields.slug, FieldName.DEFAULT);
BridgeQuery<Tenant> notDeletedCriteria = notDeleted();
BridgeQuery<Tenant> andCriteria = Bridge.and(defaultTenantCriteria, notDeletedCriteria);
Query query = new Query();
query.addCriteria(andCriteria);

return mongoOperations.findOne(query, Tenant.class).map(tenant -> {
if (tenant.getTenantConfiguration() == null) {
tenant.setTenantConfiguration(new TenantConfiguration());
}
return tenant;
});
}

@CacheEvict(cacheName = "tenant", key = "{#tenantId}")
@Override
public Mono<Void> evictCachedTenant(String tenantId) {
return Mono.empty().then();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import reactor.core.publisher.Mono;

public interface TenantRepositoryCE extends BaseRepository<Tenant, String>, CustomTenantRepositoryCE {

// Use tenantService.getDefaultTenant() instead of this method as it is cached to redis.
@Deprecated(forRemoval = true)
Mono<Tenant> findBySlug(String slug);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.appsmith.server.services;

import com.appsmith.server.helpers.FeatureFlagMigrationHelper;
import com.appsmith.server.repositories.CacheableRepositoryHelper;
import com.appsmith.server.repositories.TenantRepository;
import com.appsmith.server.services.ce.TenantServiceCEImpl;
import com.appsmith.server.solutions.EnvManager;
Expand All @@ -19,7 +20,15 @@ public TenantServiceImpl(
AnalyticsService analyticsService,
ConfigService configService,
@Lazy EnvManager envManager,
FeatureFlagMigrationHelper featureFlagMigrationHelper) {
super(validator, repository, analyticsService, configService, envManager, featureFlagMigrationHelper);
FeatureFlagMigrationHelper featureFlagMigrationHelper,
CacheableRepositoryHelper cacheableRepositoryHelper) {
super(
validator,
repository,
analyticsService,
configService,
envManager,
featureFlagMigrationHelper,
cacheableRepositoryHelper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
@Slf4j
@RequiredArgsConstructor
public class CacheableFeatureFlagHelperCEImpl implements CacheableFeatureFlagHelperCE {

private final TenantRepository tenantRepository;
private final ConfigService configService;
private final CloudServicesConfig cloudServicesConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.CollectionUtils;
import com.appsmith.server.helpers.FeatureFlagMigrationHelper;
import com.appsmith.server.repositories.CacheableRepositoryHelper;
import com.appsmith.server.repositories.TenantRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.BaseService;
Expand Down Expand Up @@ -39,17 +40,21 @@ public class TenantServiceCEImpl extends BaseService<TenantRepository, Tenant, S

private final FeatureFlagMigrationHelper featureFlagMigrationHelper;

private final CacheableRepositoryHelper cacheableRepositoryHelper;

public TenantServiceCEImpl(
Validator validator,
TenantRepository repository,
AnalyticsService analyticsService,
ConfigService configService,
@Lazy EnvManager envManager,
FeatureFlagMigrationHelper featureFlagMigrationHelper) {
FeatureFlagMigrationHelper featureFlagMigrationHelper,
CacheableRepositoryHelper cacheableRepositoryHelper) {
super(validator, repository, analyticsService);
this.configService = configService;
this.envManager = envManager;
this.featureFlagMigrationHelper = featureFlagMigrationHelper;
this.cacheableRepositoryHelper = cacheableRepositoryHelper;
}

@Override
Expand All @@ -59,7 +64,6 @@ public Mono<String> getDefaultTenantId() {
if (StringUtils.hasLength(tenantId)) {
return Mono.just(tenantId);
}

return repository.findBySlug(FieldName.DEFAULT).map(Tenant::getId).map(tenantId -> {
// Set the cache value before returning.
this.tenantId = tenantId;
Expand All @@ -69,6 +73,7 @@ public Mono<String> getDefaultTenantId() {

@Override
public Mono<Tenant> updateTenantConfiguration(String tenantId, TenantConfiguration tenantConfiguration) {
Mono<Void> evictTenantCache = cacheableRepositoryHelper.evictCachedTenant(tenantId);
return repository
.findById(tenantId, MANAGE_TENANT)
.switchIfEmpty(Mono.error(
Expand All @@ -89,14 +94,23 @@ public Mono<Tenant> updateTenantConfiguration(String tenantId, TenantConfigurati
return Mono.empty();
});
}

return envMono.then(Mono.zip(Mono.just(oldtenantConfiguration), Mono.just(tenant)));
})
.flatMap(tuple2 -> {
Tenant tenant = tuple2.getT2();
TenantConfiguration oldConfig = tuple2.getT1();
AppsmithBeanUtils.copyNestedNonNullProperties(tenantConfiguration, oldConfig);
tenant.setTenantConfiguration(oldConfig);
return repository.updateById(tenantId, tenant, MANAGE_TENANT);
Mono<Tenant> updatedTenantMono = repository
.updateById(tenantId, tenant, MANAGE_TENANT)
.cache();
// Firstly updating the Tenant object in the database and then evicting the cache.
// returning the updatedTenant, notice the updatedTenantMono is cached using .cache()
// hence it will not be evaluated again
return updatedTenantMono
.then(Mono.defer(() -> evictTenantCache))
.then(updatedTenantMono);
});
}

Expand Down Expand Up @@ -151,17 +165,29 @@ public Mono<Tenant> getTenantConfiguration() {

@Override
public Mono<Tenant> getDefaultTenant() {
// Get the default tenant object from the DB and then populate the relevant user permissions in that
// We are doing this differently because `findBySlug` is a Mongo JPA query and not a custom Appsmith query
return repository
.findBySlug(FieldName.DEFAULT)
.map(tenant -> {
if (tenant.getTenantConfiguration() == null) {
tenant.setTenantConfiguration(new TenantConfiguration());
}
return tenant;
})
.flatMap(tenant -> repository.setUserPermissionsInObject(tenant).switchIfEmpty(Mono.just(tenant)));
// Fetching Tenant from redis cache
return getDefaultTenantId()
.flatMap(tenantId -> cacheableRepositoryHelper.fetchDefaultTenant(tenantId))
.flatMap(tenant -> repository.setUserPermissionsInObject(tenant).switchIfEmpty(Mono.just(tenant)))
.onErrorResume(e -> {
log.error("Error fetching default tenant from redis!", e);
// If there is an error fetching the tenant from the cache, then evict the cache and fetching from
// the db. This handles the case for deserialization errors. This prevents the entire instance to
// go down if tenant cache is corrupted.
// More info - https://github.com/appsmithorg/appsmith/issues/33504
log.info("Evicting the default tenant from cache and fetching from the database!");
return cacheableRepositoryHelper
.evictCachedTenant(tenantId)
.then(repository.findBySlug(FieldName.DEFAULT).map(tenant -> {
if (tenant.getTenantConfiguration() == null) {
tenant.setTenantConfiguration(new TenantConfiguration());
}
return tenant;
}))
.flatMap(tenant -> repository
.setUserPermissionsInObject(tenant)
.switchIfEmpty(Mono.just(tenant)));
});
}

@Override
Expand Down Expand Up @@ -192,9 +218,12 @@ protected Mono<Tenant> getClientPertinentTenant(Tenant dbTenant, Tenant clientTe
return Mono.just(clientTenant);
}

// This function is used to save the tenant object in the database and evict the cache
@Override
public Mono<Tenant> save(Tenant tenant) {
return repository.save(tenant);
Mono<Void> evictTenantCache = cacheableRepositoryHelper.evictCachedTenant(tenantId);
Mono<Tenant> savedTenantMono = repository.save(tenant).cache();
return savedTenantMono.then(Mono.defer(() -> evictTenantCache)).then(savedTenantMono);
}

/**
Expand Down Expand Up @@ -242,6 +271,19 @@ public Mono<Tenant> retrieveById(String id) {
return repository.findById(id);
}

/**
* This function updates the tenant object in the database and evicts the cache
* @param tenantId
* @param tenant
* @return
*/
@Override
public Mono<Tenant> update(String tenantId, Tenant tenant) {
Mono<Void> evictTenantCache = cacheableRepositoryHelper.evictCachedTenant(tenantId);
Mono<Tenant> updatedTenantMono = super.update(tenantId, tenant).cache();
return updatedTenantMono.then(Mono.defer(() -> evictTenantCache)).then(updatedTenantMono);
}

/**
* This function checks if the tenant needs to be restarted and restarts after the feature flag migrations are
* executed.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.appsmith.server.services.ce;

import com.appsmith.caching.components.CacheManager;
import com.appsmith.external.enums.FeatureFlagEnum;
import com.appsmith.server.constants.FeatureMigrationType;
import com.appsmith.server.constants.LicensePlan;
Expand All @@ -9,18 +10,22 @@
import com.appsmith.server.helpers.FeatureFlagMigrationHelper;
import com.appsmith.server.helpers.UserUtils;
import com.appsmith.server.helpers.ce.bridge.Bridge;
import com.appsmith.server.repositories.CacheableRepositoryHelper;
import com.appsmith.server.repositories.TenantRepository;
import com.appsmith.server.repositories.UserRepository;
import com.appsmith.server.services.TenantService;
import com.appsmith.server.solutions.EnvManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import reactor.core.publisher.Mono;
Expand All @@ -42,6 +47,8 @@
import static java.lang.Boolean.TRUE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;

@SpringBootTest
@ExtendWith(SpringExtension.class)
Expand All @@ -62,6 +69,15 @@ class TenantServiceCETest {
@Autowired
UserUtils userUtils;

@Autowired
ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;

@SpyBean
CacheManager cacheManager;

@SpyBean
CacheableRepositoryHelper cacheableRepositoryHelper;

@MockBean
FeatureFlagMigrationHelper featureFlagMigrationHelper;

Expand All @@ -84,16 +100,19 @@ public void setup() throws IOException {
.findByEmail("api_user")
.flatMap(user -> userUtils.makeSuperUser(List.of(user)))
.block();
doReturn(Mono.empty()).when(cacheManager).get(anyString(), anyString());
}

@AfterEach
public void cleanup() {
Tenant updatedTenant = new Tenant();
updatedTenant.setTenantConfiguration(originalTenantConfiguration);
tenantService
.getDefaultTenant()
.flatMap(tenant -> tenantRepository.updateAndReturn(
tenant.getId(),
Bridge.update().set(Tenant.Fields.tenantConfiguration, originalTenantConfiguration),
Optional.empty()))
.getDefaultTenantId()
.flatMap(tenantId -> tenantService.update(tenantId, updatedTenant))
.doOnError(error -> {
System.err.println("Error during cleanup: " + error.getMessage());
})
.block();
}

Expand Down Expand Up @@ -362,4 +381,21 @@ void updateTenantConfiguration_updateStrongPasswordPolicy_success() {
})
.verifyComplete();
}

/**
* This test checks that the tenant cache is created and data is fetched without any deserialization errors
* This will ensure if any new nested user-defined classes are created in the tenant object in the future, and
* implements serializable is missed for that class, the deserialization will fail leads this test to fail.
*/
@Test
@WithUserDetails(value = "api_user")
public void testDeserializationErrors() {
String tenantId = tenantService.getDefaultTenantId().block();
Mono<Void> evictTenantCache = cacheableRepositoryHelper.evictCachedTenant(tenantId);
Mono<Boolean> hasKeyMono = reactiveRedisTemplate.hasKey("tenant:" + tenantId);
Mono<Tenant> cachedTenant = tenantService.getDefaultTenant();
StepVerifier.create(evictTenantCache.then(cachedTenant).then(hasKeyMono))
.assertNext(Assertions::assertTrue)
.verifyComplete();
}
}