The caches are not all cleaned when deleting the local repository, fixes #312

This commit is contained in:
Guillaume Nodet
2021-01-13 11:36:37 +01:00
parent 123ee63d0d
commit b10af9fe15
16 changed files with 1066 additions and 529 deletions

View File

@@ -70,7 +70,11 @@ import org.apache.maven.extension.internal.CoreExports;
import org.apache.maven.extension.internal.CoreExtensionEntry;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.model.building.ModelProcessor;
import org.apache.maven.plugin.ExtensionRealmCache;
import org.apache.maven.plugin.PluginArtifactsCache;
import org.apache.maven.plugin.PluginRealmCache;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.artifact.ProjectArtifactsCache;
import org.apache.maven.properties.internal.EnvironmentUtils;
import org.apache.maven.properties.internal.SystemProperties;
import org.apache.maven.session.scope.internal.SessionScopeModule;
@@ -89,6 +93,10 @@ import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.transfer.TransferListener;
import org.mvndaemon.mvnd.cache.impl.CliExtensionRealmCache;
import org.mvndaemon.mvnd.cache.impl.CliPluginArtifactsCache;
import org.mvndaemon.mvnd.cache.impl.CliPluginRealmCache;
import org.mvndaemon.mvnd.cache.impl.CliProjectArtifactsCache;
import org.mvndaemon.mvnd.common.Environment;
import org.mvndaemon.mvnd.logging.internal.Slf4jLoggerManager;
import org.mvndaemon.mvnd.logging.smart.BuildEventListener;
@@ -493,6 +501,10 @@ public class DaemonMavenCli {
protected void configure() {
bind(ILoggerFactory.class).toInstance(slf4jLoggerFactory);
bind(CoreExports.class).toInstance(exports);
bind(ExtensionRealmCache.class).to(CliExtensionRealmCache.class);
bind(PluginArtifactsCache.class).to(CliPluginArtifactsCache.class);
bind(PluginRealmCache.class).to(CliPluginRealmCache.class);
bind(ProjectArtifactsCache.class).to(CliProjectArtifactsCache.class);
}
});

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.factory;
import java.util.function.BiPredicate;
/**
* Cache containing records that can be invalidated.
*
* Whenever the paths associated to a given {@link CacheRecord} have been modified,
* the record will be invalidated using {@link CacheRecord#invalidate()}.
*
* @param <K>
* @param <V>
*/
public interface Cache<K, V extends CacheRecord> {
/**
* Check if the cache contains the given key
*/
boolean contains(K key);
/**
* Get the cached record for the key
*/
V get(K key);
/**
* Put a record in the cache
*/
void put(K key, V value);
/**
* Remove all cached records
*/
void clear();
/**
* Remove cached records according to the predicate
*/
void removeIf(BiPredicate<K, V> predicate);
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.factory;
/**
* A factory for cache objects
*/
public interface CacheFactory {
<K, V extends CacheRecord> Cache<K, V> newCache();
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.factory;
import java.nio.file.Path;
import java.util.stream.Stream;
/**
* Data stored in a {@link Cache} which depends on the state
* of a few {@link Path}s.
*/
public interface CacheRecord {
/**
* A list of Path that will invalidate this record if modified.
*/
Stream<Path> getDependentPaths();
/**
* Callback called by the cache when this record
* is removed from the cache.
*/
void invalidate();
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.factory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.sisu.Priority;
import org.mvndaemon.mvnd.common.Os;
@Named
@Singleton
@Priority(10)
public class DefaultCacheFactory implements CacheFactory {
@Inject
WatchServiceCacheFactory watchServiceCacheFactory;
@Inject
TimestampCacheFactory timestampCacheFactory;
@Override
public <K, V extends CacheRecord> Cache<K, V> newCache() {
CacheFactory factory = Os.current() == Os.WINDOWS ? watchServiceCacheFactory : timestampCacheFactory;
return factory.newCache();
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.factory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import javax.inject.Named;
import javax.inject.Singleton;
import org.codehaus.plexus.logging.AbstractLogEnabled;
/**
* A factory for {@link Cache} objects.
*/
@Named
@Singleton
public class TimestampCacheFactory extends AbstractLogEnabled implements CacheFactory {
public TimestampCacheFactory() {
}
@Override
public <K, V extends CacheRecord> Cache<K, V> newCache() {
return new DefaultCache<>();
}
static class ArtifactTimestamp {
final Path path;
final FileTime lastModifiedTime;
final Object fileKey;
ArtifactTimestamp(Path path) {
this.path = path;
try {
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
this.lastModifiedTime = attrs.lastModifiedTime();
this.fileKey = attrs.fileKey();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ArtifactTimestamp that = (ArtifactTimestamp) o;
return path.equals(that.path) &&
Objects.equals(lastModifiedTime, that.lastModifiedTime) &&
Objects.equals(fileKey, that.fileKey);
}
@Override
public int hashCode() {
return Objects.hash(path, lastModifiedTime, fileKey);
}
}
static class Record<V extends CacheRecord> {
final V record;
final Set<ArtifactTimestamp> timestamp;
public Record(V record) {
this.record = record;
this.timestamp = current();
}
private Set<ArtifactTimestamp> current() {
return record.getDependentPaths()
.map(ArtifactTimestamp::new)
.collect(Collectors.toSet());
}
}
static class DefaultCache<K, V extends CacheRecord> implements Cache<K, V> {
private final ConcurrentHashMap<K, Record<V>> map = new ConcurrentHashMap<>();
@Override
public boolean contains(K key) {
return map.containsKey(key);
}
@Override
public V get(K key) {
Record<V> record = map.compute(key, (k, v) -> {
if (v != null) {
try {
if (Objects.equals(v.timestamp, v.current())) {
return v;
}
} catch (RuntimeException e) {
// ignore and invalidate the record
}
v.record.invalidate();
v = null;
}
return v;
});
return record != null ? record.record : null;
}
@Override
public void put(K key, V value) {
map.put(key, new Record<>(value));
}
@Override
public void clear() {
removeIf((k, v) -> true);
}
@Override
public void removeIf(BiPredicate<K, V> predicate) {
for (Iterator<Map.Entry<K, Record<V>>> iterator = map.entrySet().iterator(); iterator.hasNext();) {
Map.Entry<K, Record<V>> entry = iterator.next();
if (predicate.test(entry.getKey(), entry.getValue().record)) {
entry.getValue().record.invalidate();
iterator.remove();
}
}
}
}
}

View File

@@ -0,0 +1,236 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.factory;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiPredicate;
import javax.inject.Named;
import javax.inject.Singleton;
import org.codehaus.plexus.logging.AbstractLogEnabled;
/**
* A factory for {@link Cache} objects.
*/
@Named
@Singleton
public class WatchServiceCacheFactory extends AbstractLogEnabled implements CacheFactory {
private final WatchService watchService;
/**
* Records that have no been invalidated so far. From watched JAR paths to records (because one JAR can be
* present in multiple records)
*/
private final Map<Path, List<CacheRecord>> recordsByPath = new ConcurrentHashMap<>();
/**
* {@link WatchService} can watch only directories but we actually want to watch files. So here we store
* for the given parent directory the count of its child files we watch.
*/
private final Map<Path, Registration> registrationsByDir = new ConcurrentHashMap<>();
public WatchServiceCacheFactory() {
try {
this.watchService = FileSystems.getDefault().newWatchService();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Watch the JARs associated with the given {@code record} for deletions and modifications.
*
* @param record the {@link CacheRecord} to watch
*/
public void add(CacheRecord record) {
record.getDependentPaths().forEach(p -> {
final List<CacheRecord> records = recordsByPath.computeIfAbsent(p, k -> new ArrayList<>());
synchronized (records) {
records.add(record);
registrationsByDir.compute(p.getParent(), this::register);
}
});
}
@Override
public <K, V extends CacheRecord> Cache<K, V> newCache() {
return new DefaultCache<>();
}
private Registration register(Path key, Registration value) {
if (value == null) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Starting to watch path " + key);
}
try {
final WatchKey watchKey = key.register(watchService,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
return new Registration(watchKey);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
int cnt = value.count.incrementAndGet();
if (getLogger().isDebugEnabled()) {
getLogger().debug("Already " + cnt + " watchers for path " + key);
}
return value;
}
}
/**
* Poll for events and process them.
*/
public void validateRecords() {
for (Map.Entry<Path, Registration> entry : registrationsByDir.entrySet()) {
final WatchKey watchKey = entry.getValue().watchKey;
for (WatchEvent<?> event : watchKey.pollEvents()) {
final Path dir = entry.getKey();
WatchEvent.Kind<?> kind = event.kind();
if (getLogger().isDebugEnabled()) {
getLogger().debug("Got watcher event " + kind.name());
}
if (kind == StandardWatchEventKinds.ENTRY_DELETE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
final Path path = dir.resolve((Path) event.context());
final List<CacheRecord> records = recordsByPath.remove(path);
if (getLogger().isDebugEnabled()) {
getLogger().debug("Records for path " + path + ": " + records);
}
if (records != null) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Invalidating recorder of path " + path);
}
remove(records);
}
} else if (kind == StandardWatchEventKinds.OVERFLOW) {
/* Invalidate all records under the given dir */
Iterator<Map.Entry<Path, List<CacheRecord>>> it = recordsByPath.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Path, List<CacheRecord>> en = it.next();
if (en.getKey().getParent().equals(dir)) {
it.remove();
if (getLogger().isDebugEnabled()) {
getLogger().debug("Invalidating recorder of path " + en.getKey());
}
remove(en.getValue());
}
}
}
}
}
}
/**
* Stop watching the JARs associated with the given {@code record} for deletions and modifications.
*
* @param records the {@link CacheRecord}s to stop watching
*/
void remove(List<CacheRecord> records) {
for (CacheRecord record : records) {
record.invalidate();
record.getDependentPaths()
.map(Path::getParent)
.forEach(dir -> registrationsByDir.compute(dir, this::unregister));
}
}
private Registration unregister(Path key, Registration value) {
if (value == null) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Already unwatchers for path " + key);
}
return null;
} else {
final int cnt = value.count.decrementAndGet();
if (cnt <= 0) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Unwatching path " + key);
}
value.watchKey.cancel();
return null;
} else {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Still " + cnt + " watchers for path " + key);
}
return value;
}
}
}
/**
* A watcher registration for a directory storing the {@link WatchKey} and the count of watchers to be able to
* tell when the {@link #watchKey} should be cancelled.
*/
static class Registration {
final AtomicInteger count = new AtomicInteger(1);
final WatchKey watchKey;
Registration(WatchKey watchKey) {
this.watchKey = watchKey;
}
}
class DefaultCache<K, V extends CacheRecord> implements Cache<K, V> {
private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<>();
@Override
public boolean contains(K key) {
return map.containsKey(key);
}
@Override
public V get(K key) {
validateRecords();
return map.get(key);
}
@Override
public void put(K key, V value) {
add(value);
map.put(key, value);
}
@Override
public void clear() {
removeIf((k, v) -> true);
}
@Override
public void removeIf(BiPredicate<K, V> predicate) {
for (Iterator<Map.Entry<K, V>> iterator = map.entrySet().iterator(); iterator.hasNext();) {
Map.Entry<K, V> entry = iterator.next();
if (predicate.test(entry.getKey(), entry.getValue())) {
entry.getValue().invalidate();
iterator.remove();
}
}
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.impl;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.DefaultExtensionRealmCache;
import org.apache.maven.project.ExtensionDescriptor;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
import org.mvndaemon.mvnd.cache.factory.Cache;
import org.mvndaemon.mvnd.cache.factory.CacheFactory;
@Singleton
@Named
public class CliExtensionRealmCache extends DefaultExtensionRealmCache {
protected static class Record implements org.mvndaemon.mvnd.cache.factory.CacheRecord {
private final CacheRecord record;
public Record(CacheRecord record) {
this.record = record;
}
@Override
public Stream<Path> getDependentPaths() {
return record.getArtifacts().stream().map(artifact -> artifact.getFile().toPath());
}
@Override
public void invalidate() {
ClassRealm realm = record.getRealm();
try {
realm.getWorld().disposeRealm(realm.getId());
} catch (NoSuchRealmException e) {
// ignore
}
}
}
private final Cache<Key, Record> cache;
@Inject
public CliExtensionRealmCache(CacheFactory cacheFactory) {
cache = cacheFactory.newCache();
}
@Override
public CacheRecord get(Key key) {
Record r = cache.get(key);
return r != null ? r.record : null;
}
@Override
public CacheRecord put(Key key, ClassRealm extensionRealm, ExtensionDescriptor extensionDescriptor,
List<Artifact> artifacts) {
CacheRecord record = super.put(key, extensionRealm, extensionDescriptor, artifacts);
super.cache.remove(key);
cache.put(key, new Record(record));
return record;
}
@Override
public void flush() {
cache.clear();
}
@Override
public void register(MavenProject project, Key key, CacheRecord record) {
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.impl;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.DefaultPluginArtifactsCache;
import org.apache.maven.plugin.PluginResolutionException;
import org.mvndaemon.mvnd.cache.factory.Cache;
import org.mvndaemon.mvnd.cache.factory.CacheFactory;
@Singleton
@Named
public class CliPluginArtifactsCache extends DefaultPluginArtifactsCache {
protected static class Record implements org.mvndaemon.mvnd.cache.factory.CacheRecord {
private final CacheRecord record;
public Record(CacheRecord record) {
this.record = record;
}
@Override
public Stream<Path> getDependentPaths() {
return record.getArtifacts().stream().map(artifact -> artifact.getFile().toPath());
}
@Override
public void invalidate() {
}
}
final Cache<Key, Record> cache;
public CliPluginArtifactsCache(CacheFactory cacheFactory) {
this.cache = cacheFactory.newCache();
}
public CacheRecord get(Key key) throws PluginResolutionException {
Record r = cache.get(key);
if (r != null) {
if (r.record.getException() != null) {
throw r.record.getException();
}
return r.record;
}
return null;
}
public CacheRecord put(Key key, List<Artifact> pluginArtifacts) {
CacheRecord record = super.put(key, pluginArtifacts);
super.cache.remove(key);
cache.put(key, new Record(record));
return record;
}
public CacheRecord put(Key key, PluginResolutionException exception) {
CacheRecord record = super.put(key, exception);
super.cache.remove(key);
cache.put(key, new Record(record));
return record;
}
protected void assertUniqueKey(Key key) {
if (cache.get(key) != null) {
throw new IllegalStateException("Duplicate artifact resolution result for plugin " + key);
}
}
public void flush() {
cache.clear();
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.impl;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.DefaultPluginDescriptorCache;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.RemoteRepository;
import org.mvndaemon.mvnd.cache.factory.Cache;
import org.mvndaemon.mvnd.cache.factory.CacheFactory;
import org.mvndaemon.mvnd.cache.factory.CacheRecord;
@Singleton
@Named
public class CliPluginDescriptorCache extends DefaultPluginDescriptorCache {
protected static class Record implements CacheRecord {
private final PluginDescriptor descriptor;
public Record(PluginDescriptor descriptor) {
this.descriptor = descriptor;
}
@Override
public Stream<Path> getDependentPaths() {
return Optional.ofNullable(descriptor.getArtifacts()).orElse(Collections.emptyList())
.stream().map(artifact -> artifact.getFile().toPath());
}
@Override
public void invalidate() {
ClassRealm realm = descriptor.getClassRealm();
try {
realm.getWorld().disposeRealm(realm.getId());
} catch (NoSuchRealmException e) {
// ignore
}
}
}
final Cache<Key, Record> cache;
@Inject
public CliPluginDescriptorCache(CacheFactory cacheFactory) {
this.cache = cacheFactory.newCache();
}
@Override
public Key createKey(Plugin plugin, List<RemoteRepository> repositories, RepositorySystemSession session) {
return super.createKey(plugin, repositories, session);
}
@Override
public PluginDescriptor get(Key key) {
Record r = cache.get(key);
return r != null ? clone(r.descriptor) : null;
}
@Override
public void put(Key key, PluginDescriptor descriptor) {
cache.put(key, new Record(clone(descriptor)));
}
@Override
public void flush() {
cache.clear();
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed 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.cache.impl;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.DefaultPluginRealmCache;
import org.apache.maven.plugin.PluginRealmCache;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
import org.eclipse.sisu.Priority;
import org.eclipse.sisu.Typed;
import org.mvndaemon.mvnd.cache.factory.Cache;
import org.mvndaemon.mvnd.cache.factory.CacheFactory;
@Singleton
@Named
@Priority(10)
@Typed(PluginRealmCache.class)
public class CliPluginRealmCache extends DefaultPluginRealmCache {
protected static class Record implements org.mvndaemon.mvnd.cache.factory.CacheRecord {
final CacheRecord record;
public Record(CacheRecord record) {
this.record = record;
}
@Override
public Stream<Path> getDependentPaths() {
return record.getArtifacts().stream().map(artifact -> artifact.getFile().toPath());
}
@Override
public void invalidate() {
ClassRealm realm = record.getRealm();
try {
realm.getWorld().disposeRealm(realm.getId());
} catch (NoSuchRealmException e) {
// ignore
}
}
}
final Cache<Key, Record> cache;
@Inject
public CliPluginRealmCache(CacheFactory cacheFactory) {
cache = cacheFactory.newCache();
}
@Override
public CacheRecord get(Key key) {
Record r = cache.get(key);
return r != null ? r.record : null;
}
@Override
public CacheRecord put(Key key, ClassRealm pluginRealm, List<Artifact> pluginArtifacts) {
CacheRecord record = super.put(key, pluginRealm, pluginArtifacts);
super.cache.remove(key);
cache.put(key, new Record(record));
return record;
}
@Override
public void flush() {
cache.clear();
}
@Override
public void register(MavenProject project, Key key, CacheRecord record) {
}
}

View File

@@ -13,13 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mvndaemon.mvnd.plugin;
package org.mvndaemon.mvnd.cache.impl;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
@@ -27,7 +26,6 @@ import org.apache.maven.eventspy.AbstractEventSpy;
import org.apache.maven.eventspy.EventSpy;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.plugin.PluginRealmCache;
import org.eclipse.sisu.Typed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,29 +52,30 @@ public class CliPluginRealmCacheEventSpy extends AbstractEventSpy {
/* Store the multiModuleProjectDirectory path */
multiModuleProjectDirectory = ((MavenExecutionRequest) event).getMultiModuleProjectDirectory().toPath();
} else if (event instanceof MavenExecutionResult) {
/* Evict the entries refering to jars under multiModuleProjectDirectory */
final Iterator<Map.Entry<PluginRealmCache.Key, CliPluginRealmCache.ValidableCacheRecord>> i = cache.cache
.entrySet().iterator();
while (i.hasNext()) {
final Map.Entry<PluginRealmCache.Key, CliPluginRealmCache.ValidableCacheRecord> entry = i.next();
final CliPluginRealmCache.ValidableCacheRecord record = entry.getValue();
for (URL url : record.getRealm().getURLs()) {
if (url.getProtocol().equals("file")) {
final Path path = Paths.get(url.toURI());
if (path.startsWith(multiModuleProjectDirectory)) {
LOG.debug(
"Removing PluginRealmCache entry {} because it refers to an artifact in the build tree {}",
entry.getKey(), path);
record.dispose();
i.remove();
break;
}
}
}
}
/* Evict the entries referring to jars under multiModuleProjectDirectory */
cache.cache.removeIf(this::shouldEvict);
}
} catch (Exception e) {
LOG.warn("Could not notify CliPluginRealmCache", e);
}
}
private boolean shouldEvict(CliPluginRealmCache.Key k, CliPluginRealmCache.Record v) {
try {
for (URL url : v.record.getRealm().getURLs()) {
if (url.getProtocol().equals("file")) {
final Path path = Paths.get(url.toURI());
if (path.startsWith(multiModuleProjectDirectory)) {
LOG.debug(
"Removing PluginRealmCache entry {} because it refers to an artifact in the build tree {}",
k, path);
return true;
}
}
}
return false;
} catch (URISyntaxException e) {
return true;
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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.cache.impl;
import java.io.File;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.artifact.DefaultProjectArtifactsCache;
import org.mvndaemon.mvnd.cache.factory.Cache;
import org.mvndaemon.mvnd.cache.factory.CacheFactory;
@Singleton
@Named
public class CliProjectArtifactsCache extends DefaultProjectArtifactsCache {
static class Record implements org.mvndaemon.mvnd.cache.factory.CacheRecord {
private final CacheRecord record;
public Record(CacheRecord record) {
this.record = record;
}
@Override
public Stream<Path> getDependentPaths() {
return record.getArtifacts().stream()
.map(Artifact::getFile)
.filter(Objects::nonNull)
.map(File::toPath);
}
@Override
public void invalidate() {
}
}
final Cache<Key, Record> cache;
@Inject
public CliProjectArtifactsCache(CacheFactory cacheFactory) {
this.cache = cacheFactory.newCache();
}
@Override
public CacheRecord get(Key key) throws LifecycleExecutionException {
Record r = cache.get(key);
if (r != null) {
if (r.record.getException() != null) {
throw r.record.getException();
}
return r.record;
}
return null;
}
@Override
public CacheRecord put(Key key, Set<Artifact> pluginArtifacts) {
CacheRecord record = super.put(key, pluginArtifacts);
super.cache.remove(key);
cache.put(key, new Record(record));
return record;
}
@Override
public CacheRecord put(Key key, LifecycleExecutionException e) {
CacheRecord record = super.put(key, e);
super.cache.remove(key);
cache.put(key, new Record(record));
return record;
}
@Override
public void flush() {
cache.clear();
}
@Override
public void register(MavenProject project, Key cacheKey, CacheRecord record) {
}
}

View File

@@ -1,122 +0,0 @@
/*
* 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.plugin;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Exclusion;
import org.apache.maven.model.Plugin;
/**
* File origin:
* https://github.com/apache/maven/blob/maven-3.6.1/maven-core/src/main/java/org/apache/maven/plugin/CacheUtils.java
*
* @author Benjamin Bentmann
*/
class CliCacheUtils {
public static int pluginHashCode(Plugin plugin) {
int hash = 17;
hash = hash * 31 + Objects.hashCode(plugin.getGroupId());
hash = hash * 31 + Objects.hashCode(plugin.getArtifactId());
hash = hash * 31 + Objects.hashCode(plugin.getVersion());
hash = hash * 31 + (plugin.isExtensions() ? 1 : 0);
for (Dependency dependency : plugin.getDependencies()) {
hash = hash * 31 + Objects.hashCode(dependency.getGroupId());
hash = hash * 31 + Objects.hashCode(dependency.getArtifactId());
hash = hash * 31 + Objects.hashCode(dependency.getVersion());
hash = hash * 31 + Objects.hashCode(dependency.getType());
hash = hash * 31 + Objects.hashCode(dependency.getClassifier());
hash = hash * 31 + Objects.hashCode(dependency.getScope());
for (Exclusion exclusion : dependency.getExclusions()) {
hash = hash * 31 + Objects.hashCode(exclusion.getGroupId());
hash = hash * 31 + Objects.hashCode(exclusion.getArtifactId());
}
}
return hash;
}
public static boolean pluginEquals(Plugin a, Plugin b) {
return Objects.equals(a.getArtifactId(), b.getArtifactId()) //
&& Objects.equals(a.getGroupId(), b.getGroupId()) //
&& Objects.equals(a.getVersion(), b.getVersion()) //
&& a.isExtensions() == b.isExtensions() //
&& dependenciesEquals(a.getDependencies(), b.getDependencies());
}
private static boolean dependenciesEquals(List<Dependency> a, List<Dependency> b) {
if (a.size() != b.size()) {
return false;
}
Iterator<Dependency> aI = a.iterator();
Iterator<Dependency> bI = b.iterator();
while (aI.hasNext()) {
Dependency aD = aI.next();
Dependency bD = bI.next();
boolean r = Objects.equals(aD.getGroupId(), bD.getGroupId()) //
&& Objects.equals(aD.getArtifactId(), bD.getArtifactId()) //
&& Objects.equals(aD.getVersion(), bD.getVersion()) //
&& Objects.equals(aD.getType(), bD.getType()) //
&& Objects.equals(aD.getClassifier(), bD.getClassifier()) //
&& Objects.equals(aD.getScope(), bD.getScope());
r &= exclusionsEquals(aD.getExclusions(), bD.getExclusions());
if (!r) {
return false;
}
}
return true;
}
private static boolean exclusionsEquals(List<Exclusion> a, List<Exclusion> b) {
if (a.size() != b.size()) {
return false;
}
Iterator<Exclusion> aI = a.iterator();
Iterator<Exclusion> bI = b.iterator();
while (aI.hasNext()) {
Exclusion aD = aI.next();
Exclusion bD = bI.next();
boolean r = Objects.equals(aD.getGroupId(), bD.getGroupId()) //
&& Objects.equals(aD.getArtifactId(), bD.getArtifactId());
if (!r) {
return false;
}
}
return true;
}
}

View File

@@ -1,383 +0,0 @@
/*
* 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.plugin;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.PluginRealmCache;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Disposable;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.WorkspaceRepository;
import org.eclipse.sisu.Priority;
import org.eclipse.sisu.Typed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Default PluginCache implementation. Assumes cached data does not change.
*
* File origin:
* https://github.com/apache/maven/blob/maven-3.6.2/maven-core/src/main/java/org/apache/maven/plugin/DefaultPluginRealmCache.java
*/
@Singleton
@Named
@Priority(10)
@Typed(PluginRealmCache.class)
public class CliPluginRealmCache
implements PluginRealmCache, Disposable {
/**
* CacheKey
*/
protected static class CacheKey
implements Key {
private final Plugin plugin;
private final WorkspaceRepository workspace;
private final LocalRepository localRepo;
private final List<RemoteRepository> repositories;
private final ClassLoader parentRealm;
private final Map<String, ClassLoader> foreignImports;
private final DependencyFilter filter;
private final int hashCode;
public CacheKey(Plugin plugin, ClassLoader parentRealm, Map<String, ClassLoader> foreignImports,
DependencyFilter dependencyFilter, List<RemoteRepository> repositories,
RepositorySystemSession session) {
this.plugin = plugin.clone();
this.workspace = RepositoryUtils.getWorkspace(session);
this.localRepo = session.getLocalRepository();
this.repositories = new ArrayList<>(repositories.size());
for (RemoteRepository repository : repositories) {
if (repository.isRepositoryManager()) {
this.repositories.addAll(repository.getMirroredRepositories());
} else {
this.repositories.add(repository);
}
}
this.parentRealm = parentRealm;
this.foreignImports = (foreignImports != null) ? foreignImports : Collections.emptyMap();
this.filter = dependencyFilter;
int hash = 17;
hash = hash * 31 + CliCacheUtils.pluginHashCode(plugin);
hash = hash * 31 + Objects.hashCode(workspace);
hash = hash * 31 + Objects.hashCode(localRepo);
hash = hash * 31 + RepositoryUtils.repositoriesHashCode(repositories);
hash = hash * 31 + Objects.hashCode(parentRealm);
hash = hash * 31 + this.foreignImports.hashCode();
hash = hash * 31 + Objects.hashCode(dependencyFilter);
this.hashCode = hash;
}
@Override
public String toString() {
return plugin.getId();
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof CacheKey)) {
return false;
}
CacheKey that = (CacheKey) o;
return parentRealm == that.parentRealm
&& CliCacheUtils.pluginEquals(plugin, that.plugin)
&& Objects.equals(workspace, that.workspace)
&& Objects.equals(localRepo, that.localRepo)
&& RepositoryUtils.repositoriesEquals(this.repositories, that.repositories)
&& Objects.equals(filter, that.filter)
&& Objects.equals(foreignImports, that.foreignImports);
}
}
static class ValidableCacheRecord extends CacheRecord {
private volatile boolean valid = true;
public ValidableCacheRecord(ClassRealm realm, List<Artifact> artifacts) {
super(realm, artifacts);
}
public boolean isValid() {
return valid;
}
public void dispose() {
ClassRealm realm = getRealm();
try {
realm.getWorld().disposeRealm(realm.getId());
} catch (NoSuchRealmException e) {
// ignore
}
}
}
/**
* A {@link RecordValidator} with some methods to watch JARs associated with {@link ValidableCacheRecord}.
*/
static class RecordValidator {
private final WatchService watchService;
/**
* Records that have no been invalidated so far. From watched JAR paths to records (because one JAR can be
* present in multiple records)
*/
private final Map<Path, List<ValidableCacheRecord>> validRecordsByPath = new ConcurrentHashMap<>();
/**
* {@link WatchService} can watch only directories but we actually want to watch files. So here we store
* for the given parent directory the count of its child files we watch.
*/
private final Map<Path, Registration> registrationsByDir = new ConcurrentHashMap<>();
public RecordValidator() {
try {
this.watchService = FileSystems.getDefault().newWatchService();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Watch the JARs associated with the given {@code record} for deletions and modifications.
*
* @param record the {@link ValidableCacheRecord} to watch
*/
void add(ValidableCacheRecord record) {
record.getArtifacts().stream()
.map(Artifact::getFile)
.map(File::toPath)
.forEach(p -> {
validRecordsByPath.compute(p, (key, value) -> {
if (value == null) {
value = new ArrayList<>();
}
value.add(record);
return value;
});
final Path dir = p.getParent();
registrationsByDir.compute(dir, (key, value) -> {
if (value == null) {
LOG.debug("Starting to watch path {}", key);
try {
final WatchKey watchKey = dir.register(watchService, StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
return new Registration(watchKey);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
int cnt = value.count.incrementAndGet();
LOG.debug("Already {} watchers for path {}", cnt, key);
return value;
}
});
});
}
/**
* Stopn watching the JARs associated with the given {@code record} for deletions and modifications.
*
* @param record the {@link ValidableCacheRecord} to stop watching
*/
void remove(ValidableCacheRecord record) {
record.getArtifacts().stream()
.map(Artifact::getFile)
.map(File::toPath)
.forEach(p -> {
final Path dir = p.getParent();
registrationsByDir.compute(dir, (key, value) -> {
if (value == null) {
LOG.debug("Already unwatchers for path {}", key);
return null;
} else {
final int cnt = value.count.decrementAndGet();
if (cnt <= 0) {
LOG.debug("Unwatching path {}", key);
value.watchKey.cancel();
return null;
} else {
LOG.debug("Still {} watchers for path {}", cnt, key);
return value;
}
}
});
});
}
/**
* Poll for events and process them.
*/
public void validateRecords() {
for (Entry<Path, Registration> entry : registrationsByDir.entrySet()) {
final Path dir = entry.getKey();
final WatchKey watchKey = entry.getValue().watchKey;
for (WatchEvent<?> event : watchKey.pollEvents()) {
Kind<?> kind = event.kind();
LOG.debug("Got watcher event {}", kind.name());
if (kind == StandardWatchEventKinds.ENTRY_DELETE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
final Path path = dir.resolve((Path) event.context());
final List<ValidableCacheRecord> records = validRecordsByPath.get(path);
LOG.debug("Records for path {}: {}", path, records);
if (records != null) {
synchronized (records) {
for (ValidableCacheRecord record : records) {
LOG.debug("Invalidating recorder of path {}", path);
record.valid = false;
remove(record);
}
records.clear();
}
}
} else if (kind == StandardWatchEventKinds.OVERFLOW) {
/* Invalidate all records under the given dir */
for (Entry<Path, List<ValidableCacheRecord>> en : validRecordsByPath.entrySet()) {
final Path entryParent = en.getKey().getParent();
if (entryParent.equals(dir)) {
final List<ValidableCacheRecord> records = en.getValue();
if (records != null) {
synchronized (records) {
for (ValidableCacheRecord record : records) {
record.valid = false;
remove(record);
}
records.clear();
}
}
}
}
}
}
}
}
/**
* A watcher registration for a directory storing the {@link WatchKey} and the count of watchers to be able to
* tell when the {@link #watchKey} should be cancelled.
*/
static class Registration {
final AtomicInteger count = new AtomicInteger(1);
final WatchKey watchKey;
public Registration(WatchKey watchKey) {
this.watchKey = watchKey;
}
}
public ValidableCacheRecord newRecord(ClassRealm pluginRealm, List<Artifact> pluginArtifacts) {
final ValidableCacheRecord result = new ValidableCacheRecord(pluginRealm, pluginArtifacts);
add(result);
return result;
}
}
private static final Logger LOG = LoggerFactory.getLogger(CliPluginRealmCache.class);
protected final Map<Key, ValidableCacheRecord> cache = new ConcurrentHashMap<>();
private final RecordValidator watcher;
public CliPluginRealmCache() {
this.watcher = new RecordValidator();
}
public Key createKey(Plugin plugin, ClassLoader parentRealm, Map<String, ClassLoader> foreignImports,
DependencyFilter dependencyFilter, List<RemoteRepository> repositories,
RepositorySystemSession session) {
return new CacheKey(plugin, parentRealm, foreignImports, dependencyFilter, repositories, session);
}
public CacheRecord get(Key key) {
watcher.validateRecords();
return cache.computeIfPresent(key, (k, r) -> {
if (!r.isValid()) {
r.dispose();
return null;
} else {
return r;
}
});
}
public CacheRecord put(Key key, ClassRealm pluginRealm, List<Artifact> pluginArtifacts) {
Objects.requireNonNull(pluginRealm, "pluginRealm cannot be null");
Objects.requireNonNull(pluginArtifacts, "pluginArtifacts cannot be null");
return cache.computeIfAbsent(key, k -> watcher.newRecord(pluginRealm, pluginArtifacts));
}
public void flush() {
for (ValidableCacheRecord record : cache.values()) {
record.dispose();
}
cache.clear();
}
public void register(MavenProject project, Key key, CacheRecord record) {
// default cache does not track plugin usage
}
public void dispose() {
flush();
}
}