Test CacheFactory, add JavaDoc

This commit is contained in:
Peter Palaga
2021-01-24 15:18:38 +01:00
parent d563a27139
commit 6cb1aa5be6
6 changed files with 161 additions and 35 deletions

View File

@@ -19,13 +19,13 @@ import java.util.function.BiPredicate;
import java.util.function.Function; import java.util.function.Function;
/** /**
* Cache containing records that can be invalidated. * A cache containing records that can be invalidated.
* *
* Whenever the paths associated to a given {@link CacheRecord} have been modified, * Whenever the paths associated with the given {@link CacheRecord} have been modified,
* the record will be invalidated using {@link CacheRecord#invalidate()}. * the record will be invalidated using {@link CacheRecord#invalidate()}.
* *
* @param <K> * @param <K> the type of cache keys
* @param <V> * @param <V> the type of cache values
*/ */
public interface Cache<K, V extends CacheRecord> { public interface Cache<K, V extends CacheRecord> {
@@ -36,11 +36,17 @@ public interface Cache<K, V extends CacheRecord> {
/** /**
* Get the cached record for the key * Get the cached record for the key
*
* @param key the key to search for
* @return the {@link CacheRecord} associated with the given {@code key}
*/ */
V get(K key); V get(K key);
/** /**
* Put a record in the cache * Put the given {@link CacheRecord} into the cache under the given {@code key}
*
* @param key the key to store the given {@code value} under
* @param value the value to store under the given {@code key}
*/ */
void put(K key, V value); void put(K key, V value);
@@ -50,12 +56,17 @@ public interface Cache<K, V extends CacheRecord> {
void clear(); void clear();
/** /**
* Remove cached records according to the predicate * Remove all records satisfying the given predicate
*/ */
void removeIf(BiPredicate<K, V> predicate); void removeIf(BiPredicate<K, V> predicate);
/** /**
* Get or compute the cached value if absent and return it. * Get or compute the cached value if absent and return it.
*
* @param key the key to search for
* @param mappingFunction the function to use for the computation of the new {@link CacheRecord} if the key is not
* available in this {@link Cache} yet
* @return the existing or newly computed {@link CacheRecord}
*/ */
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction); V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction);

View File

@@ -20,6 +20,11 @@ package org.mvndaemon.mvnd.cache.factory;
*/ */
public interface CacheFactory { public interface CacheFactory {
/**
* @param <K> the type of {@link Cache} keys
* @param <V> the type of {@link Cache} values
* @return a new {@link Cache}
*/
<K, V extends CacheRecord> Cache<K, V> newCache(); <K, V extends CacheRecord> Cache<K, V> newCache();
} }

View File

@@ -19,19 +19,18 @@ import java.nio.file.Path;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
* Data stored in a {@link Cache} which depends on the state * Data stored in a {@link Cache} depending on the state of a collection of files.
* of a few {@link Path}s.
*/ */
public interface CacheRecord { public interface CacheRecord {
/** /**
* A list of Path that will invalidate this record if modified. * @return a {@link Stream} of file (not directory) {@link Path}s whose modification or deletion causes invalidation
* of this {@link CacheRecord}.
*/ */
Stream<Path> getDependentPaths(); Stream<Path> getDependentPaths();
/** /**
* Callback called by the cache when this record * Callback called by the cache when this {@link CacheRecord} is removed from the cache.
* is removed from the cache.
*/ */
void invalidate(); void invalidate();

View File

@@ -28,15 +28,12 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiPredicate; import java.util.function.BiPredicate;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.inject.Named;
import javax.inject.Singleton;
import org.codehaus.plexus.logging.AbstractLogEnabled; import org.codehaus.plexus.logging.AbstractLogEnabled;
/** /**
* A factory for {@link Cache} objects. * A factory for {@link Cache} objects invalidating its entries based on {@link BasicFileAttributes#lastModifiedTime()}
* and {@link java.nio.file.attribute.BasicFileAttributes#fileKey()}.
*/ */
@Named
@Singleton
public class TimestampCacheFactory extends AbstractLogEnabled implements CacheFactory { public class TimestampCacheFactory extends AbstractLogEnabled implements CacheFactory {
public TimestampCacheFactory() { public TimestampCacheFactory() {
@@ -44,15 +41,19 @@ public class TimestampCacheFactory extends AbstractLogEnabled implements CacheFa
@Override @Override
public <K, V extends CacheRecord> Cache<K, V> newCache() { public <K, V extends CacheRecord> Cache<K, V> newCache() {
return new DefaultCache<>(); return new TimestampCache<>();
} }
static class ArtifactTimestamp { /**
* A state of a file given by {@link BasicFileAttributes#lastModifiedTime()} and
* {@link java.nio.file.attribute.BasicFileAttributes#fileKey()} at the time of {@link FileState} creation.
*/
static class FileState {
final Path path; final Path path;
final FileTime lastModifiedTime; final FileTime lastModifiedTime;
final Object fileKey; final Object fileKey;
ArtifactTimestamp(Path path) { FileState(Path path) {
this.path = path; this.path = path;
try { try {
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
@@ -69,7 +70,7 @@ public class TimestampCacheFactory extends AbstractLogEnabled implements CacheFa
return true; return true;
if (o == null || getClass() != o.getClass()) if (o == null || getClass() != o.getClass())
return false; return false;
ArtifactTimestamp that = (ArtifactTimestamp) o; FileState that = (FileState) o;
return path.equals(that.path) && return path.equals(that.path) &&
Objects.equals(lastModifiedTime, that.lastModifiedTime) && Objects.equals(lastModifiedTime, that.lastModifiedTime) &&
Objects.equals(fileKey, that.fileKey); Objects.equals(fileKey, that.fileKey);
@@ -79,31 +80,42 @@ public class TimestampCacheFactory extends AbstractLogEnabled implements CacheFa
public int hashCode() { public int hashCode() {
return Objects.hash(path, lastModifiedTime, fileKey); return Objects.hash(path, lastModifiedTime, fileKey);
} }
@Override
public String toString() {
return "FileState [path=" + path + ", lastModifiedTime=" + lastModifiedTime + ", fileKey=" + fileKey + "]";
}
} }
static class Record<V extends CacheRecord> { static class Record<V extends CacheRecord> {
final V record; final V record;
final Set<ArtifactTimestamp> timestamp;
/** {@link Set} of {@link FileState}s at the creation time of this {@link Record} */
final Set<FileState> fileStates;
public Record(V record) { public Record(V record) {
this.record = record; this.record = record;
this.timestamp = current(); this.fileStates = currentFileStates();
} }
private Set<ArtifactTimestamp> current() { /**
* @return {@link Set} of {@link FileState}s at current time
*/
private Set<FileState> currentFileStates() {
return record.getDependentPaths() return record.getDependentPaths()
.map(ArtifactTimestamp::new) .map(FileState::new)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
} }
static class DefaultCache<K, V extends CacheRecord> implements Cache<K, V> { static class TimestampCache<K, V extends CacheRecord> implements Cache<K, V> {
private final ConcurrentHashMap<K, Record<V>> map = new ConcurrentHashMap<>(); private final ConcurrentHashMap<K, Record<V>> map = new ConcurrentHashMap<>();
@Override @Override
public boolean contains(K key) { public boolean contains(K key) {
return map.containsKey(key); return this.get(key) != null;
} }
@Override @Override
@@ -111,7 +123,8 @@ public class TimestampCacheFactory extends AbstractLogEnabled implements CacheFa
Record<V> record = map.compute(key, (k, v) -> { Record<V> record = map.compute(key, (k, v) -> {
if (v != null) { if (v != null) {
try { try {
if (Objects.equals(v.timestamp, v.current())) { final Set<FileState> currentFileStates = v.currentFileStates();
if (Objects.equals(v.fileStates, currentFileStates)) {
return v; return v;
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
@@ -151,7 +164,7 @@ public class TimestampCacheFactory extends AbstractLogEnabled implements CacheFa
return map.compute(key, (k, v) -> { return map.compute(key, (k, v) -> {
if (v != null) { if (v != null) {
try { try {
if (Objects.equals(v.timestamp, v.current())) { if (Objects.equals(v.fileStates, v.currentFileStates())) {
return v; return v;
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {

View File

@@ -30,15 +30,11 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiPredicate; import java.util.function.BiPredicate;
import java.util.function.Function; import java.util.function.Function;
import javax.inject.Named;
import javax.inject.Singleton;
import org.codehaus.plexus.logging.AbstractLogEnabled; import org.codehaus.plexus.logging.AbstractLogEnabled;
/** /**
* A factory for {@link Cache} objects. * A factory for {@link Cache} objects invalidating its entries based on events received from {@link WatchService}.
*/ */
@Named
@Singleton
public class WatchServiceCacheFactory extends AbstractLogEnabled implements CacheFactory { public class WatchServiceCacheFactory extends AbstractLogEnabled implements CacheFactory {
private final WatchService watchService; private final WatchService watchService;
@@ -80,7 +76,7 @@ public class WatchServiceCacheFactory extends AbstractLogEnabled implements Cach
@Override @Override
public <K, V extends CacheRecord> Cache<K, V> newCache() { public <K, V extends CacheRecord> Cache<K, V> newCache() {
return new DefaultCache<>(); return new WatchServiceCache<>();
} }
private Registration register(Path key, Registration value) { private Registration register(Path key, Registration value) {
@@ -205,7 +201,7 @@ public class WatchServiceCacheFactory extends AbstractLogEnabled implements Cach
} }
} }
class DefaultCache<K, V extends CacheRecord> implements Cache<K, V> { class WatchServiceCache<K, V extends CacheRecord> implements Cache<K, V> {
private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<>(); private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<>();

View File

@@ -0,0 +1,102 @@
/*
* 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.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mvndaemon.mvnd.cache.factory.Cache;
import org.mvndaemon.mvnd.cache.factory.CacheRecord;
import org.mvndaemon.mvnd.cache.factory.DefaultCacheFactory;
import org.mvndaemon.mvnd.common.Os;
public class TimestampCacheFactoryTest {
@Test
void putGet(@TempDir Path tempDir) throws IOException, InterruptedException {
final Path file1 = tempDir.resolve("file1");
Files.write(file1, "content1".getBytes(StandardCharsets.UTF_8));
final SimpleCacheRecord record1 = new SimpleCacheRecord(file1);
final Path file2 = tempDir.resolve("file2");
Files.write(file2, "content2".getBytes(StandardCharsets.UTF_8));
final SimpleCacheRecord record2 = new SimpleCacheRecord(file2);
final Cache<String, CacheRecord> cache = new DefaultCacheFactory().newCache();
final String k1 = "k1";
cache.put(k1, record1);
final String k2 = "k2";
cache.put(k2, record2);
Assertions.assertTrue(cache.contains(k1));
Assertions.assertEquals(record1, cache.get(k1));
Assertions.assertFalse(record1.invalidated);
Assertions.assertTrue(cache.contains(k2));
Assertions.assertEquals(record2, cache.get(k2));
Assertions.assertFalse(record2.invalidated);
Files.write(file1, "content1.1".getBytes(StandardCharsets.UTF_8));
if (Os.current() == Os.WINDOWS) {
Thread.sleep(3000);
}
Assertions.assertFalse(cache.contains(k1));
Assertions.assertNull(cache.get(k1));
Assertions.assertTrue(record1.invalidated);
Assertions.assertTrue(cache.contains(k2));
Assertions.assertEquals(record2, cache.get(k2));
Assertions.assertFalse(record2.invalidated);
Files.delete(file2);
if (Os.current() == Os.WINDOWS) {
Thread.sleep(3000);
}
Assertions.assertFalse(cache.contains(k2));
Assertions.assertNull(cache.get(k2));
Assertions.assertTrue(record2.invalidated);
}
static class SimpleCacheRecord implements org.mvndaemon.mvnd.cache.factory.CacheRecord {
private final List<Path> paths;
private boolean invalidated = false;
SimpleCacheRecord(Path... paths) {
this.paths = Arrays.asList(paths);
}
@Override
public Stream<Path> getDependentPaths() {
return paths.stream();
}
@Override
public void invalidate() {
this.invalidated = true;
}
}
}