Skip to content

Commit 0f73552

Browse files
authored
Fix DtdUtils static field retaining disposed Project (#8658)
#8657
1 parent bfae8d1 commit 0f73552

File tree

3 files changed

+109
-10
lines changed

3 files changed

+109
-10
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
## Next
2+
3+
### Added
4+
5+
### Changed
6+
7+
### Removed
8+
9+
### Fixed
10+
11+
- DtdUtils static field retaining disposed Project (#8658)
12+
113
## 88.2.0
214

315
### Added

src/io/flutter/dart/DtdUtils.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@
1414
import java.util.concurrent.*;
1515

1616
public class DtdUtils {
17-
private static final Map<Project, CompletableFuture<DartToolingDaemonService>> WAITERS = new ConcurrentHashMap<>();
17+
// Visible for testing
18+
static final Map<Project, CompletableFuture<DartToolingDaemonService>> WAITERS = new ConcurrentHashMap<>();
1819

1920
public @NotNull CompletableFuture<DartToolingDaemonService> readyDtdService(@NotNull Project project) {
21+
final DartToolingDaemonService dtdService = DartToolingDaemonService.getInstance(project);
22+
if (dtdService.getWebSocketReady()) {
23+
return CompletableFuture.completedFuture(dtdService);
24+
}
25+
2026
return WAITERS.computeIfAbsent(project, p -> {
21-
final DartToolingDaemonService dtdService = DartToolingDaemonService.getInstance(project);
2227
CompletableFuture<DartToolingDaemonService> readyService = new CompletableFuture<>();
2328

24-
if (dtdService.getWebSocketReady()) {
25-
readyService.complete(dtdService);
26-
return readyService;
27-
}
28-
2929
final ScheduledExecutorService scheduler = AppExecutorUtil.getAppScheduledExecutorService();
3030

3131
final ScheduledFuture<?> poll = scheduler.scheduleWithFixedDelay(() -> {
@@ -42,9 +42,17 @@ public class DtdUtils {
4242
readyService.whenComplete((s, t) -> {
4343
poll.cancel(false);
4444
timeout.cancel(false);
45-
if (t != null) {
46-
WAITERS.remove(p);
47-
}
45+
// Remove from waiters when done.
46+
// We use the scheduler to ensure this runs after computeIfAbsent returns,
47+
// in case readyService was completed synchronously inside computeIfAbsent.
48+
// Although with the check above, synchronous completion here is unlikely,
49+
// it's safer to be async or just rely on the fact that polling completes it async.
50+
// If polling completes it, we are on a different thread, so computeIfAbsent is definitely done.
51+
// If timeout completes it, we are on a different thread.
52+
// So direct removal is safe IF completion happens async.
53+
// The only sync completion risk is if we checked again inside and completed.
54+
// But we don't check inside synchronously anymore (only in poll).
55+
WAITERS.remove(project);
4856
});
4957

5058
return readyService;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package io.flutter.dart;
2+
3+
import com.intellij.openapi.project.Project;
4+
import com.intellij.testFramework.ServiceContainerUtil;
5+
import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService;
6+
import io.flutter.testing.CodeInsightProjectFixture;
7+
import io.flutter.testing.Testing;
8+
import org.junit.After;
9+
import org.junit.Before;
10+
import org.junit.Rule;
11+
import org.junit.Test;
12+
import org.mockito.Mock;
13+
import org.mockito.MockitoAnnotations;
14+
15+
import java.util.concurrent.CompletableFuture;
16+
17+
import static org.junit.Assert.assertTrue;
18+
import static org.mockito.Mockito.when;
19+
20+
public class DtdUtilsTest {
21+
@Rule
22+
public final CodeInsightProjectFixture fixture = Testing.makeCodeInsightModule();
23+
24+
@Mock
25+
private DartToolingDaemonService mockDtdService;
26+
27+
private Project project;
28+
29+
@Before
30+
public void setUp() throws Exception {
31+
MockitoAnnotations.initMocks(this);
32+
project = fixture.getProject();
33+
34+
// Register mock service
35+
// We need to use the disposable from the fixture to ensure it gets cleaned up
36+
ServiceContainerUtil.registerServiceInstance(project, DartToolingDaemonService.class, mockDtdService);
37+
}
38+
39+
@Test
40+
public void testReadyDtdServiceRemovesProjectFromWaiters() throws Exception {
41+
when(mockDtdService.getWebSocketReady()).thenReturn(true);
42+
43+
DtdUtils dtdUtils = new DtdUtils();
44+
CompletableFuture<DartToolingDaemonService> future = dtdUtils.readyDtdService(project);
45+
46+
assertTrue(future.isDone());
47+
// Verify WAITERS map is empty
48+
assertTrue("WAITERS map should be empty after service is ready", DtdUtils.WAITERS.isEmpty());
49+
}
50+
51+
@Test
52+
public void testWaitersMapIsClearedAfterAsyncCompletion() throws Exception {
53+
when(mockDtdService.getWebSocketReady()).thenReturn(false);
54+
55+
DtdUtils dtdUtils = new DtdUtils();
56+
CompletableFuture<DartToolingDaemonService> future = dtdUtils.readyDtdService(project);
57+
58+
// It should be in the map now
59+
assertTrue("Project should be in WAITERS map", DtdUtils.WAITERS.containsKey(project));
60+
61+
// Simulate service becoming ready
62+
when(mockDtdService.getWebSocketReady()).thenReturn(true);
63+
64+
// Wait for the poller to pick it up (it runs every 500ms)
65+
// We can just wait on the future with a timeout
66+
future.get(5, java.util.concurrent.TimeUnit.SECONDS);
67+
68+
assertTrue(future.isDone());
69+
70+
// Wait for the removal to happen (it happens in whenComplete, which might be slightly delayed if async)
71+
// We can poll the map
72+
long start = System.currentTimeMillis();
73+
while (!DtdUtils.WAITERS.isEmpty() && System.currentTimeMillis() - start < 2000) {
74+
Thread.sleep(100);
75+
}
76+
77+
assertTrue("WAITERS map should be empty after async completion", DtdUtils.WAITERS.isEmpty());
78+
}
79+
}

0 commit comments

Comments
 (0)