Skip to content

Fix ConcurrentModificationException in parallel builds#284

Closed
wisechengyi wants to merge 1 commit intonebula-plugins:mainfrom
wisechengyi:conccurrentmod
Closed

Fix ConcurrentModificationException in parallel builds#284
wisechengyi wants to merge 1 commit intonebula-plugins:mainfrom
wisechengyi:conccurrentmod

Conversation

@wisechengyi
Copy link
Collaborator

@wisechengyi wisechengyi commented Nov 18, 2025

Fix ConcurrentModificationException in parallel builds

Problem

When running Gradle commands with parallel execution enabled (or even single commands like ./gradlew help), the build fails with a ConcurrentModificationException:

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':XXX:saveLock'.
> Could not create task ':saveGlobalLock'.
   > java.util.ConcurrentModificationException (no error message)

This issue occurs even when saveGlobalLock is not scheduled to run, affecting basic Gradle operations. The problem is particularly severe with Gradle 9.0 due to stricter concurrency enforcement.

Root Cause

The issue stems from lazy task realization being triggered during parallel task execution:

  1. Configuration Phase: Tasks are registered lazily using tasks.register(), but their properties aren't fully evaluated until accessed.

  2. Execution Phase: When subproject saveLock tasks execute, their doFirst actions perform validation checks:

    SaveLockTask globalSave = project.rootProject.tasks.findByName(SAVE_GLOBAL_LOCK_TASK_NAME)
    if (globalSave && globalSave.outputLock && globalSave.outputLock.exists()) {
        throw new GradleException('...')
    }
  3. Lazy Realization Trigger: Accessing globalSave.outputLock triggers evaluation of the task's convention mappings, which contains this problematic code:

    configurations = {
        project.allprojects.each { p->
            def conf = p.configurations.create("globalLockConfig${System.currentTimeMillis()}") {
                // ...
            }
        }
    }
  4. Concurrent Modification: When multiple subproject tasks run in parallel, they all:

    • Look up the same saveGlobalLock task
    • Trigger the same lazy property evaluation
    • Try to create configurations on the same projects concurrently
    • Result: ConcurrentModificationException

The same issue exists in reverse: saveGlobalLock looks up subproject tasks during execution, causing similar problems.

Solution

Replace runtime task lookups with direct file system checks to avoid triggering lazy task realization:

In configureSaveTask (DependencyLockTaskConfigurer.groovy:157-167)

Before:

SaveLockTask globalSave = project.rootProject.tasks.findByName(SAVE_GLOBAL_LOCK_TASK_NAME)
if (globalSave && globalSave.outputLock && globalSave.outputLock.exists()) {
    throw new GradleException('...')
}

After:

def rootExtension = project.rootProject.extensions.findByType(DependencyLockExtension)
if (rootExtension) {
    File globalLockFile = new File(project.rootProject.projectDir, lockFilename ?: rootExtension.globalLockFile)
    if (globalLockFile.exists()) {
        throw new GradleException('...')
    }
}

In configureGlobalSaveTask (DependencyLockTaskConfigurer.groovy:195-207)

Before:

project.subprojects.each { Project sub ->
    SaveLockTask save = sub.tasks.findByName(SAVE_LOCK_TASK_NAME)
    if (save && save.outputLock?.exists()) {
        throw new GradleException('...')
    }
}

After:

project.subprojects.each { Project sub ->
    def subExtension = sub.extensions.findByType(DependencyLockExtension)
    if (subExtension) {
        File subLockFile = new File(sub.projectDir, lockFilename ?: subExtension.lockFile)
        if (subLockFile.exists()) {
            throw new GradleException("Cannot save global lock, one or more individual locks are in place (found: ${subLockFile}), run deleteLock task")
        }
    }
}

Benefits

  • ✅ Eliminates concurrent access to task collections
  • ✅ Avoids triggering lazy task realization during parallel execution
  • ✅ Compatible with Gradle 9.0's stricter concurrency model
  • ✅ Removes deprecated DeprecationLogger.whileDisabled usage
  • ✅ Provides more informative error messages (shows which lock file was found)
  • ✅ Functionally equivalent to original validation logic

The fix ensures that validation checks directly inspect the file system rather than accessing task objects, preventing race conditions in parallel builds.

@wisechengyi wisechengyi changed the title fix Fix ConcurrentModificationException in parallel builds Nov 18, 2025
@wisechengyi
Copy link
Collaborator Author

Close in favor of #285

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.

1 participant