<https://github.com/avast/gradle-docker-compose-pl...
# community-support
m
https://github.com/avast/gradle-docker-compose-plugin/issues/486 Any insights into whether the problem is with the plugin or Gradle's configuration cache? Slightly lean towards plugin (although it should support the configuration cache), but the error message makes me wonder. There's also some weird behavior where the issue doesn't repro if you build first with the cache disabled, but then build the exact same code with the cache enabled.
Copy code
Execution failed for task ':app:test'.
> Cannot cast object 'org.gradle.internal.serialize.codecs.core.ClosureCodec$BrokenObject@46f481e2' with class 'org.gradle.internal.serialize.codecs.core.ClosureCodec$BrokenObject' to class 'com.avast.gradle.dockercompose.TasksConfigurator'
The issue includes a fairly simple MCVE.
v
If it fails with 9 but worked in 8, did you check with 8.14.3 whether you get a deprecation warning? If not, you should probably report an error to Gradle as breaking changes should always go through a deprecation cycle. From the top of my head, I'd assume that the problem is the
tasks.test
in the
dockerCompose { ... }
.
m
I checked and it didn't work in 8.14.3 either. It's a weird error, as the
dockerCompose
mainly adds dependencies to
tasks.test
(or whatever task is provided to
isRequiredBy()
in the
dockerCompose
block):
dependsOn
and
finalizedBy
. Though it also does have a
doFirst
if the task is a
JavaForkOptions
or
ProcessForkOptions
. See the relevant lines: https://github.com/avast/gradle-docker-compose-plugin/blob/34a7d182c1a11dbfe617865[…]/groovy/com/avast/gradle/dockercompose/TasksConfigurator.groovy
And I've confirmed in the MCVE that the
test
task is both a
JavaForkOptions
and
ProcessForkOptions
.
v
I think the problem is, that there the two closures in the last two lines try to access a field of the class
TasksConfigurator
at execution time of the task. But at execution time this confguration-time class is no longer accessible with configuration cache. It is similar to trying to access some field of the script object at execution time. The owner of the closure is serialized / deserialized as
ClosureCodec.BrokenObject
. As long as the closure does not try to access it, all is fine. But if it tries to access it, it fails, and as the class is compiled with
CompileStatic
, with a cast exception.
m
Can you call
buildFeatures.configurationCache.active.get()
at configuration time? (Assuming you inject a
BuildFeatures
into the appropriate object.) It's a
Provider<Boolean>
, but does that mean it's lazy configuration, or something you wouldn't know at configuration time? It seems to work, but I don't know if there's edge cases where it wouldn't work. Not exactly the most elegant solution to wrap those closures with an
if
block that checks if the configuration cache is disabled, but I suspect that many people don't use those environment variables and system properties anyway. (Fixing
TaskConfigurator
may be a much more involved and/or difficult fix.)
v
Generally it is always a bad idea to call
.get()
on a provider at configuration time if you can in any way avoid it, as you introduce the same race conditions you earn for using
afterEvaluate
. In this specific case it might be ok, as that boolean should be impossible to change. But it really seems to be a bad idea. Next user is relying on the behavior and is wondering why it sometimes works and sometimes not. Besides that, if those actions are changing the task configuration, it is anyway a very bad idea and should be changed, even without configuration cache.
m
Vampire's analysis is correct. It is not particularly well-documented, but CC doesn't preserve all Closure's backing stuff (
owner,
delegate
and
this
) when serializing. It may go unnoticed if the caller of the closure sets up the delegate before invoking (so accessing task stuff in
doLast
still works, for example, even unqualified), but this isn't the case here, as we're relying on the
owner
. The immediate workaround for the plugin could be as simple as capturing
composeSettings
in a variable inside the
isRequiredByCore
and using this variable in the closures, though I'm not sure if the whole thing is CC-serializable, and how dynamic the values are. As a plugin user, you can likely copy the logic of
isRequiredByCore
to your build, do necessary adjustments there, and use the fixed version whenever needed. But I'm not very familiar with the plugin, just looked at the MCVE.
Besides that, if those actions are changing the task configuration, it is anyway a very bad idea and should be changed, even without configuration cache.
Sometimes you have to, when trying to wire dynamic stuff there. Ideally,
ProcessForkOptions
and
JavaForkOptions
should have lazy API to configure environment variables and system properties, but we're not there yet. The "great Provider migration" will help when it happens.
does that mean it's lazy configuration, or something you wouldn't know at configuration time?
In general, if a provider has some lifecycle restrictions, it is documented as such. In this particular case the value is indeed immutable within a given build, and can be accessed at any time.
v
Sometimes you have to, when trying to wire dynamic stuff there. Ideally,
ProcessForkOptions
and
JavaForkOptions
should have lazy API to configure environment variables and system properties, but we're not there yet. The "great Provider migration" will help when it happens.
Sometimes, yes, but for env variables and sys properties unless the lazification landed you should usually be able to use `CommandLineArgumentProvider`s where you then can also cleanly model task inputs for up-to-dateness and cache key calculation if it is not static values you need to configure.
m
yes, indeed, input normalization for env vars and the likes is still an open question to us 😞 Obviously, you don't always want
@Input
behavior there.
v
Yeah, that's the nice thing with the
CommandLineArgumentProvider
where you have full control over what is or is not considered an input even if it sometimes can be a bit boiler-platey. :-)
m
I found a way to work around it, but there was a second issue beyond capturing
composeSettings
in a local variable:
composeSettings.tasksConfigurator
is null when those
doFirst
actions run; see
ComposeSettings.getServicesInfos()
.