Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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 @@ -18,4 +18,5 @@ public class ActionCollectionSpanCE {

// Getter spans
public static final String GET_ACTION_COLLECTION_BY_ID = APPSMITH_SPAN_PREFIX + "get.actionCollectionById";
public static final String GET_ACTION_COLLECTIONS_BY_IDS = APPSMITH_SPAN_PREFIX + "get.actionCollectionsByIds";
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Mono<ActionCollectionDTO> deleteUnpublishedActionCollection(

Mono<ActionCollection> findById(String id, AclPermission aclPermission);

Flux<ActionCollection> findAllByIds(List<String> ids, AclPermission aclPermission);

Mono<ActionCollectionDTO> findActionCollectionDTObyIdAndViewMode(
String id, Boolean viewMode, AclPermission permission);

Expand Down Expand Up @@ -81,6 +83,16 @@ Flux<ActionCollection> findAllActionCollectionsByContextIdAndContextTypeAndViewM

Mono<Void> bulkValidateAndUpdateActionCollectionInRepository(List<ActionCollection> actionCollectionList);

/**
* General purpose bulk update method that directly saves action collections to the database without validation.
* This method is optimized for scenarios where the action collections are already validated or when
* validation is not required (e.g., refactoring operations, data migrations).
*
* @param actionCollectionList List of ActionCollection objects to update
* @return Mono<Void> indicating completion of the bulk update operation
*/
Mono<Void> bulkUpdateActionCollections(List<ActionCollection> actionCollectionList);

Mono<Void> saveLastEditInformationInParent(ActionCollectionDTO actionCollectionDTO);

Flux<ActionCollection> findByArtifactIdAndArtifactType(String artifactId, ArtifactType artifactType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import java.util.UUID;

import static com.appsmith.external.constants.spans.ActionCollectionSpan.GET_ACTION_COLLECTION_BY_ID;
import static com.appsmith.external.constants.spans.ce.ActionCollectionSpanCE.GET_ACTION_COLLECTIONS_BY_IDS;
import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNewFieldValuesIntoOldObject;
import static java.lang.Boolean.TRUE;

Expand Down Expand Up @@ -369,6 +370,14 @@ public Mono<ActionCollection> findById(String id, AclPermission aclPermission) {
.tap(Micrometer.observation(observationRegistry));
}

@Override
public Flux<ActionCollection> findAllByIds(List<String> ids, AclPermission aclPermission) {
return repository
.findAllByIds(ids, aclPermission)
.name(GET_ACTION_COLLECTIONS_BY_IDS)
.tap(Micrometer.observation(observationRegistry));
}

@Override
public Mono<ActionCollectionDTO> findActionCollectionDTObyIdAndViewMode(
String id, Boolean viewMode, AclPermission permission) {
Expand Down Expand Up @@ -622,6 +631,28 @@ public Mono<Void> bulkValidateAndUpdateActionCollectionInRepository(List<ActionC
.flatMap(repository::bulkUpdate);
}

/**
* General purpose bulk update method that directly saves action collections to the database without validation.
* This method is optimized for scenarios where the action collections are already validated or when
* validation is not required (e.g., refactoring operations, data migrations).
*
* @param actionCollectionList List of ActionCollection objects to update
* @return Mono<Void> indicating completion of the bulk update operation
*/
@Override
public Mono<Void> bulkUpdateActionCollections(List<ActionCollection> actionCollectionList) {
if (actionCollectionList == null || actionCollectionList.isEmpty()) {
return Mono.empty();
}

// Set git sync IDs for action collections that don't have them
actionCollectionList.stream()
.filter(actionCollection -> actionCollection.getGitSyncId() == null)
.forEach(this::setGitSyncIdInActionCollection);

return repository.bulkUpdate(actionCollectionList);
}

@Override
public Mono<Void> saveLastEditInformationInParent(ActionCollectionDTO actionCollectionDTO) {
// Do nothing as this is already taken care for JS objects in the context of page
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -51,25 +53,63 @@ public Mono<Void> refactorReferencesInExistingEntities(

String oldName = refactorEntityNameDTO.getOldFullyQualifiedName();
String newName = refactorEntityNameDTO.getNewFullyQualifiedName();
Mono<List<ActionCollection>> actionCollectionsMono = evalVersionMono.flatMap(evalVersion -> Flux.fromIterable(
updatableCollectionIds)
.flatMap(collectionId ->
actionCollectionService.findById(collectionId, actionPermission.getEditPermission()))

return evalVersionMono.flatMap(evalVersion -> actionCollectionService
.findAllByIds(new ArrayList<>(updatableCollectionIds), actionPermission.getEditPermission())
.collectList()
.flatMap(actionCollections -> {
if (actionCollections.isEmpty()) {
return Mono.empty();
}

log.debug(
"Processing {} action collections for refactoring with bulk processing",
actionCollections.size());

// Process all action collections in bulk
return processBulkActionCollections(
actionCollections, oldName, newName, evalVersion, oldNamePattern);
}));
}

/**
* Process all action collections in a single bulk operation for refactoring
*/
private Mono<Void> processBulkActionCollections(
List<ActionCollection> actionCollections,
String oldName,
String newName,
int evalVersion,
Pattern oldNamePattern) {

return Flux.fromIterable(actionCollections)
.flatMap(actionCollection -> {
final ActionCollectionDTO unpublishedCollection = actionCollection.getUnpublishedCollection();

return this.refactorNameInActionCollection(
unpublishedCollection, oldName, newName, evalVersion, oldNamePattern)
.flatMap(isPresent -> {
if (Boolean.TRUE.equals(isPresent)) {
return actionCollectionService.save(actionCollection);
}
return Mono.just(actionCollection);
});
.map(isPresent -> Boolean.TRUE.equals(isPresent) ? actionCollection : null);
})
.collectList());
// Keep only the ones that actually need update
.filter(Objects::nonNull)
.collectList()
.flatMap(collectionsToUpdate -> {
if (collectionsToUpdate.isEmpty()) {
log.debug("No action collections require updates for refactoring");
return Mono.empty();
}

log.debug(
"Processing bulk operation for {} action collections that require refactoring",
collectionsToUpdate.size());

return actionCollectionsMono.then();
// Use bulk update for all action collections at once
return actionCollectionService
.bulkUpdateActionCollections(collectionsToUpdate)
.doOnSuccess(unused -> log.debug(
"Bulk refactoring completed for {} action collections",
collectionsToUpdate.size()));
});
}

protected Mono<Boolean> refactorNameInActionCollection(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import lombok.Data;
import reactor.core.publisher.Mono;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

@Data
Expand All @@ -14,8 +14,19 @@ public class RefactoringMetaDTO {
Mono<PageDTO> pageDTOMono;
Pattern oldNamePattern;

Set<String> updatedBindingPaths = new HashSet<>();
/**
* Thread-safe set for tracking binding paths that have been updated during refactoring.
* Uses ConcurrentHashMap.newKeySet() to ensure thread-safety as this collection can be
* modified concurrently by multiple refactoring services during reactive operations.
*/
Set<String> updatedBindingPaths = ConcurrentHashMap.newKeySet();
Comment on lines +17 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that concurrent modification actually occurs.

The switch from HashSet to ConcurrentHashMap.newKeySet() adds overhead. While the Javadoc mentions "modified concurrently by multiple refactoring services during reactive operations," reactive flows don't automatically imply concurrent modification unless threads are explicitly switched via .subscribeOn() or .publishOn().

Run this script to verify concurrent access patterns:


🏁 Script executed:

#!/bin/bash
# Search for concurrent access patterns to updatedBindingPaths

# Look for subscribeOn/publishOn calls that might indicate thread switching
rg -n -C3 'updatedBindingPaths' --type java | rg -C3 '(subscribeOn|publishOn|parallel)'

# Check if RefactoringMetaDTO is shared across reactive chains
rg -n -A5 'RefactoringMetaDTO' --type java | rg -B2 -A3 '(flatMap|zip|merge|concat)'

# Look for documentation or comments about concurrent access
rg -n -C2 'RefactoringMetaDTO.*concurrent|concurrent.*RefactoringMetaDTO' --type java

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find all usages of updatedBindingPaths field
echo "=== Usages of updatedBindingPaths ==="
rg -n 'updatedBindingPaths' --type java -C2

echo ""
echo "=== Where RefactoringMetaDTO is instantiated ==="
rg -n 'new RefactoringMetaDTO|RefactoringMetaDTO()' --type java -C3

echo ""
echo "=== Classes/methods that use RefactoringMetaDTO ==="
rg -n 'RefactoringMetaDTO' --type java -C1 | head -50

Length of output: 45648


🏁 Script executed:

#!/bin/bash
# Check for thread switching operators in the reactive chains that modify updatedBindingPaths

echo "=== Checking refactoring methods that modify updatedBindingPaths ==="
echo "WidgetRefactoringServiceCEImpl - refactorReferencesInExistingEntities:"
rg -n -A30 'public Mono<Void> refactorReferencesInExistingEntities' app/server/appsmith-server/src/main/java/com/appsmith/server/widgets/refactors/WidgetRefactoringServiceCEImpl.java | head -50

echo ""
echo "NewActionRefactoringServiceCEImpl - refactorReferencesInExistingEntities:"
rg -n -A30 'public Mono<Void> refactorReferencesInExistingEntities' app/server/appsmith-server/src/main/java/com/appsmith/server/newactions/refactors/NewActionRefactoringServiceCEImpl.java | head -50

echo ""
echo "=== Looking for thread switching in the composition (RefactoringServiceCEImpl) ==="
rg -n -B5 -A15 'widgetEntityRefactoringService.refactorReferencesInExistingEntities' app/server/appsmith-server/src/main/java/com/appsmith/server/refactors/applications/RefactoringServiceCEImpl.java

Length of output: 4832


Revert updatedBindingPaths to HashSet; concurrent modification does not occur.

The refactoring services execute sequentially via .then() chaining without thread-switching operators. widgetsMono.then(actionsAndCollectionsMono) ensures operations run one after another on the same thread, so ConcurrentHashMap.newKeySet() is unnecessary overhead.

Set<String> updatedBindingPaths = new HashSet<>();
🤖 Prompt for AI Agents
In
app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/RefactoringMetaDTO.java
around lines 17 to 22, replace the thread-safe ConcurrentHashMap.newKeySet()
initialization with a plain HashSet because concurrent modification does not
occur; change the declaration to use new HashSet<>() for updatedBindingPaths and
ensure any necessary imports are adjusted/removed accordingly.


PageDTO updatedPage;

Set<String> updatableCollectionIds = new HashSet<>();
/**
* Thread-safe set for tracking action collection IDs that need to be updated during refactoring.
* Uses ConcurrentHashMap.newKeySet() to ensure thread-safety as this collection can be
* modified concurrently by multiple refactoring services during reactive operations.
*/
Set<String> updatableCollectionIds = ConcurrentHashMap.newKeySet();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.appsmith.external.views.Views;
import com.appsmith.server.dtos.ActionCollectionDTO;
import com.appsmith.server.dtos.EntityType;
import com.appsmith.server.dtos.PageDTO;
import com.fasterxml.jackson.annotation.JsonView;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -25,6 +26,36 @@ public class RefactorEntityNameCE_DTO {

String actionId;
String actionCollectionId;

public RefactorEntityNameCE_DTO(
String pageId,
String layoutId,
String oldName,
String newName,
EntityType entityType,
String actionId,
String actionCollectionId,
String collectionName,
ActionCollectionDTO actionCollection,
String oldFullyQualifiedName,
String newFullyQualifiedName,
Boolean isInternal,
CreatorContextType contextType) {
this.pageId = pageId;
this.layoutId = layoutId;
this.oldName = oldName;
this.newName = newName;
this.entityType = entityType;
this.actionId = actionId;
this.actionCollectionId = actionCollectionId;
this.collectionName = collectionName;
this.actionCollection = actionCollection;
this.oldFullyQualifiedName = oldFullyQualifiedName;
this.newFullyQualifiedName = newFullyQualifiedName;
this.isInternal = isInternal;
this.contextType = contextType;
}

String collectionName;
ActionCollectionDTO actionCollection;

Expand All @@ -38,4 +69,38 @@ public class RefactorEntityNameCE_DTO {
Boolean isInternal;

CreatorContextType contextType;

// Cache fields for optimization - storing objects directly
@JsonView(Views.Internal.class)
private PageDTO cachedPageDTO;

@JsonView(Views.Internal.class)
private Integer cachedEvaluationVersion;

@JsonView(Views.Internal.class)
private String cachedApplicationId;

// Helper methods for cache management
public boolean hasCachedPageDTO() {
return cachedPageDTO != null;
}

public boolean hasCachedEvaluationVersion() {
return cachedEvaluationVersion != null;
}

public boolean hasCachedApplicationId() {
return cachedApplicationId != null;
}

public RefactorEntityNameCE_DTO withCachedPageDTO(PageDTO pageDTO) {
this.cachedPageDTO = pageDTO;
this.cachedApplicationId = pageDTO.getApplicationId(); // Auto-populate applicationId
return this;
}

public RefactorEntityNameCE_DTO withCachedEvaluationVersion(Integer evaluationVersion) {
this.cachedEvaluationVersion = evaluationVersion;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ public interface NewActionServiceCE extends CrudService<NewAction, String> {

Mono<Void> bulkValidateAndUpdateActionInRepository(List<NewAction> newActionList);

/**
* General purpose bulk update method that directly saves actions to the database without validation.
* This method is optimized for scenarios where the actions are already validated or when
* validation is not required (e.g., refactoring operations, data migrations).
*
* @param newActionList List of NewAction objects to update
* @return Mono<Void> indicating completion of the bulk update operation
*/
Mono<Void> bulkUpdateActions(List<NewAction> newActionList);

Mono<NewAction> extractAndSetJsonPathKeys(NewAction newAction);

Mono<ActionDTO> updateUnpublishedAction(String id, ActionDTO action);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,26 @@ public Mono<Void> bulkValidateAndUpdateActionInRepository(List<NewAction> newAct
.flatMap(repository::bulkUpdate);
}

/**
* General purpose bulk update method that directly saves actions to the database without validation.
* This method is optimized for scenarios where the actions are already validated or when
* validation is not required (e.g., refactoring operations, data migrations).
*
* @param newActionList List of NewAction objects to update
* @return Mono<Void> indicating completion of the bulk update operation
*/
@Override
public Mono<Void> bulkUpdateActions(List<NewAction> newActionList) {
if (newActionList == null || newActionList.isEmpty()) {
return Mono.empty();
}

// Set git sync IDs for actions that don't have them
newActionList.stream().filter(action -> action.getGitSyncId() == null).forEach(this::setGitSyncIdInNewAction);

return repository.bulkUpdate(newActionList);
}

protected boolean isValidActionName(ActionDTO action) {
return entityValidationService.validateName(action.getName());
}
Expand Down
Loading
Loading