Summary
While investigating flaky MavenProjectMetadataFileTest failures (see #3840, which fixes one confirmed root cause), I found a second, deeper root cause affecting testMetadataFileSync and part of testDeleteClasspath when java.import.generatesMetadataFilesAtProjectRoot=false (the EFS metadata-redirect mode implemented in org.eclipse.jdt.ls.filesystem).
Symptom
After a classpath recovery flow (e.g. .classpath deleted and regenerated, or a Maven project reconfigured after a pom.xml change), reading the .classpath resource via the standard resource API fails or returns stale data:
org.eclipse.core.internal.resources.ResourceException(/quickstart2/.classpath)[368]: java.lang.Exception: Resource '/quickstart2/.classpath' does not exist.
at org.eclipse.core.internal.resources.Resource.checkAccessible(Resource.java:248)
at org.eclipse.core.internal.resources.File.getContents(File.java:366)
at org.eclipse.jdt.ls.core.internal.ResourceUtils.getContent(ResourceUtils.java:155)
at org.eclipse.jdt.ls.core.internal.filesystem.MavenProjectMetadataFileTest.testMetadataFileSync(MavenProjectMetadataFileTest.java:100)
and, in testDeleteClasspath:
org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
at org.eclipse.jdt.ls.core.internal.filesystem.MavenProjectMetadataFileTest.testDeleteClasspath(MavenProjectMetadataFileTest.java:144)
(assertTrue(dotClasspath.exists()) — the resource-tree reports the file does not exist, even after recovery.)
What I've proven via instrumented tracing
I built an instrumented eclipse.jdt.core (added System.out.println trace points in SetClasspathOperation#executeOperation and JavaProject#writeFileEntries) and temporarily swapped it into the p2-resolved test runtime for a local run. This confirms:
SetClasspathOperation#executeOperation is invoked during recovery, with canChangeResources=true.
JavaProject#writeFileEntries does detect the classpath changed (equalToOnDisk=false) and successfully calls setSharedProperty(...), which uses IFile#write(bytes, true, false, false, null) — a resource-API write, not a raw filesystem write.
writeAndCacheClasspath returns true ("file written") every time in the recovery flow.
In other words: the classpath write itself always succeeds, in-memory and (per JDT Core's own code) via the standard resource API. Yet the test's subsequent read via IFile#getContents()/IFile#exists() on the very same logical resource still fails or is stale.
Root cause candidates identified (not yet conclusively isolated)
-
JLSFsUtils.shouldStoreInMetadataArea() re-evaluates redirect eligibility on every access (org.eclipse.jdt.ls.filesystem/src/org/eclipse/jdt/ls/core/internal/filesystem/JLSFsUtils.java):
// do not redirect if the file already exists on disk
if (location.toFile().exists()) {
return false;
}
This check is timing-dependent: if a project-root .classpath transiently exists at read-time but not at write-time (or vice versa), reads and writes for the "same" logical file can resolve to two different physical files (project root vs. the metadata-area redirect target). I prototyped a fix that makes the decision "sticky" (prefers the metadata-area copy once it already has content) but reverted it after confirming — via repeated local runs — that it did not change the observed test outcomes, meaning this isn't the (sole) trigger for these two specific tests. It may still be worth hardening independently, but I didn't want to land an unverified change.
-
Resource-tree / IProject handle stability across the classpath-recovery flow. testDeleteClasspath captures IFile dotClasspath before triggering projectsManager.fileChanged(..., DELETED). If the recovery flow (ProjectUtils.removeJavaNatureAndBuilder() + reimport) closes/recreates the underlying IProject rather than mutating it in place, a stale IFile handle captured beforehand could report exists()==false even though a new .classpath legitimately exists for the (recreated) project. I was not able to conclusively confirm or rule this out without live debugging (breakpoints across eclipse.jdt.core, m2e-core, and eclipse.jdt.ls in a single running JVM).
-
Ruled out: this is not simple write skipping (canChangeResources=false) — traces prove writes always report success. Also ruled out: m2e-core's AbstractJavaProjectConfigurator.configure() calling the 3-arg setRawClasspath(...) overload — per JavaProject's own Javadoc this always passes canModifyResources=true, consistent with the trace evidence.
Reproduction
cd eclipse.jdt.ls
.\mvnw.cmd -o -pl org.eclipse.jdt.ls.tests -am integration-test "-Dtest=MavenProjectMetadataFileTest" "-DfailIfNoTests=false"
Reliably reproduces both failures locally (Windows, Java 25, Maven 3.9.12) both on main and with #3840 applied (expected — #3840 only fixes the separate stray-bin-folder race).
Suggested next steps for a maintainer with live-debugging access
- Set a breakpoint in
Resource.checkAccessible()/checkExists() for /quickstart2/.classpath and compare the resource-tree path/URI actually being queried against the path that SetClasspathOperation wrote to.
- Instrument or breakpoint
JLSFileSystem.getStore()/JLSFile.getChild()/getFileStore() at both the write call site and the test's read call site to see whether they compute the same redirected IFileStore location both times.
- Check whether
ProjectUtils.removeJavaNatureAndBuilder() (or m2e-core's project update flow) ever produces a new IProject proxy instance rather than mutating the existing one in place.
Scope note
This is a narrower, more precise follow-up to #3840; that PR contains only the confirmed, independently-verified fix for the stray-bin-folder race. This issue tracks the remaining, more subtle EFS-redirect/resource-tree synchronization problem, which needs interactive debugging (attaching a debugger across eclipse.jdt.core, m2e-core, and eclipse.jdt.ls in the same JVM) rather than static analysis or one-off trace instrumentation to pin down conclusively.
Summary
While investigating flaky
MavenProjectMetadataFileTestfailures (see #3840, which fixes one confirmed root cause), I found a second, deeper root cause affectingtestMetadataFileSyncand part oftestDeleteClasspathwhenjava.import.generatesMetadataFilesAtProjectRoot=false(the EFS metadata-redirect mode implemented inorg.eclipse.jdt.ls.filesystem).Symptom
After a classpath recovery flow (e.g.
.classpathdeleted and regenerated, or a Maven project reconfigured after apom.xmlchange), reading the.classpathresource via the standard resource API fails or returns stale data:and, in
testDeleteClasspath:(
assertTrue(dotClasspath.exists())— the resource-tree reports the file does not exist, even after recovery.)What I've proven via instrumented tracing
I built an instrumented
eclipse.jdt.core(addedSystem.out.printlntrace points inSetClasspathOperation#executeOperationandJavaProject#writeFileEntries) and temporarily swapped it into the p2-resolved test runtime for a local run. This confirms:SetClasspathOperation#executeOperationis invoked during recovery, withcanChangeResources=true.JavaProject#writeFileEntriesdoes detect the classpath changed (equalToOnDisk=false) and successfully callssetSharedProperty(...), which usesIFile#write(bytes, true, false, false, null)— a resource-API write, not a raw filesystem write.writeAndCacheClasspathreturnstrue("file written") every time in the recovery flow.In other words: the classpath write itself always succeeds, in-memory and (per JDT Core's own code) via the standard resource API. Yet the test's subsequent read via
IFile#getContents()/IFile#exists()on the very same logical resource still fails or is stale.Root cause candidates identified (not yet conclusively isolated)
JLSFsUtils.shouldStoreInMetadataArea()re-evaluates redirect eligibility on every access (org.eclipse.jdt.ls.filesystem/src/org/eclipse/jdt/ls/core/internal/filesystem/JLSFsUtils.java):This check is timing-dependent: if a project-root
.classpathtransiently exists at read-time but not at write-time (or vice versa), reads and writes for the "same" logical file can resolve to two different physical files (project root vs. the metadata-area redirect target). I prototyped a fix that makes the decision "sticky" (prefers the metadata-area copy once it already has content) but reverted it after confirming — via repeated local runs — that it did not change the observed test outcomes, meaning this isn't the (sole) trigger for these two specific tests. It may still be worth hardening independently, but I didn't want to land an unverified change.Resource-tree /
IProjecthandle stability across the classpath-recovery flow.testDeleteClasspathcapturesIFile dotClasspathbefore triggeringprojectsManager.fileChanged(..., DELETED). If the recovery flow (ProjectUtils.removeJavaNatureAndBuilder()+ reimport) closes/recreates the underlyingIProjectrather than mutating it in place, a staleIFilehandle captured beforehand could reportexists()==falseeven though a new.classpathlegitimately exists for the (recreated) project. I was not able to conclusively confirm or rule this out without live debugging (breakpoints acrosseclipse.jdt.core,m2e-core, andeclipse.jdt.lsin a single running JVM).Ruled out: this is not simple write skipping (
canChangeResources=false) — traces prove writes always report success. Also ruled out:m2e-core'sAbstractJavaProjectConfigurator.configure()calling the 3-argsetRawClasspath(...)overload — perJavaProject's own Javadoc this always passescanModifyResources=true, consistent with the trace evidence.Reproduction
Reliably reproduces both failures locally (Windows, Java 25, Maven 3.9.12) both on
mainand with #3840 applied (expected — #3840 only fixes the separate stray-bin-folder race).Suggested next steps for a maintainer with live-debugging access
Resource.checkAccessible()/checkExists()for/quickstart2/.classpathand compare the resource-tree path/URI actually being queried against the path thatSetClasspathOperationwrote to.JLSFileSystem.getStore()/JLSFile.getChild()/getFileStore()at both the write call site and the test's read call site to see whether they compute the same redirectedIFileStorelocation both times.ProjectUtils.removeJavaNatureAndBuilder()(orm2e-core's project update flow) ever produces a newIProjectproxy instance rather than mutating the existing one in place.Scope note
This is a narrower, more precise follow-up to #3840; that PR contains only the confirmed, independently-verified fix for the stray-
bin-folder race. This issue tracks the remaining, more subtle EFS-redirect/resource-tree synchronization problem, which needs interactive debugging (attaching a debugger acrosseclipse.jdt.core,m2e-core, andeclipse.jdt.lsin the same JVM) rather than static analysis or one-off trace instrumentation to pin down conclusively.