Summary
In a multi-module Maven reactor where several modules bind metaobjects-maven-plugin at generate-sources (goal generate), running a parallel build (mvn -T<N>, e.g. -T1C / -T4) deadlocks / hangs. The serial default (-T1) always works. This forces consumers to build serially and forfeit multi-core parallelism — a meaningful wall-clock cost on large reactors (10+ modules).
Version: metaobjects-maven-plugin 7.8.0 · Maven 3.9.x · JDK 21.
Root cause (surface)
Two compounding issues:
-
The mojos are not declared thread-safe. MetaDataGeneratorMojo (@Mojo(name="generate", …)), MetaDataVerifyMojo, and DocsMojo have no threadSafe = true on @Mojo. Under -T, Maven runs multiple modules' executions concurrently in one JVM.
-
The generate/load path mutates JVM-process-global static singletons rather than per-MetaDataLoader / per-execution state. Concurrent executions from different reactor modules contend on several independently-locked globals, which is a classic lock-ordering deadlock (and a shared-mutable-state correctness hazard):
registry/MetaDataRegistry — private static volatile instance + INSTANCE_LOCK. RegistryManifest documents this as "the process-global MetaDataRegistry.getInstance() singleton."
registry/ObjectClassRegistry — static volatile globalInstance, initialized under synchronized (ObjectClassRegistry.class).
registry/CoreTypeInitializer — static boolean initialized, public static synchronized initializeCoreTypes().
registry/ServiceRegistryFactory — static volatile defaultInstance + static LOCK.
registry/RegistryManifest — static volatile defaultLoaderRegistry + static DEFAULT_LOCK.
Because core-type initialization + type registration walk across these separately-locked globals, two threads initializing/registering at once can acquire the locks in different orders and deadlock.
Reproduction
- A reactor with 2+ modules that each run
metaobjects:generate at generate-sources.
mvn -T1C clean install (or -T4).
- The build hangs; a thread dump shows worker threads blocked in registry initialization / type registration on the static locks above.
mvn (serial) or mvn -T1 always completes.
Impact
Consumers must pin serial builds (-T1). On a 10-module reactor this leaves most cores idle for the whole build.
Suggested remediation (maintainers' call)
- Preferred: scope the registries to the
MetaDataLoader / execution instance instead of JVM-global statics, so each module's mojo run is isolated. This also aligns with the "loaded model is read-only / no global mutation" doctrine.
- If a process-global registry must persist across executions, make initialization fully idempotent + thread-safe under a single consistent lock (one registry lock, one acquisition order) so concurrent
generate runs cannot deadlock.
- Only after (1) or (2), mark the mojos
@Mojo(threadSafe = true). Declaring threadSafe without fixing the shared state would be worse — it silences Maven's warning while the deadlock remains.
- Until fixed, document that consumers must build with
-T1 (no parallel reactor).
Happy to test a candidate fix against a large multi-module reactor.
Summary
In a multi-module Maven reactor where several modules bind
metaobjects-maven-pluginatgenerate-sources(goalgenerate), running a parallel build (mvn -T<N>, e.g.-T1C/-T4) deadlocks / hangs. The serial default (-T1) always works. This forces consumers to build serially and forfeit multi-core parallelism — a meaningful wall-clock cost on large reactors (10+ modules).Version:
metaobjects-maven-plugin7.8.0 · Maven 3.9.x · JDK 21.Root cause (surface)
Two compounding issues:
The mojos are not declared thread-safe.
MetaDataGeneratorMojo(@Mojo(name="generate", …)),MetaDataVerifyMojo, andDocsMojohave nothreadSafe = trueon@Mojo. Under-T, Maven runs multiple modules' executions concurrently in one JVM.The generate/load path mutates JVM-process-global static singletons rather than per-
MetaDataLoader/ per-execution state. Concurrent executions from different reactor modules contend on several independently-locked globals, which is a classic lock-ordering deadlock (and a shared-mutable-state correctness hazard):registry/MetaDataRegistry—private static volatile instance+INSTANCE_LOCK.RegistryManifestdocuments this as "the process-globalMetaDataRegistry.getInstance()singleton."registry/ObjectClassRegistry—static volatile globalInstance, initialized undersynchronized (ObjectClassRegistry.class).registry/CoreTypeInitializer—static boolean initialized,public static synchronized initializeCoreTypes().registry/ServiceRegistryFactory—static volatile defaultInstance+ staticLOCK.registry/RegistryManifest—static volatile defaultLoaderRegistry+ staticDEFAULT_LOCK.Because core-type initialization + type registration walk across these separately-locked globals, two threads initializing/registering at once can acquire the locks in different orders and deadlock.
Reproduction
metaobjects:generateatgenerate-sources.mvn -T1C clean install(or-T4).mvn(serial) ormvn -T1always completes.Impact
Consumers must pin serial builds (
-T1). On a 10-module reactor this leaves most cores idle for the whole build.Suggested remediation (maintainers' call)
MetaDataLoader/ execution instance instead of JVM-global statics, so each module's mojo run is isolated. This also aligns with the "loaded model is read-only / no global mutation" doctrine.generateruns cannot deadlock.@Mojo(threadSafe = true). DeclaringthreadSafewithout fixing the shared state would be worse — it silences Maven's warning while the deadlock remains.-T1(no parallel reactor).Happy to test a candidate fix against a large multi-module reactor.