Skip to content

fix(module): attribute classes to the module that actually provides them - #5352

Merged
Cervator merged 7 commits into
MovingBlocks:developfrom
SiliconSaga:fix/module-attribution-in-tests
Aug 1, 2026
Merged

fix(module): attribute classes to the module that actually provides them#5352
Cervator merged 7 commits into
MovingBlocks:developfrom
SiliconSaga:fix/module-attribution-in-tests

Conversation

@agent-refr

@agent-refr agent-refr commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

AI-assisted change proposal. Filed by agent driven by @Cervator via GDD.

Module integration tests have been failing for any module whose dependencies carry Java code, with VerifyException: Environment has no module for <X> thrown from EntitySystemSetupUtil.registerEvents during Engines.createHost — before any test body runs. SimpleFarming was 7/7 failing, BlockPicker identically; Health, whose only dependency is code-free, passed throughout.

There are two independent defects here, three years apart.

The regression

ModuleManager.loadAndConfigureEngineModule built the engine module's class index with:

packageClassIndex.add(UrlClassIndex.byClassLoader(aClass.getClassLoader()));

That is the app class loader, so the index claimed every META-INF/subtypes entry on the entire classpath — including every module jar. The engine module's class predicate, a few lines further down, still correctly refused to own those classes. Anything falling in that gap gets discovered by getSubtypesOf and then has no module to attribute it to.

It surfaces in module-land specifically because that is where module jars sit on the classpath while StateHeadlessSetup.init runs initEntityAndComponentManagers() — which happens before the game manifest and the handoff to StateLoading, against a pre-game environment containing only engine and unittest.

Fixed by indexing only the classpath entry each class actually came from, which is what the predicate already matches on.

Introduced by the Gestalt 8 upgrade (#5267).

The latent bug

ClasspathCompromisingModuleFactory.ClassesInModule compared URLs. The same jar has two valid spellings — file:...jar as a code source, jar:file:...jar!/ as a classpath root — and those differ in both protocol and file component, so a class served from a module's own jar never matched its own module. (URL.equals is worth avoiding here regardless: it compares components and resolves host names to do it, making it a blocking call.)

Fixed by comparing normalised Paths.

This has been latent since 8aef6cc (2021-04-16), whose title is "allow code from either class or jars of module directories" and which left @Disabled("TODO: need a jar module alongside a classes directory") on the test that would have caught it — in the same commit. That test is now implemented. It builds the build/classes + build/libs/foo.jar layout, loads the class through an isolated URLClassLoader, and asserts attribution. It fails against the 2021 code, verified by reverting the file and re-running.

Fixing the predicate alone does not fix the regression; that was tested in isolation. Both changes are needed.

Diagnostics

Environment has no module for GetItemTooltip named neither where the class was loaded from nor which modules were actually asked, and both are invisible at the point of failure. Reconstructing them by hand took most of a debugging session. The message now carries them, on the failure path only.

docs/Engine-Testing-Patterns.md gains a Module Attribution Failures section covering the index-vs-predicate split, the pre-game environment, and the trap that instrumenting the class predicate tells you nothing when the module isn't in the environment — which reads like broken logging rather than a code path that never ran. It also documents why unittest:empty is unusable for any test that spawns a player.

Also here

Two things found while chasing this, both small and both in the same area:

setBlock says when it discards a write. It returns null when the target chunk is not loaded — documented, and universally ignored. The write silently does not happen, later reads come back as engine:unloaded, and the failure surfaces somewhere unrelated. Found via ItemPipes, which was losing 756 writes per test run with nothing in the log about it. Warns at the point of loss, capped at five positions each from ten chunks, and says so when it reaches either cap.

MTE restores the CoreRegistry context on teardown. Engines.setup() replaces the process-global context with a wrapper parented on the previous one and never puts it back, so a test class accumulates a chain of contexts belonging to shut-down engines. This is hygiene rather than a fix for anything observed — it was written to test a hypothesis about chunk generation stalling, which it disproved — but symmetric teardown of global state is worth having.

Verification

Suite Result
SimpleFarming:integrationTest 7/7 pass (was 0/7)
BlockPicker:integrationTest 2/2 pass
Health:integrationTest 16 pass, 1 pre-existing skip
engine-tests integrationenvironment.* 44 pass
engine-tests core.module.* 15 pass, 1 pre-existing skip

Module-side PRs for SimpleFarming and ItemPipes (both requesting the facet-free unittest:empty world generator) are held until this merges, since their tests cannot pass without it.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 39b2ed8c-1f38-4cb7-8b7c-7801306176be

📥 Commits

Reviewing files that changed from the base of the PR and between 304baa5 and af36b3b.

📒 Files selected for processing (5)
  • engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java
  • engine/src/main/java/org/terasology/engine/core/bootstrap/EntitySystemSetupUtil.java
  • engine/src/main/java/org/terasology/engine/core/module/ClasspathCompromisingModuleFactory.java
  • engine/src/main/java/org/terasology/engine/core/module/ModuleAttribution.java
  • engine/src/main/java/org/terasology/engine/world/internal/WorldProviderCoreImpl.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java
  • engine/src/main/java/org/terasology/engine/core/module/ClasspathCompromisingModuleFactory.java
  • engine/src/main/java/org/terasology/engine/world/internal/WorldProviderCoreImpl.java

📝 Walkthrough

Summary by CodeRabbit

  • Documentation

    • Added integration testing guidance for world generator selection and diagnosing module attribution failures.
  • Bug Fixes

    • Improved module-to-class matching across directory and archive locations.
    • Enhanced error details when classes cannot be attributed to a module.
    • Corrected engine classpath indexing for more reliable scanning.
  • Tests

    • Expanded coverage for classes loaded from packaged archives.
    • Improved isolation and cleanup between test runs.
  • Observability

    • Added rate-limited warnings for block writes targeting unloaded chunks.

Walkthrough

The PR documents engine testing patterns, improves diagnostics for unattributed event classes, changes module classpath matching to normalized filesystem paths, restores integration-test contexts, and logs discarded writes to unloaded chunks.

Changes

Engine diagnostics and test infrastructure

Layer / File(s) Summary
Classpath attribution and regression coverage
engine/src/main/java/org/terasology/engine/core/module/..., engine-tests/src/test/java/org/terasology/engine/core/module/...
Module predicates and engine indexing use normalized filesystem classpath entries, with an enabled test covering classes loaded from a generated JAR.
Attribution failure diagnostics
engine/src/main/java/org/terasology/engine/core/bootstrap/EntitySystemSetupUtil.java, engine/src/main/java/org/terasology/engine/core/module/ModuleAttribution.java, docs/Engine-Testing-Patterns.md
Unattributed event errors include class-source and module details, with documentation covering diagnosis and world-generator selection.
Integration environment context restoration
engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java
Setup saves the existing CoreRegistry context and teardown restores it, including when shutdown fails.
Discarded world-write logging
engine/src/main/java/org/terasology/engine/world/internal/WorldProviderCoreImpl.java
Writes to unloaded chunks emit rate-limited warnings while retaining existing result handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: Type: Bug

Suggested reviewers: benjaminamos, cervator

Poem

A rabbit hops where classpaths meet,
And jars make module tests complete.
Old contexts rest, lost writes are heard,
Clear errors bloom in every word.
Hop hop—the engine’s paths are bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: correcting module class attribution.
Description check ✅ Passed The description directly explains the attribution fixes, related regressions, tests, diagnostics, and supporting changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes incorrect module attribution for Java classes discovered during early engine/module integration test startup by ensuring classes are indexed and attributed to the module that actually provides them. It also improves diagnostics and adds coverage/documentation for diagnosing similar attribution failures.

Changes:

  • Fix engine module class indexing to index only the specific classpath entry a class originates from (instead of the whole classloader).
  • Fix module class ownership checks to compare normalized filesystem Paths (via code source) rather than URL strings.
  • Add a regression test for directory modules whose classes are served from a jar, plus improved failure diagnostics and documentation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java Restricts engine module indexing to the originating classpath entry to avoid “indexed but unowned” classes.
engine/src/main/java/org/terasology/engine/core/module/ClasspathCompromisingModuleFactory.java Switches class ownership checks from URL-equality to normalized filesystem path comparison.
engine/src/main/java/org/terasology/engine/core/bootstrap/EntitySystemSetupUtil.java Improves attribution failure diagnostics for event registration.
engine-tests/src/test/java/org/terasology/engine/core/module/ClasspathCompromisingModuleFactoryTest.java Adds a test covering attribution when a directory module’s classes are loaded from a jar.
docs/Engine-Testing-Patterns.md Documents module attribution failure modes and diagnostic approach for integration tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 155 to 159
for (Class<? extends Event> type : environment.getSubtypesOf(Event.class)) {
if (type.getAnnotation(DoNotAutoRegister.class) == null) {
Name module = verifyNotNull(environment.getModuleProviding(type),
"Environment has no module for %s", type.getSimpleName());
"Environment has no module for %s", unattributedClassReport(type, environment));
eventSystem.registerEvent(new ResourceUrn(module.toString(), type.getSimpleName()), type);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
engine/src/main/java/org/terasology/engine/core/bootstrap/EntitySystemSetupUtil.java (1)

154-162: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Build the diagnostic report only when event registration fails.

the unattributedClassReport(type, environment) argument to verifyNotNull is evaluated before the call, so getProtectionDomain()/getCodeSource() and the ordered module-id list are rebuilt for every successful event subtype registration. Use an explicit null check so the expensive report is computed only when getModuleProviding(type) is actually null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@engine/src/main/java/org/terasology/engine/core/bootstrap/EntitySystemSetupUtil.java`
around lines 154 - 162, Update registerEvents to store the result of
environment.getModuleProviding(type) and explicitly check it for null before
registration. Only when it is null should you call unattributedClassReport(type,
environment) and raise the existing failure with the same diagnostic message;
otherwise reuse the resolved module for ResourceUrn creation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/Engine-Testing-Patterns.md`:
- Around line 277-280: Specify the fenced code block language as text for the
VerifyException example in the EntitySystemSetupUtil.registerEvents
documentation snippet.
- Around line 327-329: Update the classpath equality guidance in
Engine-Testing-Patterns to use java.net.URI for non-blocking comparisons instead
of java.net.URL.equals, while retaining the recommendation to normalize and
compare code sources as Path values rather than comparing URL spellings.

---

Outside diff comments:
In
`@engine/src/main/java/org/terasology/engine/core/bootstrap/EntitySystemSetupUtil.java`:
- Around line 154-162: Update registerEvents to store the result of
environment.getModuleProviding(type) and explicitly check it for null before
registration. Only when it is null should you call unattributedClassReport(type,
environment) and raise the existing failure with the same diagnostic message;
otherwise reuse the resolved module for ResourceUrn creation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0531db2-88d6-46aa-a746-a060194e3555

📥 Commits

Reviewing files that changed from the base of the PR and between 6187314 and 45ba830.

📒 Files selected for processing (5)
  • docs/Engine-Testing-Patterns.md
  • engine-tests/src/test/java/org/terasology/engine/core/module/ClasspathCompromisingModuleFactoryTest.java
  • engine/src/main/java/org/terasology/engine/core/bootstrap/EntitySystemSetupUtil.java
  • engine/src/main/java/org/terasology/engine/core/module/ClasspathCompromisingModuleFactory.java
  • engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java

Comment thread docs/Engine-Testing-Patterns.md Outdated
Comment thread docs/Engine-Testing-Patterns.md Outdated
soloturn
soloturn previously approved these changes Jul 30, 2026
Cervator and others added 6 commits July 30, 2026 13:47
Module integration tests have been failing with `VerifyException: Environment has no module for <X>`, where X is any event class from a module dependency. Two independent defects.

The engine module's class index was built with `UrlClassIndex.byClassLoader(aClass.getClassLoader())`. That is the app class loader, so the index claimed every `META-INF/subtypes` entry on the whole classpath — including every module jar — while the engine module's class predicate still (correctly) refused to own those classes. `registerEvents` runs during `StateHeadlessSetup`, against the pre-game environment that holds only `engine` and `unittest`, so a module's event class was discovered there with no module able to claim it. Index only the classpath entry each class came from, which is what the predicate already matches on. This arrived with the Gestalt 8 upgrade (MovingBlocks#5267) and only bites where module jars are on the classpath, which is why it reads as a module-land-only failure.

`ClassesInModule` compared `URL`s. A jar has two valid spellings — `file:...jar` as a code source, `jar:file:...jar!/` as a classpath root — and URL equality is exact-string over them, so a class served from a module's own jar never matched its module. Compare normalised paths instead. This has been latent since 8aef6cc (2021), whose verifying test was left `@Disabled` in the same commit; that test is now implemented and passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pXw6g6JWNGP6eAyDJ1Cf8
`Environment has no module for GetItemTooltip` names neither where the class was loaded from nor which modules were actually asked, and both are invisible at the point of failure. Reconstructing them took most of a debugging session; the message now carries them.

Also documents module attribution in Engine-Testing-Patterns, including the pre-game environment that `registerEvents` runs against, and the world generator choice that makes `unittest:empty` unusable for any test that spawns a player.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pXw6g6JWNGP6eAyDJ1Cf8
`setBlock` returns null when the target chunk is not loaded, which is documented but easy to ignore — and every caller in practice does ignore it. The write silently does not happen, later reads come back as `engine:unloaded`, and the failure surfaces somewhere else entirely with nothing connecting it to the lost write.

Found via ItemPipes' integration tests, where a `@BeforeEach` clearing an 11x11x11 region lost 756 writes into chunks that were never loaded. Nothing in the logs mentioned it; the tests instead failed much later on pipe connection counts.

Warn at the point of loss, with the position, the block, and the chunk. Reported for the first three positions per chunk — a caller writing a region into missing chunks loses thousands of blocks at a time, so the log has to stay a diagnostic rather than become the failure. Bounded to 256 chunks so a long session cannot accumulate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pXw6g6JWNGP6eAyDJ1Cf8
Engines.setup() replaces the process-global CoreRegistry context with a wrapper whose parent is whatever was installed before, and teardown never puts the old one back. Across a test class that accumulates a chain of contexts belonging to shut-down engines, and lookups that miss on the newest can still resolve to them.

Captured before createHost, because bringing up the host installs its own context.

This is hygiene, not a fix for anything observed: it was written to test whether stale contexts explained ItemPipes' chunk generation stalling in every test after the first, and it did not — that failure is unchanged. Keeping it because symmetric teardown of global state is worth having on its own, and the 44 integrationenvironment tests pass with it.

Note the module-side MTE (org.terasology.moduletestingenvironment) does not share this shape: it calls CoreRegistry.put(GameEngine.class, host) rather than replacing the context, so it has no chain to accumulate. The two variants have drifted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pXw6g6JWNGP6eAyDJ1Cf8
Reporting three positions each from up to 256 chunks is still hundreds of lines describing one bug. Five positions from at most ten chunks says the same thing.

Both limits now announce themselves rather than going quiet, so a log that has stopped complaining is not mistaken for a problem that has stopped happening: "Reporting limit reached - further discarded writes are silent. Fix the underlying cause until this message stops appearing."

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pXw6g6JWNGP6eAyDJ1Cf8
Build the attribution report only on failure. As an argument to verifyNotNull it was evaluated for every event, so a protection-domain lookup and the ordered module-id list ran once per registered event subtype at startup. Reported independently by Copilot and CodeRabbit; both were right.

Correct the claim that URL equality is exact-string. It is not — URL.equals compares protocol, host, port and file components, and resolves host names to do it, which also makes it a blocking call. The conclusion still holds, because `file:...jar` and `jar:file:...jar!/` differ in protocol and file, but the stated reason was wrong in both the code comment and the docs. The docs now point at java.net.URI for cases where a URL comparison really is wanted.

Also gives the exception example in the docs a language tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pXw6g6JWNGP6eAyDJ1Cf8

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java`:
- Around line 132-136: Move the CoreRegistry restoration and contextBeforeSetup
clearing into a finally block surrounding the engine shutdown and cleanup
operations. Ensure CoreRegistry.setContext(contextBeforeSetup) executes and
contextBeforeSetup is cleared even when shutdown() or cleanup() throws, while
preserving the existing cleanup flow.

In
`@engine/src/main/java/org/terasology/engine/core/module/ClasspathCompromisingModuleFactory.java`:
- Around line 190-193: Update the location-resolution logic in
ClasspathCompromisingModuleFactory to catch FileSystemNotFoundException
alongside the existing URI and argument failures, returning null for unavailable
filesystem providers so attribution continues without aborting.

In
`@engine/src/main/java/org/terasology/engine/world/internal/WorldProviderCoreImpl.java`:
- Around line 231-246: Update logDroppedWrite so checking the global
DROPPED_WRITE_CHUNK_REPORT_LIMIT and inserting a new chunk entry in
droppedWritesPerChunk occur atomically, preventing concurrent new keys from
exceeding the limit. Preserve the existing warning announcement and per-chunk
counter behavior for existing entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a0030d0-5f2b-43ac-988e-36578509c4a3

📥 Commits

Reviewing files that changed from the base of the PR and between 45ba830 and 304baa5.

📒 Files selected for processing (7)
  • docs/Engine-Testing-Patterns.md
  • engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java
  • engine-tests/src/test/java/org/terasology/engine/core/module/ClasspathCompromisingModuleFactoryTest.java
  • engine/src/main/java/org/terasology/engine/core/bootstrap/EntitySystemSetupUtil.java
  • engine/src/main/java/org/terasology/engine/core/module/ClasspathCompromisingModuleFactory.java
  • engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java
  • engine/src/main/java/org/terasology/engine/world/internal/WorldProviderCoreImpl.java

@Cervator Cervator self-assigned this Jul 30, 2026
Name module = verifyNotNull(environment.getModuleProviding(type),
"Environment has no module for %s", type.getSimpleName());
Name module = environment.getModuleProviding(type);
if (module == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seem to be a few other critical call sites in the codebase that call ModuleEnvironment#getModuleProviding (or equivalent). Would it make sense to add logging upon failure is those areas as well?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed these deserve the same treatment. Doing it as a follow-up PR rather than here - this one has grown a couple of times already, and the report helper needed extracting first (now ModuleAttribution.describeUnattributedClass, per your other comment), which turns the follow-up into a mechanical change instead of a refactor bundled with behaviour.

Comment on lines +248 to +251
int seen = dropped.incrementAndGet();
if (seen > DROPPED_WRITE_SAMPLES_PER_CHUNK) {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any point at which dropped is reset? It is possible that the messages might be missed during a longer testing session if it happens briefly mid-test. Perhaps you could reset it after a certain amount of real time has elapsed (for example, 30s)?

I am assuming here that WorldProviderCoreImpl is only instantiated once during game load and is not a transient object.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Your assumption checks out - WorldProviderCoreImpl is constructed in InitialiseWorld/InitialiseRemoteWorld, so it lasts for the whole world load rather than being transient. That makes it worse than the odd missed message: without a reset, one early burst spends the reporting budget for the rest of the session. Added a 30s quiet-period reset, so a later unrelated burst reports afresh.

From @BenjaminAmos:

- The attribution report moves out of EntitySystemSetupUtil into ModuleAttribution, alongside the rest of the module machinery. It was never specific to event registration, and the other call sites that want it are not in that class.
- `contextBeforeSetup` becomes `coreRegistryBeforeSetup`, so where it came from is obvious at the use site. Noted there that CoreRegistry is deprecated and anything read from it may be stale.
- The path-comparison comment loses the URL.equals digression and keeps the point: normalise so a classpath entry and a code source are comparable.
- The reporting-limit note is inlined rather than held in a constant.
- Discarded-write counters now reset after 30s of quiet. Checking the assumption behind that comment: WorldProviderCoreImpl is built in InitialiseWorld/InitialiseRemoteWorld, so it does last for the whole world load — which means without a reset a single early burst spent the budget for the rest of the session.

From CodeRabbit:

- Restore the CoreRegistry context in a finally block, so a throwing shutdown cannot leak it into the next test — the exact failure the restore exists to prevent.
- Catch FileSystemNotFoundException when resolving a code source, for schemes with no installed provider.

Still to come, as a follow-up: the same failure reporting at the other getModuleProviding call sites @BenjaminAmos listed. Extracting the helper here was the prerequisite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pXw6g6JWNGP6eAyDJ1Cf8

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@agent-refr

Copy link
Copy Markdown
Collaborator Author

Some detail on a separate MTE problem this work kept running into, in case it is useful — it is not part of this PR, but it shaped some of the choices here.

Chunk generation stops working once a JVM has built enough engines. Turning on ChunkRegionFuture at debug shows the shape clearly. Three test methods in one class, each building its own engine:

Test 1:  Got chunk 1/27 … 27/27   → completes in ~21s
Test 2:  Got chunk 1/27 …  7/27   → stops
Test 3:  Got chunk 1/27 …  4/27   → stops

Delivery ceases rather than slowing, so it is not a timeout-tuning problem — raising the game-time budget from 30s to 120s changed nothing except how long the failure took to arrive.

It is not module-only. engine-tests' own ChunkRegionFutureTest fails the same way (UncheckedTimeoutException out of MainLoop.runUntil) when it runs late in the full suite, and passes on its own. It is also intermittent — that suite passed 44/44 twice before failing once.

Ruled out so far:

  • Stale CoreRegistry contexts. Restoring the context on teardown (the change in this PR) was written specifically to test this hypothesis, and it does not fix the stall. It is kept here only as teardown hygiene, not as a fix.
  • A shared executor. ChunkProcessingPipeline builds its own ThreadPoolExecutor per instance, and StateIngame.dispose() does reach chunkProvider.dispose().

Still suspicious, unproven:

  • ChunkMonitor is entirely static — an EventBus whose listeners are never unregistered, plus a CHUNKS map that fireChunkProviderDisposed clears globally. Every engine leaves listeners behind it.
  • ChunkRegionFuture.addMargin rounds each dimension up to odd and then adds 2 * REQUIRED_CHUNK_MARGIN, so a 2×2×2 chunk request becomes a 5×5×5 = 125-chunk relevance region while the future waits on the 27 that survive removeMargin. The file already carries FIXMEs about this, including "Is the complete relevance region not actually loaded‽".

Next step when we pick this up is a thread dump of a stalled second engine — whether Chunk-Processing-Reactor is alive and whether the executor queue is draining should separate "nothing is asking for chunks" from "chunks are requested and never delivered". Planning that as its own change rather than widening this one.

In the meantime @TestInstance(TestInstance.Lifecycle.PER_CLASS) avoids it by not building the extra engines, which took ItemPipes' suite from 1/3 to 3/3. That is a workaround, not a fix.

If any of the above is obviously wrong to you, I would rather hear it before we spend more time down the ChunkMonitor path.

@Cervator

Copy link
Copy Markdown
Member

Replying to my own agent (oh the times we live in): I wonder if #5348 might relate to the chunk mystery - going to have a different workspace investigate.

@agent-refr

Copy link
Copy Markdown
Collaborator Author

Cross-checking this from a second machine of ours. The numbers above came from Phoenix, an older and weaker Win10 laptop; everything below is from Dionysus, an aged but considerably beefier Win10 desktop running a 55-module workspace. Four things to add, one of which changes how the 27/7/4 evidence should be read.

1. The counter that produced those numbers is measuring two different regions.

ChunkRegionFuture.create runs the requested size through addMargin and registers the relevance listener on that padded region — desiredSize |= 1, then + 2 * REQUIRED_CHUNK_MARGIN — so a 2x2x2 request becomes 5x5x5 = 125. chunks is then removeMargin(...) of it, the inner 3x3x3 = 27. But onChunkRelevant adds every relevant chunk, margin included, and compares that running total against 27:

loadedChunks.add(chunk);
if (chunks.isValid()) {
    logger.debug("Got chunk {} / {}", loadedChunks.size(), chunks.volume());

So "7/27" is 7 arrivals somewhere across a 125-chunk region, not 7 of the 27 that were asked for. And in the other direction, the future could resolve as soon as any 27 padding chunks arrived, before the requested chunks existed at all.

#5348 fixes exactly this — an early return when !chunks.contains(chunk.getPosition()). Practical consequence for this investigation: that debug line is not a clean progress meter today, and it is worth re-baselining on top of #5348 before spending more on the ChunkMonitor path, because the numbers mean something different (and stricter) afterwards.

2. #5348 makes MTE wait harder, so it may surface more stalls rather than fewer. Anything that passed because the future resolved early on padding now genuinely waits for all 27. Worth expecting when re-running module suites against it.

3. Could not reproduce the stall here today. Full :engine-tests:test on develop: 0 failures, 10m07s. Same suite on #5348: 13m30s, 5 failures — all one order-sensitive ModuleManagerTest @BeforeEach NPE that passes in isolation, nothing chunk-related. No [World generation timed out], no UncheckedTimeoutException in either. Since this box is meaningfully stronger than the one those numbers came from, a non-reproduction here is consistent with a resource-pressure component rather than a pure logic deadlock.

4. The pipeline deadlock is real, and it produces exactly the "delivery ceases" signature described here. Verified by cherry-picking #5348's diagnostic test (test-only, 181 lines) onto clean develop, so the test and the fix could be varied independently:

TestReporter metric develop + test only #5348
baseline_solo_region_ms 902 479
connect_8_clients_ms 51695 50482
concurrent_8_regions_ms 4135 4672
reload_200_chunks_with_entities_ms never — TimeoutException 61968

The first three phases match within noise; only the bulk reload differs. Without the fix, chunks blocked on neighbours nobody ever requested sit in chunkProcessingInfoMap forever — delivery stops partway rather than slowing down, which is the same shape as 27 -> 7 -> 4.

So before more ChunkMonitor archaeology, the highest-value move is simply re-running the module stall on top of #5348. If it clears, this was #5150 all along and the static-listener theory can be dropped.

A hypothesis I had and then disproved, recorded so nobody re-runs it. #5348 also raises the test JVM from -Xmx1024m to 4096m, because many MTE tests together hit Java heap space. That looked like a strong fit for "engines already built in this JVM", since it is the one change scaling with engine count. It does not hold up here: the failure above is a TimeoutException, not an OutOfMemoryError, and 9 engines connected fine at the old 1024m on this box. Heap is worth keeping as a cheap sanity check on a smaller machine, but it is not what separates pass from fail in this scenario — the pipeline change is.

@Cervator Cervator left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have done more local review and testing including with a rebase, going to merge this as-is just to avoid another full cycle in Jenkins to confirm the already-confirmed.

@Cervator
Cervator merged commit d150b7d into MovingBlocks:develop Aug 1, 2026
12 checks passed
@Cervator
Cervator deleted the fix/module-attribution-in-tests branch August 1, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants