How to pass nested parameters to `org.gradle.workers.WorkAction`?

I have a task that accepts a nested property of configuration parameters, which is then needed in the work action executed in parallel. However, when running the task it fails with

Execution failed for task ‘:1.21.1:stonecutterPrepare’.

A failure occurred while executing dev.kikugie.stonecutter.process.SCPrepareAction
Could not isolate value dev.kikugie.stonecutter.process.SCPrepareAction$Parameters_Decorated@285b7228 of type SCPrepareAction.Parameters
Could not serialize value of type StonecutterBuildParameters

So what do I need to make an object passed to the work action serializable/isolatable?
Relevant code:

Task: SCPrepareTask.kt

public abstract class SCPrepareTask : DefaultTask() {
    @get:Nested
    public abstract val params: Property<StonecutterBuildParameters> 
    ...
}

private interface SCPrepareAction : WorkAction<SCPrepareAction.Parameters> {
    interface Parameters : WorkParameters {
        @get:Nested val params: Property<StonecutterBuildParameters>
        @get:Input val source: RegularFileProperty
        @get:Input val output: RegularFileProperty
    }
    ...
}

Parameters: StonecutterBuildParameters.kt

public abstract class StonecutterBuildParameters @Inject internal constructor(flags: StonecutterFlags, current: Version, factory: ProviderFactory) {
    @get:Input public abstract val constants: MapProperty<Identifier, Boolean>
    @get:Input public abstract val swaps: MapProperty<Identifier, String>
    @get:Input public abstract val dependencies: MapProperty<Identifier, Version>

    @get:Nested public abstract val stringReplacements: ListProperty<StringReplacementSpec>
    @get:Nested public abstract val regexReplacements: ListProperty<RegexReplacementSpec>

    // Supplies 'dependencies' with a provider transforming this map
    @get:Internal internal abstract val dummyDependencies: MapProperty<Identifier, Version>

    @get:Inject protected abstract val objects: ObjectFactory

    // Used to build string and regex replacements. Need to be properties to allow replacement graph validation.
    @get:Internal private val stringReplacementBuilder: ReplacementBuilder<StringReplacement>
    @get:Internal private val regexReplacementBuilder: ReplacementBuilder<RegexReplacement>
    ...
}

How do you get the error?
I’ve run all three testclasses where I found 1.21.1 and all were successful.

What is the full --stacktrace of the error?
Most probably it shows more information about what the actual problem is.

But I guess the problem are the two ReplacementBuilder properties, as they are no managed properties.

Btw. all the input annotations (@Input, @Nested, @Internal) should be pretty pointless on that class, unless you also use instances of that class as @Nested input on some task or artifact transform. Worker API actions are not individually up-to-dateable or cacheable afair.

I’ve uploaded the stacktrace log here: Execution failed for task ':1.21.8:stonecutterPrepare'.> A failure occurred wh - Pastebin.com
The code changed a bit to pass a data class instead of the object with properties, as that is what will be used by the file processor. The testing setup is Making sure you're not a bot! (should fail when using IntelliJ Sync).

From my understanding of the error, it needs every object to implement java.io.Serializable. If there’s no workaround, I can accept it being that. However it doesn’t make much sense because I run the worker without isolation, so doing something like this works (but I really don’t want to - it’s bodge of all bodges)

private val cache: MutableMap<Int, TransformParameters> = mutableMapOf()

public abstract class SCPrepareTask : DefaultTask() {
    @get:Nested
    public abstract val params: Property<StonecutterBuildParameters>

    @TaskAction
    public fun run(inputs: InputChanges) {
        val data = params.get().build()
        val key = Random.nextInt()
        cache[key] = data
        
        try {
            executor.execute {
                for (change in inputs.getFileChanges(source))
                    if (change.fileType != FileType.DIRECTORY) it.processFile(change, key)
            }
        } finally {
            cache.remove(key)
        }
    }

    private fun WorkQueue.processFile(change: FileChange, cacheKey: Int): Unit = submit(SCPrepareAction::class) {
        key.set(cacheKey)
        source.set(change.file)
        output.set(change.file.cacheFile())
    }
}

@OptIn(StonecutterInternalAPI::class)
private interface SCPrepareAction : WorkAction<SCPrepareAction.Parameters> {
    interface Parameters : WorkParameters {
        val key: Property<Int>
        val source: RegularFileProperty
        val output: RegularFileProperty
    }

    override fun execute() {
        val source: Path = parameters.source.asFile().toPath()
        val output: Path = parameters.output.asFile().toPath()
        val params = cache[parameters.key.get()]!! // OK...
    }
}

Afair they must be either Serializable or fully-managed types.

The JavaDoc of WorkParameters even advices explicitly:

Parameter types should be interfaces, only declaring getters for Property-like objects. Example:

 public interface MyParameters extends WorkParameters {
     Property<String> getStringParameter();
     ConfigurableFileCollection getFiles();
 }

Thank you for your response, though in documentation it only recommends using Property<T>-s, which may be worth noting.

data class ExampleClass(val str: String)

abstract class ExampleTask : DefaultTask() {
    // Is a Property-like getter, works as a task input with caching support
    abstract val example: Property<ExampleClass>
}

interface ExampleParameters : WorkParameters {
    // Is a Property-like getter, but is not supported
    val example: Property<ExampleClass>
}
data class ExampleClass(val str: String) : java.io.Serializable

interface ExampleParameters : WorkParameters {
    // Works fine when it implements java.io.Serializable
    val example: Property<ExampleClass>
}

Yeah, it is a bit blurry there.
But as I said, it most probably has to either be Serializable or fully-managed.
“fully-managed” means the types of the Propertys are also managed types, which includes primitives, and strings, and other fully managed types.

Feel free to open a documentation improvement request, I’m just a user like you. :slight_smile:

1 Like