fix: Concurrent modification issue with pending migrations feature flags (#27926)

## Description
In this code change, a separate loop has been introduced to handle the
removal of specific flags from a map to avoid encountering a
`ConcurrentModificationException` that can occur when attempting to
remove an entry from a map while iterating over it directly. This new
loop iterates through a collection of `featureFlagsToBeRemoved`,
removing each corresponding flag from the mentioned maps, ensuring safe
removal without disrupting the ongoing iteration process.

#### PR fixes following issue(s)
Fixes https://github.com/appsmithorg/appsmith-ee/issues/2608

#### Type of change
- Bug fix (non-breaking change which fixes an issue)

## Testing
>
#### How Has This Been Tested?
- [x] Manual
- [x] JUnit
- [ ] Jest
- [ ] Cypress
>
>

## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
Abhijeet 2023-10-11 12:57:13 +05:30 committed by GitHub
parent f5a0e41f60
commit cb3cca0a85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 61 additions and 4 deletions

View File

@ -14,7 +14,9 @@ import reactor.core.publisher.Mono;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static com.appsmith.server.constants.FeatureMigrationType.DISABLE; import static com.appsmith.server.constants.FeatureMigrationType.DISABLE;
@ -136,6 +138,9 @@ public class FeatureFlagMigrationHelperCEImpl implements FeatureFlagMigrationHel
} catch (Exception e) { } catch (Exception e) {
// Ignore IllegalArgumentException as all the feature flags are not added on // Ignore IllegalArgumentException as all the feature flags are not added on
// server side // server side
if (!(e instanceof IllegalArgumentException)) {
log.error("Error while parsing the feature flag {} with value {}", key, value, e);
}
} }
} }
}); });
@ -157,13 +162,13 @@ public class FeatureFlagMigrationHelperCEImpl implements FeatureFlagMigrationHel
// featuresWithPendingMigrationDB => {feature1 : enable, feature2 : disable} // featuresWithPendingMigrationDB => {feature1 : enable, feature2 : disable}
// latestFeatureDiffsWithMigrationType => {feature1 : enable, feature2 : enable, feature3 : disable} // latestFeatureDiffsWithMigrationType => {feature1 : enable, feature2 : enable, feature3 : disable}
// updatedFlagsForMigrations => {feature1 : enable, feature3 : disable} // updatedFlagsForMigrations => {feature1 : enable, feature3 : disable}
List<FeatureFlagEnum> featureFlagsToBeRemoved = new ArrayList<>();
updatedFlagsForMigrations.forEach((featureFlagEnum, featureMigrationType) -> { updatedFlagsForMigrations.forEach((featureFlagEnum, featureMigrationType) -> {
if (latestFeatureDiffsWithMigrationType.containsKey(featureFlagEnum) if (latestFeatureDiffsWithMigrationType.containsKey(featureFlagEnum)
&& !featureMigrationType.equals(latestFeatureDiffsWithMigrationType.get(featureFlagEnum))) { && !featureMigrationType.equals(latestFeatureDiffsWithMigrationType.get(featureFlagEnum))) {
/* /*
Scenario when the migrations will be blocked on user input and may end up in a case where we just have Scenario when the migrations will be blocked on user input and may end up in a case where we just have
to remove the entry as migration it's no longer needed: to remove the entry as migration is no longer needed:
Step 1: Feature gets enabled by adding a valid licence and enable migration gets registered Step 1: Feature gets enabled by adding a valid licence and enable migration gets registered
Step 2: License expires which results in feature getting disabled so migration entry gets registered Step 2: License expires which results in feature getting disabled so migration entry gets registered
with disable type (This will happen via cron to check the license status) with disable type (This will happen via cron to check the license status)
@ -172,10 +177,16 @@ public class FeatureFlagMigrationHelperCEImpl implements FeatureFlagMigrationHel
Step 4: User adds the valid key or renews the subscription again which results in enabling the Step 4: User adds the valid key or renews the subscription again which results in enabling the
feature and ends up in nullifying the effect for step 2 feature and ends up in nullifying the effect for step 2
*/ */
updatedFlagsForMigrations.remove(featureFlagEnum); featureFlagsToBeRemoved.add(featureFlagEnum);
latestFeatureDiffsWithMigrationType.remove(featureFlagEnum);
} }
}); });
// Added a separate loop to remove the flags as we cannot remove the entry while iterating over the map to
// avoid the ConcurrentModificationException
featureFlagsToBeRemoved.forEach(featureFlagEnum -> {
updatedFlagsForMigrations.remove(featureFlagEnum);
latestFeatureDiffsWithMigrationType.remove(featureFlagEnum);
});
// Add the latest flags which were not part of earlier check. // Add the latest flags which were not part of earlier check.
updatedFlagsForMigrations.putAll(latestFeatureDiffsWithMigrationType); updatedFlagsForMigrations.putAll(latestFeatureDiffsWithMigrationType);
return updatedFlagsForMigrations; return updatedFlagsForMigrations;

View File

@ -226,4 +226,50 @@ class FeatureFlagMigrationHelperTest {
}) })
.verifyComplete(); .verifyComplete();
} }
@Test
void
getUpdatedFlagsWithPendingMigration_diffForExistingAndLatestFlag_sameFlagIsFlippedAsPerDBState_flagGetsRemovedFromPendingMigrationList() {
// Mock DB state to have the feature flag in pending migration list with DISABLE status which means the feature
// flag flipped from true to false
Tenant defaultTenant = new Tenant();
defaultTenant.setId(UUID.randomUUID().toString());
TenantConfiguration tenantConfiguration = new TenantConfiguration();
tenantConfiguration.setFeaturesWithPendingMigration(Map.of(TENANT_TEST_FEATURE, DISABLE));
defaultTenant.setTenantConfiguration(tenantConfiguration);
// Mock CS calls to fetch the feature flags to have the feature flag in pending migration list with ENABLE
// status
// This means the feature flag flipped from false to true again with latest check
CachedFeatures existingCachedFeatures = new CachedFeatures();
Map<String, Boolean> featureMap = new HashMap<>();
featureMap.put(TENANT_TEST_FEATURE.name(), false);
existingCachedFeatures.setFeatures(featureMap);
existingCachedFeatures.setRefreshedAt(Instant.now().minus(1, ChronoUnit.DAYS));
CachedFeatures latestCachedFeatures = new CachedFeatures();
Map<String, Boolean> latestFeatureMap = new HashMap<>();
latestFeatureMap.put(TENANT_TEST_FEATURE.name(), true);
latestCachedFeatures.setFeatures(latestFeatureMap);
latestCachedFeatures.setRefreshedAt(Instant.now());
Mockito.when(cacheableFeatureFlagHelper.fetchCachedTenantFeatures(any()))
.thenReturn(Mono.just(existingCachedFeatures))
.thenReturn(Mono.just(latestCachedFeatures));
Mockito.when(cacheableFeatureFlagHelper.evictCachedTenantFeatures(any()))
.thenReturn(Mono.empty());
Mono<Map<FeatureFlagEnum, FeatureMigrationType>> getUpdatedFlagsWithPendingMigration =
featureFlagMigrationHelper.getUpdatedFlagsWithPendingMigration(defaultTenant);
StepVerifier.create(getUpdatedFlagsWithPendingMigration)
.assertNext(featureFlagEnumFeatureMigrationTypeMap -> {
// As the feature flag is flipped back to true, the feature flag should be removed from the pending
// migration entries as the migration is no longer required
assertThat(featureFlagEnumFeatureMigrationTypeMap).isEmpty();
})
.verifyComplete();
}
} }