Fine grain locking to avoid deadlocks in CU cache

Signed-off-by: aboyko <alex.boyko@broadcom.com>
This commit is contained in:
aboyko
2025-03-16 14:04:47 -04:00
parent e906ad9991
commit e0d45e2aa9
2 changed files with 67 additions and 43 deletions

View File

@@ -216,8 +216,6 @@ public final class CompilationUnitCache implements DocumentContentProvider {
logger.info("CU Cache: work item submitted for doc {}", uri.toASCIIString());
if (project != null) {
ReadLock lock = environmentCacheLock.readLock();
lock.lock();
try {
CompilationUnit cu = null;
try {
@@ -234,10 +232,16 @@ public final class CompilationUnitCache implements DocumentContentProvider {
}
if (cu != null) {
projectToDocs.get(project.getLocationUri(), () -> new HashSet<>()).add(uri);
logger.debug("CU Cache: start work on AST for {}", uri.toString());
ReadLock lock = environmentCacheLock.readLock();
lock.lock();
try {
logger.info("CU Cache: start work on AST for {}", uri.toString());
return requestor.apply(cu);
} finally {
logger.info("CU Cache: end work on AST for {}", uri.toString());
lock.unlock();
}
}
}
catch (CancellationException e) {
@@ -246,10 +250,6 @@ public final class CompilationUnitCache implements DocumentContentProvider {
catch (Exception e) {
logger.error("", e);
}
finally {
logger.debug("CU Cache: end work on AST for {}", uri.toString());
lock.unlock();
}
}
return requestor.apply(null);
@@ -259,34 +259,49 @@ public final class CompilationUnitCache implements DocumentContentProvider {
CompletableFuture<CompilationUnit> cuFuture = uriToCu.getIfPresent(uri);
if (cuFuture == null) {
cuFuture = CompletableFuture.supplyAsync(() -> {
ReadLock lock = environmentCacheLock.readLock();
lock.lock();
logger.info("Started parsing CU for " + uri);
try {
logger.info("Started parsing CU for " + uri);
Tuple2<List<Classpath>, INameEnvironmentWithProgress> lookupEnvTuple = loadLookupEnvTuple(project);
String uriStr = uri.toASCIIString();
String unitName = uriStr.substring(uriStr.lastIndexOf("/") + 1); // skip over '/'
CompilationUnit cUnit = parse2(fetchContent(uri).toCharArray(), uriStr, unitName, lookupEnvTuple.getT1(), lookupEnvTuple.getT2(),
annotationHierarchies.get(project.getLocationUri(), AnnotationHierarchies::new));
logger.debug("CU Cache: created new AST for {}", uri.toASCIIString());
logger.info("Parsed successfully CU for " + uri);
return cUnit;
} catch (Throwable t) {
// Complete future exceptionally
throw new CompletionException(t);
} finally {
logger.info("Finished parsing CU for {}", uri);
lock.unlock();
}
}, createCuExecutorThreadPool);
// Cache the future
uriToCu.put(uri, cuFuture);
// If CU future completed exceptionally invalidate the cache entry
cuFuture.exceptionally(t -> {
if (!(t instanceof CancellationException)) {
logger.error("", t);
}
uriToCu.invalidate(uri);
return null;
});
cuFuture
.thenAccept(cu -> {
synchronized(CompilationUnitCache.this) {
try {
projectToDocs.get(project.getLocationUri(), () -> new HashSet<>()).add(uri);
} catch (ExecutionException e) {
// shouldn't happen
}
}
})
.exceptionally(t -> {
if (!(t instanceof CancellationException)) {
logger.error("", t);
}
synchronized(CompilationUnitCache.this) {
uriToCu.invalidate(uri);
}
return null;
});
}
return cuFuture;
}

View File

@@ -338,43 +338,52 @@ public class JdtLsProjectCache implements InitializableJavaProjectsService, Serv
private class JstLsClasspathListener implements ClasspathListener {
/*
* Synchronize to make non-reentrant such that events handled in predictable order
*/
@Override
public void changed(Event event) {
public synchronized void changed(Event event) {
log.debug("claspath event received {}", event);
server.doOnInitialized(() -> {
try {
synchronized (table) {
String uri = UriUtil.normalize(event.projectUri);
log.debug("uri = {}", uri);
if (event.deleted) {
log.debug("event.deleted = true");
IJavaProject deleted = table.remove(uri);
if (deleted!=null) {
log.debug("removed from table = true");
notifyDelete(deleted);
} else {
log.warn("Deleted project not removed because uri {} not found in {}", uri, table.keySet());
}
String uri = UriUtil.normalize(event.projectUri);
log.debug("uri = {}", uri);
if (event.deleted) {
log.debug("event.deleted = true");
IJavaProject deleted;
synchronized (table) {
deleted = table.remove(uri);
}
// Notify outside of the lock
if (deleted!=null) {
log.debug("removed from table = true");
notifyDelete(deleted);
} else {
log.debug("deleted = false");
URI projectUri = new URI(uri);
ClasspathData classpath = new ClasspathData(event.name, event.classpath.getEntries(), event.classpath.getJavaVersion());
IJavaProject oldProject = table.get(uri);
log.warn("Deleted project not removed because uri {} not found in {}", uri, table.keySet());
}
} else {
log.debug("deleted = false");
URI projectUri = new URI(uri);
ClasspathData classpath = new ClasspathData(event.name, event.classpath.getEntries(), event.classpath.getJavaVersion());
IJavaProject oldProject, newProject;
synchronized(table) {
oldProject = table.get(uri);
if (oldProject != null && classpath.equals(oldProject.getClasspath())) {
// nothing has changed
return;
}
IProjectBuild projectBuild = from(event.projectBuild);
IJavaProject newProject = IS_JANDEX_INDEX
IProjectBuild projectBuild = from(event.projectBuild);
newProject = IS_JANDEX_INDEX
? new JavaProject(getFileObserver(), projectUri, classpath,
JdtLsProjectCache.this, projectBuild)
: new JdtLsJavaProject(server.getClient(), projectUri, classpath, JdtLsProjectCache.this, projectBuild);
table.put(uri, newProject);
if (oldProject != null) {
notifyChanged(newProject);
} else {
notifyCreated(newProject);
}
}
// Notify outside of the lock
if (oldProject != null) {
notifyChanged(newProject);
} else {
notifyCreated(newProject);
}
}
} catch (Exception e) {