Skip to content

Commit a081d8a

Browse files
committed
Fine grain locking to avoid deadlocks in CU cache
1 parent 9c4aa05 commit a081d8a

File tree

2 files changed

+67
-43
lines changed

2 files changed

+67
-43
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,6 @@ public <T> T withCompilationUnit(IJavaProject project, URI uri, Function<Compila
216216
logger.info("CU Cache: work item submitted for doc {}", uri.toASCIIString());
217217

218218
if (project != null) {
219-
ReadLock lock = environmentCacheLock.readLock();
220-
lock.lock();
221219
try {
222220
CompilationUnit cu = null;
223221
try {
@@ -234,10 +232,16 @@ public <T> T withCompilationUnit(IJavaProject project, URI uri, Function<Compila
234232
}
235233

236234
if (cu != null) {
237-
projectToDocs.get(project.getLocationUri(), () -> new HashSet<>()).add(uri);
238-
239-
logger.debug("CU Cache: start work on AST for {}", uri.toString());
235+
ReadLock lock = environmentCacheLock.readLock();
236+
lock.lock();
237+
try {
238+
logger.info("CU Cache: start work on AST for {}", uri.toString());
240239
return requestor.apply(cu);
240+
} finally {
241+
logger.info("CU Cache: end work on AST for {}", uri.toString());
242+
lock.unlock();
243+
}
244+
241245
}
242246
}
243247
catch (CancellationException e) {
@@ -246,10 +250,6 @@ public <T> T withCompilationUnit(IJavaProject project, URI uri, Function<Compila
246250
catch (Exception e) {
247251
logger.error("", e);
248252
}
249-
finally {
250-
logger.debug("CU Cache: end work on AST for {}", uri.toString());
251-
lock.unlock();
252-
}
253253
}
254254

255255
return requestor.apply(null);
@@ -259,34 +259,49 @@ private synchronized CompletableFuture<CompilationUnit> requestCU(IJavaProject p
259259
CompletableFuture<CompilationUnit> cuFuture = uriToCu.getIfPresent(uri);
260260
if (cuFuture == null) {
261261
cuFuture = CompletableFuture.supplyAsync(() -> {
262+
ReadLock lock = environmentCacheLock.readLock();
263+
lock.lock();
264+
logger.info("Started parsing CU for " + uri);
262265

263266
try {
264-
logger.info("Started parsing CU for " + uri);
265267
Tuple2<List<Classpath>, INameEnvironmentWithProgress> lookupEnvTuple = loadLookupEnvTuple(project);
266268
String uriStr = uri.toASCIIString();
267269
String unitName = uriStr.substring(uriStr.lastIndexOf("/") + 1); // skip over '/'
268270
CompilationUnit cUnit = parse2(fetchContent(uri).toCharArray(), uriStr, unitName, lookupEnvTuple.getT1(), lookupEnvTuple.getT2(),
269271
annotationHierarchies.get(project.getLocationUri(), AnnotationHierarchies::new));
270-
271272
logger.debug("CU Cache: created new AST for {}", uri.toASCIIString());
272-
273273
logger.info("Parsed successfully CU for " + uri);
274274
return cUnit;
275275
} catch (Throwable t) {
276276
// Complete future exceptionally
277277
throw new CompletionException(t);
278+
} finally {
279+
logger.info("Finished parsing CU for {}", uri);
280+
lock.unlock();
278281
}
279282
}, createCuExecutorThreadPool);
280283
// Cache the future
281284
uriToCu.put(uri, cuFuture);
282285
// If CU future completed exceptionally invalidate the cache entry
283-
cuFuture.exceptionally(t -> {
284-
if (!(t instanceof CancellationException)) {
285-
logger.error("", t);
286-
}
287-
uriToCu.invalidate(uri);
288-
return null;
289-
});
286+
cuFuture
287+
.thenAccept(cu -> {
288+
synchronized(CompilationUnitCache.this) {
289+
try {
290+
projectToDocs.get(project.getLocationUri(), () -> new HashSet<>()).add(uri);
291+
} catch (ExecutionException e) {
292+
// shouldn't happen
293+
}
294+
}
295+
})
296+
.exceptionally(t -> {
297+
if (!(t instanceof CancellationException)) {
298+
logger.error("", t);
299+
}
300+
synchronized(CompilationUnitCache.this) {
301+
uriToCu.invalidate(uri);
302+
}
303+
return null;
304+
});
290305
}
291306
return cuFuture;
292307
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/jdt/ls/JdtLsProjectCache.java

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -338,43 +338,52 @@ public void initialize(InitializeParams p, ServerCapabilities cap) {
338338

339339
private class JstLsClasspathListener implements ClasspathListener {
340340

341+
/*
342+
* Synchronize to make non-reentrant such that events handled in predictable order
343+
*/
341344
@Override
342-
public void changed(Event event) {
345+
public synchronized void changed(Event event) {
343346
log.debug("claspath event received {}", event);
344347
server.doOnInitialized(() -> {
345348
try {
346-
synchronized (table) {
347-
String uri = UriUtil.normalize(event.projectUri);
348-
log.debug("uri = {}", uri);
349-
if (event.deleted) {
350-
log.debug("event.deleted = true");
351-
IJavaProject deleted = table.remove(uri);
352-
if (deleted!=null) {
353-
log.debug("removed from table = true");
354-
notifyDelete(deleted);
355-
} else {
356-
log.warn("Deleted project not removed because uri {} not found in {}", uri, table.keySet());
357-
}
349+
String uri = UriUtil.normalize(event.projectUri);
350+
log.debug("uri = {}", uri);
351+
if (event.deleted) {
352+
log.debug("event.deleted = true");
353+
IJavaProject deleted;
354+
synchronized (table) {
355+
deleted = table.remove(uri);
356+
}
357+
// Notify outside of the lock
358+
if (deleted!=null) {
359+
log.debug("removed from table = true");
360+
notifyDelete(deleted);
358361
} else {
359-
log.debug("deleted = false");
360-
URI projectUri = new URI(uri);
361-
ClasspathData classpath = new ClasspathData(event.name, event.classpath.getEntries(), event.classpath.getJavaVersion());
362-
IJavaProject oldProject = table.get(uri);
362+
log.warn("Deleted project not removed because uri {} not found in {}", uri, table.keySet());
363+
}
364+
} else {
365+
log.debug("deleted = false");
366+
URI projectUri = new URI(uri);
367+
ClasspathData classpath = new ClasspathData(event.name, event.classpath.getEntries(), event.classpath.getJavaVersion());
368+
IJavaProject oldProject, newProject;
369+
synchronized(table) {
370+
oldProject = table.get(uri);
363371
if (oldProject != null && classpath.equals(oldProject.getClasspath())) {
364372
// nothing has changed
365373
return;
366374
}
367-
IProjectBuild projectBuild = from(event.projectBuild);
368-
IJavaProject newProject = IS_JANDEX_INDEX
375+
IProjectBuild projectBuild = from(event.projectBuild);
376+
newProject = IS_JANDEX_INDEX
369377
? new JavaProject(getFileObserver(), projectUri, classpath,
370378
JdtLsProjectCache.this, projectBuild)
371379
: new JdtLsJavaProject(server.getClient(), projectUri, classpath, JdtLsProjectCache.this, projectBuild);
372380
table.put(uri, newProject);
373-
if (oldProject != null) {
374-
notifyChanged(newProject);
375-
} else {
376-
notifyCreated(newProject);
377-
}
381+
}
382+
// Notify outside of the lock
383+
if (oldProject != null) {
384+
notifyChanged(newProject);
385+
} else {
386+
notifyCreated(newProject);
378387
}
379388
}
380389
} catch (Exception e) {

0 commit comments

Comments
 (0)