Skip to content

Flaky MavenProjectMetadataFileTest (testMetadataFileSync/testDeleteClasspath): EFS-redirect/resource-tree sync gap with generatesMetadataFilesAtProjectRoot=false #3841

Description

@chagong

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)

  1. 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.

  2. 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).

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions