Is always getting same instance of the task when do `project.tasks.getByName(taskName)`?

Is always getting same instance of the task when do project.tasks.getByName(taskName) ?
I would like to create a singleton task in the root project that will receive messages from other tasks and after receiving all messages perform some validation.

To start with: do not use getByName, that breaks task-configuration avoidance.
At least if done in configuration phase.
If done in execution phase it is not better though, as accessing project at execution time is deprecated.

But to answer the actual question: maybe.
Theoretically a task could be replaced, even though it is very bad practice and highly discouraged.

But either way, maybe also here elaborate on your use-case, you probably have an XY problem here and there is most probably a better way to achieve whatever you try, but for that it must be clear what you try to achieve exactly.

I would like to collect all snapshots for every project and throw an exception if they are presented.
I collect SNAPSHOT with configuration.afterEvaluate in plugin into errors set, create a task within current project passing errors into constructor. Some code omitted…

class BuildChecksPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        val errors = mutableSetOf<String>()
        project.afterEvaluate {
                <collect snapshots async into errors with project.configurations.filter { canBeResolved(it) }.forEach { config ->  config.incoming.afterResolve { ... } >
                val finalizerTask = createOrGetFinalizerTaskIfNotCreated(project.rootProject)
                finalizerTask.addProject(project)

                val collectorTask = project.tasks.register(
                    SnapshotPresenceCollectorTask.NAME,
                    SnapshotPresenceCollectorTask::class.java,
                    finalizerTask,
                    errors
                ).get()

                val compileTask = getTaskSilently(project, "compileKotlin")?.finalizedBy(collectorTask)
                val compileTestKotlin = getTaskSilently(project, "compileTestKotlin")
                    ?.finalizedBy(collectorTask)
                // some projects don't have compileTestKotlin and compileKotlin tasks, but should be removed
                // from  finalizerTask 
                if (compileTask == null && compileTestKotlin == null) {
                    project.gradle.taskGraph.whenReady {
                        it.allTasks[it.allTasks.size - 1].doLast {
                            finalizerTask.addErrorsFromProject(project, errors)
                        }
                    }
                }
        }

       private fun createOrGetFinalizerTaskIfNotCreated(project: Project) =
        getTaskSilently(project, SnapshotPresenceFinalizerTask.NAME) as SnapshotPresenceFinalizerTask?
            ?: project.tasks.register(
                SnapshotPresenceFinalizerTask.NAME,
                SnapshotPresenceFinalizerTask::class.java
            ).get()
    }
//-------------------------------------------------------------------------
open class SnapshotPresenceFinalizerTask : DefaultTask() {
    private val errors = mutableSetOf<String>()
    private val projects = mutableSetOf<Project>()

    @TaskAction
    fun run() {
        <throw an exception, the method should not be invoked>
    }

    fun addProject(project: Project) {
        projects.add(project)
    }

    fun addErrorsFromProject(project: Project, prjErrors: MutableSet<String>) {
        this.errors += prjErrors
        this.projects.remove(project)
        if (this.projects.isEmpty()) {
            if (this.errors.isNotEmpty()) {
                <throw an exception>
            }
        }
    }
}
//-------------------------------------------------------------------------
open class SnapshotPresenceCollectorTask
@Inject constructor(
    private val finalizerTask: SnapshotPresenceFinalizerTask,
    private val errors: MutableSet<String>
) : DefaultTask() {

    @TaskAction
    fun run() {
        finalizerTask.addErrorsFromProject(project, errors)
    }
}

That’s actually an extremely bad idea.

afterEvaluate is highly discouraged. The main gain you get from using it are timing problems, ordering problems, and race conditions. It is like using SwingUtilities.invokeLater or Platform.runLater to “fix” a GUI problem. It just does symptom treatment, delaying the problem to a later, harder to reproduce, harder to debug, and harder to fix point in time.

Instead of the cursed afterEvaluate use proper lazy API, so for example project.configurations.matching { ... }.configureEach { ...afterResolve... }, then you don’t need to manually delay something. Actually can probably even leave out the filter / matching and just attach the afterResolve action to all configurations using project.configurations.configureEach { ... }, because I think if the collection is not resolvable it should be a no-op.

Doing cross-project configuration like registering a task in another project (like the root project), or reconfiguring a task in another project, or even just getting information from another project’s model are highly problematic and discouraged. You instantly introduce project coupling which works against or disables some of the more sophisticated features or optimizations.

Registering a task for not executing it, just for coordination / collection is an extreme abuse and pretty-much non-sense. For that you have shared build services. They are scoped to the current build execution, to that you can access from all projects safely and use it to collect and share the information you need.

Also adding a doLast { ... } to all tasks just to trigger one “after all finished” action is pretty bad.

Let alone the way you configure “all” tasks by using taskGraph .whenReady instead of just using the safe tasks.configureEach { ... }, besides that you should not do it anyway.

Also it makes little sense to use the task-configuration avoidance safe tasks.register if you immediately break task-configuration avoidance again by calling get() on it. If you would really immediately need the task instance, use tasks.create, but in practically all cases use neither.

Also, even if it would work somehow, your logic is flaky, as you output the errors as soon as the first task of the last project has finished executing, but you might miss resolutions happening in later tasks of that or other projects.


Ok, let’s stop the ranting, here is what you should do:

  • create a shared build service
  • make the shared build service implement Autocloseable
  • make the shared build service implement OperationCompletionListener
  • register the build service as OperationCompletionListener
  • in the close() method throw your exception with your findings
  • in the the afterResolve add the findings to a collector field in the shared build service
  • do not use any afterEvaluate or filter or forEach or custom tasks or modify any tasks

By having the shared build service being an OperationCompletionListener you make sure it is closed only between the last task was executed and the build being finished, the implementation of the OperationCompletionListener method can be empty and the constructor also does not need to do anything.