diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/cache/invalidating/InvalidatingRealmCacheEventSpy.java b/daemon/src/main/java/org/mvndaemon/mvnd/cache/invalidating/InvalidatingRealmCacheEventSpy.java index e69da0a0b..81e8d1782 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/cache/invalidating/InvalidatingRealmCacheEventSpy.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/cache/invalidating/InvalidatingRealmCacheEventSpy.java @@ -37,6 +37,7 @@ import org.apache.maven.execution.MavenExecutionResult; import org.apache.maven.project.MavenProject; import org.eclipse.sisu.Typed; +import org.mvndaemon.mvnd.cache.CacheRecord; import org.mvndaemon.mvnd.common.Environment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +52,8 @@ public class InvalidatingRealmCacheEventSpy extends AbstractEventSpy { private final InvalidatingPluginRealmCache pluginCache; private final InvalidatingExtensionRealmCache extensionCache; private final InvalidatingProjectArtifactsCache projectArtifactsCache; + private final InvalidatingPluginDescriptorCache pluginDescriptorCache; + private final InvalidatingPluginArtifactsCache pluginArtifactsCache; private Path multiModuleProjectDirectory; private String pattern; private PathMatcher matcher; @@ -59,10 +62,14 @@ public class InvalidatingRealmCacheEventSpy extends AbstractEventSpy { public InvalidatingRealmCacheEventSpy( InvalidatingPluginRealmCache cache, InvalidatingExtensionRealmCache extensionCache, - InvalidatingProjectArtifactsCache projectArtifactsCache) { + InvalidatingProjectArtifactsCache projectArtifactsCache, + InvalidatingPluginDescriptorCache pluginDescriptorCache, + InvalidatingPluginArtifactsCache pluginArtifactsCache) { this.pluginCache = cache; this.extensionCache = extensionCache; this.projectArtifactsCache = projectArtifactsCache; + this.pluginDescriptorCache = pluginDescriptorCache; + this.pluginArtifactsCache = pluginArtifactsCache; } @Override @@ -110,6 +117,8 @@ public void onEvent(Object event) throws Exception { /* Evict the entries referring to jars under multiModuleProjectDirectory */ pluginCache.cache.removeIf(this::shouldEvict); extensionCache.cache.removeIf(this::shouldEvict); + pluginDescriptorCache.cache.removeIf((k, r) -> shouldEvict(r)); + pluginArtifactsCache.cache.removeIf((k, r) -> shouldEvict(r)); MavenExecutionResult mer = (MavenExecutionResult) event; List projects = mer.getTopologicallySortedProjects(); projectArtifactsCache.cache.removeIf( @@ -127,6 +136,30 @@ private boolean shouldEvict( return projects.stream().anyMatch(p -> k.matches(p.getGroupId(), p.getArtifactId(), p.getVersion())); } + /** + * Evicts cache records (plugin descriptors, plugin artifacts) whose dependency paths refer to an + * artifact in the build tree or match the configured eviction pattern. + */ + private boolean shouldEvict(CacheRecord record) { + try { + return record.getDependencyPaths().anyMatch(path -> { + if (path.startsWith(multiModuleProjectDirectory)) { + LOG.debug("Removing cache entry because it refers to an artifact in the build tree {}", path); + return true; + } else if (matcher != null && matcher.matches(path)) { + LOG.debug( + "Removing cache entry because its components {} matches the eviction pattern '{}'", + path, + pattern); + return true; + } + return false; + }); + } catch (Exception e) { + return true; + } + } + private boolean shouldEvict(InvalidatingPluginRealmCache.Key k, InvalidatingPluginRealmCache.Record v) { try { for (URL url : v.record.getRealm().getURLs()) { diff --git a/integration-tests/src/test/java/org/mvndaemon/mvnd/it/ReactorPluginDescriptorReloadTest.java b/integration-tests/src/test/java/org/mvndaemon/mvnd/it/ReactorPluginDescriptorReloadTest.java new file mode 100644 index 000000000..4ae2b162c --- /dev/null +++ b/integration-tests/src/test/java/org/mvndaemon/mvnd/it/ReactorPluginDescriptorReloadTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.mvndaemon.mvnd.it; + +import javax.inject.Inject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mvndaemon.mvnd.assertj.TestClientOutput; +import org.mvndaemon.mvnd.client.Client; +import org.mvndaemon.mvnd.client.DaemonParameters; +import org.mvndaemon.mvnd.junit.MvndNativeTest; +import org.mvndaemon.mvnd.junit.TestUtils; + +/** + * Reproduces the case where a reactor plugin is rebuilt between two daemon invocations without being installed + * (so it is resolved as a {@code target/classes} directory). The plugin descriptor (e.g. parameter default values, + * the {@code threadSafe} flag, ...) is cached independently from the plugin realm and must be evicted as well, + * otherwise the daemon keeps serving the stale descriptor. See + * #877. + */ +@MvndNativeTest(projectDir = "src/test/projects/module-and-plugin") +class ReactorPluginDescriptorReloadTest { + + @Inject + Client client; + + @Inject + DaemonParameters parameters; + + @Test + void reloadDescriptorOnReactorPluginChange() throws IOException, InterruptedException { + /* Note: we run "package" (not "install"), so the plugin is consumed from the reactor as a + * target/classes directory rather than from a freshly installed jar in the local repository. */ + final Path helloPropertyPath = + parameters.multiModuleProjectDirectory().resolve("hello/target/hello.property.txt"); + + /* Build #1: the "property" parameter has its default value "Hello" */ + { + final TestClientOutput output = new TestClientOutput(); + client.execute(output, "clean", "package", "-e").assertSuccess(); + Assertions.assertThat(helloPropertyPath) + .usingCharset(StandardCharsets.UTF_8) + .hasContent("Hello"); + } + + /* Build #2: change the parameter default value (a plugin descriptor change that does not alter the + * mojo's byte code path) and verify the daemon picks it up instead of reusing the stale descriptor. */ + { + final Path mojoPath = parameters + .multiModuleProjectDirectory() + .resolve("plugin/src/main/java/org/mvndaemon/mvnd/test/module/plugin/mojo/HelloMojo.java"); + TestUtils.replace(mojoPath, "defaultValue = \"Hello\"", "defaultValue = \"Goodbye\""); + + Thread.sleep(200); // avoid intermittent timestamp resolution issues on some filesystems + + final TestClientOutput output = new TestClientOutput(); + client.execute(output, "clean", "package", "-e").assertSuccess(); + Assertions.assertThat(helloPropertyPath) + .usingCharset(StandardCharsets.UTF_8) + .hasContent("Goodbye"); + } + } +}