Skip to content

Add async deletion - #198

Open
Meklo wants to merge 6 commits into
mainfrom
marcellinh/add_async_deletion
Open

Add async deletion#198
Meklo wants to merge 6 commits into
mainfrom
marcellinh/add_async_deletion

Conversation

@Meklo

@Meklo Meklo commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

PR Summary

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Meklo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 952f584e-ad09-43a7-b8f9-a12a4bcdac43

📥 Commits

Reviewing files that changed from the base of the PR and between 608aeff and 8a5b3bf.

📒 Files selected for processing (4)
  • src/main/java/org/gridsuite/explore/server/services/DirectoryService.java
  • src/main/java/org/gridsuite/explore/server/services/ExploreService.java
  • src/test/java/org/gridsuite/explore/server/ExploreTest.java
  • src/test/java/org/gridsuite/explore/server/SpreadsheetConfigCollectionTest.java
📝 Walkthrough

Walkthrough

The change adds directory element status transitions, a managed asynchronous execution service, and asynchronous single and bulk deletion flows. Tests add WireMock-based HTTP stubbing and verification for background deletion behavior.

Changes

Directory deletion lifecycle

Layer / File(s) Summary
Directory status update contract
src/main/java/org/gridsuite/explore/server/dto/DirectoryElementStatus.java, src/main/java/org/gridsuite/explore/server/services/DirectoryService.java, src/test/java/org/gridsuite/explore/server/SpreadsheetConfigCollectionTest.java
Adds ACTIVE and DELETING statuses and a bulk PUT API for updating directory element statuses.
Managed asynchronous execution
src/main/java/org/gridsuite/explore/server/services/ExploreServerExecutionService.java
Adds context-propagating cached-thread-pool execution, task error logging, and executor lifecycle management.
Deletion orchestration and validation
src/main/java/org/gridsuite/explore/server/services/ExploreService.java, src/test/java/org/gridsuite/explore/server/ExploreTest.java
Converts deletion methods to return CompletableFuture<Void>, applies deleting and active status transitions, handles successful and failed bulk deletions, and verifies background-thread execution with WireMock.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ExploreService
  participant ExploreServerExecutionService
  participant DirectoryService
  participant DirectoryServer
  Caller->>ExploreService: deleteElement(...) or deleteElementsFromDirectory(...)
  ExploreService->>ExploreServerExecutionService: runAsync(deletion task)
  ExploreServerExecutionService->>ExploreService: execute deletion task
  ExploreService->>DirectoryService: updateElementsStatus(..., DELETING, userId)
  ExploreService->>DirectoryServer: delete directory elements
  ExploreService->>DirectoryService: updateElementsStatus(..., ACTIVE, userId) for failures
  ExploreServerExecutionService-->>Caller: CompletableFuture completion
Loading

Suggested reviewers: jonenst

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is just a template placeholder and does not meaningfully describe the changes. Replace the placeholder with a short summary of the async deletion behavior and related service changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: asynchronous deletion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@flomillot
flomillot self-requested a review July 23, 2026 08:18
@PostConstruct
private void postConstruct() {
ContextSnapshotFactory snapshotFactory = ContextSnapshotFactory.builder().build();
executorService = ContextExecutorService.wrap(Executors.newCachedThreadPool(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No thread limit ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think it's crucial to have a thread limit with this implementation, it spawns only one thread per deletion request. Previously I tried doing n thread for n element to delete and added a thread count limit but its potential to suck up all resources was too risky

try {
directoryService.deleteElement(id, userId);
directoryService.deleteDirectoryElement(id, userId);
// FIXME dirty fix to ignore errors and still delete the elements in the directory-server. To delete when handled properly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't understand this part, why this fixme and the dev didn't do a finally ?

@Meklo Meklo Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it's legacy, there was a period when we struggled with elements still appearing in the directory even after a deletion so a dirty overkill solution was adopted. Not mutualizing deleteDirectoryElement in a finally clause was surely an oversight

@Meklo Meklo Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That said I don't see drawbacks to moving it to a finally in this PR

} finally {
directoryService.deleteElementsFromDirectory(uuids, parentDirectoryUuids, userId);
}
return exploreServerExecutionService.runAsync(() -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

By the way, why the asynchronous threads are not inside directory server instead ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

deletion is all orchestrated by explore server, directory server has no vision over all the servers holding the actual data

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

maybe the logic should be separated from the directoryService to make this clearer

directoryService.deleteElement(id, userId);
deletedIds.add(id);
} catch (Exception e) {
LOGGER.error("Failed to delete element {}", id, e);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So without a finally, the status can be stuck in DELETING forever no ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same if a thread stop while executing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've added a tracking of failed deletion in order to revert those back.
For thread crash it's more tricky and would necessitate a more global solution at the project level to establish how we handle these type of recovery imo


public void deleteElementsFromDirectory(List<UUID> uuids, UUID parentDirectoryUuids, String userId) {
public CompletableFuture<Void> deleteElementsFromDirectory(List<UUID> uuids, UUID parentDirectoryUuid, String userId) {
directoryService.updateElementsStatus(uuids, "DELETING", userId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why it's outside the async ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No valid reason, I focused on turning the deletion async but this can also be included in the new thread

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just added an enum to make the call cleaner btw

Comment on lines +323 to +324
return new MockResponse(200);
} else if (path.matches("/v1/elements\\?ids=.*&status=.*") && "PUT".equals(request.getMethod())) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I though every new test should be with WireMock ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

True I added mindlessly added it here since there's currently no wiremock test in this class but let's do things clean

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/test/java/org/gridsuite/explore/server/ExploreTest.java (1)

180-181: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

WireMockServer is started/stopped for every test, not just the one that needs it.

Only testDeleteElementSpawnsDeletionThread uses wireMockServer, yet it's instantiated and started in the class-wide @BeforeEach and stopped in @AfterEach, adding a real socket bind/teardown to every other test in this large class.

♻️ Proposed refactor to scope WireMockServer to the single test
-    private WireMockServer wireMockServer;
-
     ...
     `@BeforeEach`
     void setup(final MockWebServer server) throws Exception {
-        wireMockServer = new WireMockServer(wireMockConfig().dynamicPort());
-        wireMockServer.start();
-
         ...
     }

-    `@AfterEach`
-    void teardown() {
-        if (wireMockServer != null) {
-            wireMockServer.stop();
-        }
-    }
-
     `@Test`
     void testDeleteElementSpawnsDeletionThread() throws Exception {
+        WireMockServer wireMockServer = new WireMockServer(wireMockConfig().dynamicPort());
+        wireMockServer.start();
+        try {
             ...
+        } finally {
+            wireMockServer.stop();
+        }
     }

Also applies to: 193-195, 522-528

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/org/gridsuite/explore/server/ExploreTest.java` around lines 180
- 181, Scope the WireMockServer lifecycle to
testDeleteElementSpawnsDeletionThread instead of the class-wide setup. Remove
its `@BeforeEach` initialization and `@AfterEach` shutdown, instantiate and start it
within that test, and stop it when the test completes while preserving the
existing wireMockServer stubbing and usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/org/gridsuite/explore/server/services/ExploreServerExecutionService.java`:
- Around line 39-42: Update ExploreServerExecutionService.preDestroy to await
graceful executor termination after calling shutdown, using TimeUnit and an
appropriate timeout so in-flight deletion tasks can complete before shutdown
finishes. Preserve the existing shutdown behavior and handle interruption
according to the service’s established conventions.

In `@src/main/java/org/gridsuite/explore/server/services/ExploreService.java`:
- Around line 210-231: Update deleteElementsFromDirectory so each post-loop
cleanup call—directoryService.deleteElementsFromDirectory for deletedIds and
directoryService.updateElementsStatus for failedIds—is independently guarded
against exceptions, ensuring one cleanup failure does not prevent the other and
both groups receive their intended cleanup or status restoration.

---

Nitpick comments:
In `@src/test/java/org/gridsuite/explore/server/ExploreTest.java`:
- Around line 180-181: Scope the WireMockServer lifecycle to
testDeleteElementSpawnsDeletionThread instead of the class-wide setup. Remove
its `@BeforeEach` initialization and `@AfterEach` shutdown, instantiate and start it
within that test, and stop it when the test completes while preserving the
existing wireMockServer stubbing and usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c2f211b8-f49a-4c95-b5f8-d8b3a8c46b89

📥 Commits

Reviewing files that changed from the base of the PR and between 7de623a and 608aeff.

📒 Files selected for processing (6)
  • src/main/java/org/gridsuite/explore/server/dto/DirectoryElementStatus.java
  • src/main/java/org/gridsuite/explore/server/services/DirectoryService.java
  • src/main/java/org/gridsuite/explore/server/services/ExploreServerExecutionService.java
  • src/main/java/org/gridsuite/explore/server/services/ExploreService.java
  • src/test/java/org/gridsuite/explore/server/ExploreTest.java
  • src/test/java/org/gridsuite/explore/server/SpreadsheetConfigCollectionTest.java

Comment on lines +39 to +42
@PreDestroy
private void preDestroy() {
executorService.shutdown();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Executor shutdown doesn't wait for in-flight deletion tasks.

shutdown() alone stops new task submission but returns immediately; it doesn't block until running tasks finish. On app shutdown/redeploy, in-flight async deletion tasks (multi-step directory-server calls) can be cut off mid-flight, leaving elements stuck in DELETING status.

🔧 Proposed fix to wait for graceful termination
     `@PreDestroy`
     private void preDestroy() {
-        executorService.shutdown();
+        executorService.shutdown();
+        try {
+            if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
+                executorService.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            executorService.shutdownNow();
+            Thread.currentThread().interrupt();
+        }
     }

Requires adding import java.util.concurrent.TimeUnit;.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/gridsuite/explore/server/services/ExploreServerExecutionService.java`
around lines 39 - 42, Update ExploreServerExecutionService.preDestroy to await
graceful executor termination after calling shutdown, using TimeUnit and an
appropriate timeout so in-flight deletion tasks can complete before shutdown
finishes. Preserve the existing shutdown behavior and handle interruption
according to the service’s established conventions.

Comment on lines +210 to 231
public CompletableFuture<Void> deleteElementsFromDirectory(List<UUID> uuids, UUID parentDirectoryUuid, String userId) {
return exploreServerExecutionService.runAsync(() -> {
directoryService.updateElementsStatus(uuids, DirectoryElementStatus.DELETING, userId);
List<UUID> deletedIds = new ArrayList<>();
List<UUID> failedIds = new ArrayList<>();
for (UUID id : uuids) {
try {
directoryService.deleteElement(id, userId);
deletedIds.add(id);
} catch (Exception e) {
LOGGER.error("Failed to delete element {}", id, e);
failedIds.add(id);
}
}
if (!deletedIds.isEmpty()) {
directoryService.deleteElementsFromDirectory(deletedIds, parentDirectoryUuid, userId);
}
if (!failedIds.isEmpty()) {
directoryService.updateElementsStatus(failedIds, DirectoryElementStatus.ACTIVE, userId);
}
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Elements can be left permanently stuck in DELETING if the post-loop cleanup calls fail.

The per-element try/catch (lines 216-223) only guards against individual deletion failures. If directoryService.deleteElementsFromDirectory(deletedIds, ...) (line 225) or the subsequent updateElementsStatus(failedIds, ACTIVE, ...) (line 228) itself throws, the exception escapes uncaught: elements in deletedIds were already deleted from their owning services but never removed from the directory listing (stuck DELETING), and failedIds never get reverted to ACTIVE. This is a related-but-distinct gap from the per-element failure handling already discussed in past review (which added the deletedIds/failedIds tracking but didn't protect these two follow-up calls).

🔧 Proposed fix to guard the cleanup calls
             if (!deletedIds.isEmpty()) {
-                directoryService.deleteElementsFromDirectory(deletedIds, parentDirectoryUuid, userId);
+                try {
+                    directoryService.deleteElementsFromDirectory(deletedIds, parentDirectoryUuid, userId);
+                } catch (Exception e) {
+                    LOGGER.error("Failed to remove deleted elements {} from directory", deletedIds, e);
+                }
             }
             if (!failedIds.isEmpty()) {
-                directoryService.updateElementsStatus(failedIds, DirectoryElementStatus.ACTIVE, userId);
+                try {
+                    directoryService.updateElementsStatus(failedIds, DirectoryElementStatus.ACTIVE, userId);
+                } catch (Exception e) {
+                    LOGGER.error("Failed to restore ACTIVE status for {}", failedIds, e);
+                }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public CompletableFuture<Void> deleteElementsFromDirectory(List<UUID> uuids, UUID parentDirectoryUuid, String userId) {
return exploreServerExecutionService.runAsync(() -> {
directoryService.updateElementsStatus(uuids, DirectoryElementStatus.DELETING, userId);
List<UUID> deletedIds = new ArrayList<>();
List<UUID> failedIds = new ArrayList<>();
for (UUID id : uuids) {
try {
directoryService.deleteElement(id, userId);
deletedIds.add(id);
} catch (Exception e) {
LOGGER.error("Failed to delete element {}", id, e);
failedIds.add(id);
}
}
if (!deletedIds.isEmpty()) {
directoryService.deleteElementsFromDirectory(deletedIds, parentDirectoryUuid, userId);
}
if (!failedIds.isEmpty()) {
directoryService.updateElementsStatus(failedIds, DirectoryElementStatus.ACTIVE, userId);
}
});
}
public CompletableFuture<Void> deleteElementsFromDirectory(List<UUID> uuids, UUID parentDirectoryUuid, String userId) {
return exploreServerExecutionService.runAsync(() -> {
directoryService.updateElementsStatus(uuids, DirectoryElementStatus.DELETING, userId);
List<UUID> deletedIds = new ArrayList<>();
List<UUID> failedIds = new ArrayList<>();
for (UUID id : uuids) {
try {
directoryService.deleteElement(id, userId);
deletedIds.add(id);
} catch (Exception e) {
LOGGER.error("Failed to delete element {}", id, e);
failedIds.add(id);
}
}
if (!deletedIds.isEmpty()) {
try {
directoryService.deleteElementsFromDirectory(deletedIds, parentDirectoryUuid, userId);
} catch (Exception e) {
LOGGER.error("Failed to remove deleted elements {} from directory", deletedIds, e);
}
}
if (!failedIds.isEmpty()) {
try {
directoryService.updateElementsStatus(failedIds, DirectoryElementStatus.ACTIVE, userId);
} catch (Exception e) {
LOGGER.error("Failed to restore ACTIVE status for {}", failedIds, e);
}
}
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/org/gridsuite/explore/server/services/ExploreService.java`
around lines 210 - 231, Update deleteElementsFromDirectory so each post-loop
cleanup call—directoryService.deleteElementsFromDirectory for deletedIds and
directoryService.updateElementsStatus for failedIds—is independently guarded
against exceptions, ensuring one cleanup failure does not prevent the other and
both groups receive their intended cleanup or status restoration.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
58.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants