Skip to content

maven-plugin: generate/verify/docs mojos deadlock under parallel builds (mvn -T) — process-global registry singletons, not thread-safe #233

Description

@dmealing

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:

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

  2. 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/MetaDataRegistryprivate static volatile instance + INSTANCE_LOCK. RegistryManifest documents this as "the process-global MetaDataRegistry.getInstance() singleton."
    • registry/ObjectClassRegistrystatic volatile globalInstance, initialized under synchronized (ObjectClassRegistry.class).
    • registry/CoreTypeInitializerstatic boolean initialized, public static synchronized initializeCoreTypes().
    • registry/ServiceRegistryFactorystatic volatile defaultInstance + static LOCK.
    • registry/RegistryManifeststatic 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

  1. A reactor with 2+ modules that each run metaobjects:generate at generate-sources.
  2. mvn -T1C clean install (or -T4).
  3. The build hangs; a thread dump shows worker threads blocked in registry initialization / type registration on the static locks above.
  4. 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)

  1. 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.
  2. 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.
  3. 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.
  4. Until fixed, document that consumers must build with -T1 (no parallel reactor).

Happy to test a candidate fix against a large multi-module reactor.

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