-
-
Notifications
You must be signed in to change notification settings - Fork 305
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
resource: Add a File Resource cache to ResourceBuilder
The cache reduces the need to process files to Resource objects, including SHA-256 computation, for unchanged files. Fixes #5367 Signed-off-by: BJ Hargrave <bj@hargrave.dev>
- Loading branch information
1 parent
6cd8742
commit a4d466d
Showing
4 changed files
with
286 additions
and
21 deletions.
There are no files selected for viewing
127 changes: 127 additions & 0 deletions
127
biz.aQute.bndlib.tests/test/aQute/bnd/osgi/resource/FileResourceCacheKeyTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package aQute.bnd.osgi.resource; | ||
|
||
import static org.assertj.core.api.Assertions.assertThatObject; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.attribute.BasicFileAttributeView; | ||
import java.nio.file.attribute.BasicFileAttributes; | ||
import java.nio.file.attribute.FileTime; | ||
import java.time.Instant; | ||
|
||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.condition.DisabledOnOs; | ||
import org.junit.jupiter.api.condition.OS; | ||
|
||
import aQute.bnd.osgi.resource.FileResourceCache.CacheKey; | ||
import aQute.bnd.test.jupiter.InjectTemporaryDirectory; | ||
import aQute.lib.io.IO; | ||
|
||
class FileResourceCacheKeyTest { | ||
|
||
@Test | ||
void unchanged(@InjectTemporaryDirectory | ||
Path tmp) throws Exception { | ||
Path subject = tmp.resolve("test"); | ||
IO.store("line1", subject, StandardCharsets.UTF_8); | ||
CacheKey key1 = new CacheKey(subject); | ||
CacheKey key2 = new CacheKey(subject); | ||
assertThatObject(key1).isEqualTo(key2); | ||
assertThatObject(key1).hasSameHashCodeAs(key2); | ||
} | ||
|
||
@Test | ||
void change_modified(@InjectTemporaryDirectory | ||
Path tmp) throws Exception { | ||
Path subject = tmp.resolve("test"); | ||
IO.store("line1", subject, StandardCharsets.UTF_8); | ||
CacheKey key1 = new CacheKey(subject); | ||
BasicFileAttributes attributes = Files.getFileAttributeView(subject, BasicFileAttributeView.class) | ||
.readAttributes(); | ||
FileTime lastModifiedTime = attributes.lastModifiedTime(); | ||
Instant plusSeconds = lastModifiedTime.toInstant() | ||
.plusSeconds(10L); | ||
Files.setLastModifiedTime(subject, FileTime.from(plusSeconds)); | ||
CacheKey key2 = new CacheKey(subject); | ||
assertThatObject(key1).isNotEqualTo(key2); | ||
assertThatObject(key1).doesNotHaveSameHashCodeAs(key2); | ||
} | ||
|
||
@Test | ||
void change_size(@InjectTemporaryDirectory | ||
Path tmp) throws Exception { | ||
Path subject = tmp.resolve("test"); | ||
IO.store("line1", subject, StandardCharsets.UTF_8); | ||
CacheKey key1 = new CacheKey(subject); | ||
BasicFileAttributes attributes = Files.getFileAttributeView(subject, BasicFileAttributeView.class) | ||
.readAttributes(); | ||
FileTime lastModifiedTime = attributes.lastModifiedTime(); | ||
IO.store("line100", subject, StandardCharsets.UTF_8); | ||
Files.setLastModifiedTime(subject, lastModifiedTime); | ||
CacheKey key2 = new CacheKey(subject); | ||
assertThatObject(key1).isNotEqualTo(key2); | ||
assertThatObject(key1).doesNotHaveSameHashCodeAs(key2); | ||
} | ||
|
||
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows FS does not support fileKey") | ||
@Test | ||
void change_filekey(@InjectTemporaryDirectory | ||
Path tmp) throws Exception { | ||
Path subject = tmp.resolve("test"); | ||
IO.store("line1", subject, StandardCharsets.UTF_8); | ||
CacheKey key1 = new CacheKey(subject); | ||
BasicFileAttributes attributes = Files.getFileAttributeView(subject, BasicFileAttributeView.class) | ||
.readAttributes(); | ||
assertThatObject(attributes.fileKey()).isNotNull(); | ||
FileTime lastModifiedTime = attributes.lastModifiedTime(); | ||
Path subject2 = tmp.resolve("test.tmp"); | ||
IO.store("line2", subject2, StandardCharsets.UTF_8); | ||
Files.setLastModifiedTime(subject2, lastModifiedTime); | ||
IO.rename(subject2, subject); | ||
CacheKey key2 = new CacheKey(subject); | ||
attributes = Files.getFileAttributeView(subject, BasicFileAttributeView.class) | ||
.readAttributes(); | ||
assertThatObject(attributes.fileKey()).isNotNull(); | ||
assertThatObject(key1).as("key2 not equal") | ||
.isNotEqualTo(key2); | ||
assertThatObject(key1).as("key2 different hash") | ||
.doesNotHaveSameHashCodeAs(key2); | ||
} | ||
|
||
@Test | ||
void change_file_modified(@InjectTemporaryDirectory | ||
Path tmp) throws Exception { | ||
Path subject = tmp.resolve("test"); | ||
IO.store("line1", subject, StandardCharsets.UTF_8); | ||
CacheKey key1 = new CacheKey(subject); | ||
Path subject2 = tmp.resolve("test.tmp"); | ||
IO.store("line2", subject2, StandardCharsets.UTF_8); | ||
BasicFileAttributes attributes = Files.getFileAttributeView(subject2, BasicFileAttributeView.class) | ||
.readAttributes(); | ||
FileTime lastModifiedTime = attributes.lastModifiedTime(); | ||
Instant plusSeconds = lastModifiedTime.toInstant() | ||
.plusSeconds(10L); | ||
Files.setLastModifiedTime(subject2, FileTime.from(plusSeconds)); | ||
IO.rename(subject2, subject); | ||
CacheKey key2 = new CacheKey(subject); | ||
assertThatObject(key1).as("key2 not equal") | ||
.isNotEqualTo(key2); | ||
assertThatObject(key1).as("key2 different hash") | ||
.doesNotHaveSameHashCodeAs(key2); | ||
} | ||
|
||
@Test | ||
void different_files(@InjectTemporaryDirectory | ||
Path tmp) throws Exception { | ||
Path subject1 = tmp.resolve("test1"); | ||
IO.store("line1", subject1, StandardCharsets.UTF_8); | ||
CacheKey key1 = new CacheKey(subject1); | ||
Path subject2 = tmp.resolve("test2"); | ||
IO.copy(subject1, subject2); | ||
CacheKey key2 = new CacheKey(subject2); | ||
assertThatObject(key1).isNotEqualTo(key2); | ||
assertThatObject(key1).doesNotHaveSameHashCodeAs(key2); | ||
} | ||
|
||
} |
142 changes: 142 additions & 0 deletions
142
biz.aQute.bndlib/src/aQute/bnd/osgi/resource/FileResourceCache.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package aQute.bnd.osgi.resource; | ||
|
||
import static aQute.bnd.exceptions.SupplierWithException.asSupplierOrElse; | ||
import static aQute.bnd.osgi.Constants.MIME_TYPE_BUNDLE; | ||
import static aQute.bnd.osgi.Constants.MIME_TYPE_JAR; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.attribute.BasicFileAttributeView; | ||
import java.nio.file.attribute.BasicFileAttributes; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import org.osgi.resource.Resource; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import aQute.bnd.exceptions.Exceptions; | ||
import aQute.bnd.osgi.Domain; | ||
import aQute.libg.cryptography.SHA256; | ||
|
||
class FileResourceCache { | ||
private final static Logger logger = LoggerFactory.getLogger(FileResourceCache.class); | ||
private final static long EXPIRED_DURATION_NANOS = TimeUnit.NANOSECONDS.convert(30L, | ||
TimeUnit.MINUTES); | ||
private static final FileResourceCache INSTANCE = new FileResourceCache(); | ||
private final Map<CacheKey, Resource> cache; | ||
private long time; | ||
|
||
private FileResourceCache() { | ||
cache = new ConcurrentHashMap<>(); | ||
time = System.nanoTime(); | ||
} | ||
|
||
static FileResourceCache getInstance() { | ||
return INSTANCE; | ||
} | ||
|
||
Resource getResource(File file, URI uri) { | ||
if (!file.isFile()) { | ||
return null; | ||
} | ||
// Make sure we don't grow infinitely | ||
final long now = System.nanoTime(); | ||
if ((now - time) > EXPIRED_DURATION_NANOS) { | ||
cache.keySet() | ||
.removeIf(key -> (now - key.time) > EXPIRED_DURATION_NANOS); | ||
time = now; | ||
} | ||
CacheKey cacheKey = new CacheKey(file); | ||
Resource resource = cache.computeIfAbsent(cacheKey, key -> { | ||
logger.debug("parsing {}", file); | ||
ResourceBuilder rb = new ResourceBuilder(); | ||
try { | ||
Domain manifest = Domain.domain(file); | ||
boolean hasIdentity = false; | ||
if (manifest != null) { | ||
hasIdentity = rb.addManifest(manifest); | ||
} | ||
String mime = hasIdentity ? MIME_TYPE_BUNDLE : MIME_TYPE_JAR; | ||
int deferredHashCode = hashCode(file); | ||
DeferredValue<String> sha256 = new DeferredComparableValue<>(String.class, | ||
asSupplierOrElse(() -> SHA256.digest(file) | ||
.asHex(), null), | ||
deferredHashCode); | ||
rb.addContentCapability(uri, sha256, file.length(), mime); | ||
|
||
if (hasIdentity) { | ||
rb.addHashes(file); | ||
} | ||
} catch (Exception e) { | ||
throw Exceptions.duck(e); | ||
} | ||
return rb.build(); | ||
}); | ||
return resource; | ||
} | ||
|
||
private static int hashCode(File file) { | ||
return file.getAbsoluteFile() | ||
.hashCode(); | ||
} | ||
|
||
static final class CacheKey { | ||
private final Object fileKey; | ||
private final long lastModifiedTime; | ||
private final long size; | ||
final long time; | ||
|
||
CacheKey(File file) { | ||
this(file.toPath()); | ||
} | ||
|
||
CacheKey(Path path) { | ||
BasicFileAttributes attributes; | ||
try { | ||
attributes = Files.getFileAttributeView(path, BasicFileAttributeView.class) | ||
.readAttributes(); | ||
} catch (IOException e) { | ||
throw Exceptions.duck(e); | ||
} | ||
if (!attributes.isRegularFile()) { | ||
throw new IllegalArgumentException("File must be a regular file: " + path); | ||
} | ||
Object fileKey = attributes.fileKey(); | ||
this.fileKey = (fileKey != null) ? fileKey // | ||
: path.toAbsolutePath(); // Windows FS does not have fileKey | ||
this.lastModifiedTime = attributes.lastModifiedTime() | ||
.toMillis(); | ||
this.size = attributes.size(); | ||
this.time = System.nanoTime(); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return (Objects.hashCode(fileKey) * 31 + Long.hashCode(lastModifiedTime)) * 31 + Long.hashCode(size); | ||
} | ||
|
||
@Override | ||
public boolean equals(Object obj) { | ||
if (this == obj) { | ||
return true; | ||
} | ||
if (!(obj instanceof CacheKey)) { | ||
return false; | ||
} | ||
CacheKey other = (CacheKey) obj; | ||
return Objects.equals(fileKey, other.fileKey) && (lastModifiedTime == other.lastModifiedTime) | ||
&& (size == other.size); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return Objects.toString(fileKey); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters