Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Use Caffeine for request aggregation #6

Merged
merged 7 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<guava.version>31.0.1-jre</guava.version>
<caffeine.version>3.0.5</caffeine.version>
<jwt.version>3.18.2</jwt.version>
<cryptolib.version>2.0.2</cryptolib.version>

Expand Down Expand Up @@ -70,6 +71,11 @@
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
exports org.cryptomator.cloudaccess;
exports org.cryptomator.cloudaccess.api;
exports org.cryptomator.cloudaccess.api.exceptions;
exports org.cryptomator.cloudaccess.requestdecorator;

requires java.xml;
requires com.google.common;
Expand All @@ -11,4 +12,5 @@
requires okhttp.digest;
requires okio;
requires com.auth0.jwt;
requires com.github.benmanes.caffeine;
}
10 changes: 7 additions & 3 deletions src/main/java/org/cryptomator/cloudaccess/CloudAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.cryptomator.cloudaccess.api.exceptions.VaultVerificationFailedException;
import org.cryptomator.cloudaccess.api.exceptions.VaultVersionVerificationFailedException;
import org.cryptomator.cloudaccess.localfs.LocalFsCloudProvider;
import org.cryptomator.cloudaccess.requestdecorator.CloudProviderDecoratorFactory;
import org.cryptomator.cloudaccess.vaultformat8.VaultFormat8ProviderDecorator;
import org.cryptomator.cloudaccess.webdav.WebDavCloudProvider;
import org.cryptomator.cloudaccess.webdav.WebDavCredential;
Expand Down Expand Up @@ -59,9 +60,12 @@ public static CloudProvider vaultFormat8GCMCloudAccess(CloudProvider cloudProvid

verifyVaultFormat8GCMConfig(cloudProvider, pathToVault, rawKey);

VaultFormat8ProviderDecorator provider = new VaultFormat8ProviderDecorator(cloudProvider, pathToVault.resolve("d"), cryptor);
provider.initialize();
return new MetadataCachingProviderDecorator(provider);
var decoratedCloudProvider = new CloudProviderDecoratorFactory().get(cloudProvider, cloudProvider.cachingCapability());

VaultFormat8ProviderDecorator vaultFormat8Provider = new VaultFormat8ProviderDecorator(decoratedCloudProvider, pathToVault.resolve("d"), cryptor);
vaultFormat8Provider.initialize();

return vaultFormat8Provider;
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("JVM doesn't supply a CSPRNG", e);
} catch (InterruptedException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.cryptomator.cloudaccess.requestdecorator;

import org.cryptomator.cloudaccess.api.CloudItemList;
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
import org.cryptomator.cloudaccess.api.CloudPath;
import org.cryptomator.cloudaccess.api.CloudProvider;
import org.cryptomator.cloudaccess.api.ProgressListener;
import org.cryptomator.cloudaccess.api.Quota;

import java.io.InputStream;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.CompletionStage;

interface CloudProviderDecorator extends CloudProvider {

CloudProvider delegate();

@Override
default CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node) {
return delegate().itemMetadata(node);
}

@Override
default CompletionStage<Quota> quota(CloudPath folder) {
return delegate().quota(folder);
}

@Override
default CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pageToken) {
return delegate().list(folder, pageToken);
}

@Override
default CompletionStage<InputStream> read(CloudPath file, long offset, long count, ProgressListener progressListener) {
return delegate().read(file, offset, count, progressListener);
}

@Override
default CompletionStage<Void> write(CloudPath file, boolean replace, InputStream data, long size, Optional<Instant> lastModified, ProgressListener progressListener) {
return delegate().write(file, replace, data, size, lastModified, progressListener);
}

@Override
default CompletionStage<CloudPath> createFolder(CloudPath folder) {
return delegate().createFolder(folder);
}

@Override
default CompletionStage<Void> deleteFile(CloudPath file) {
return delegate().deleteFile(file);
}

@Override
default CompletionStage<Void> deleteFolder(CloudPath folder) {
return delegate().deleteFolder(folder);
}

@Override
default CompletionStage<CloudPath> move(CloudPath source, CloudPath target, boolean replace) {
return delegate().move(source, target, replace);
}

@Override
default boolean cachingCapability() {
return delegate().cachingCapability();
}

@Override
default CompletionStage<Void> pollRemoteChanges() {
return delegate().pollRemoteChanges();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.cryptomator.cloudaccess.requestdecorator;

import org.cryptomator.cloudaccess.api.CloudProvider;

/**
* Factory class to add a caching or request-deduplication decorator around an existing {@link CloudProvider}.
*/
public class CloudProviderDecoratorFactory {

public CloudProvider get(CloudProvider cloudProvider, boolean cloudCachingCapability) {
if (cloudCachingCapability) {
var quotaCachingDecorator = new QuotaRequestCachingDecorator(cloudProvider);
return new MetadataRequestDeduplicationDecorator(quotaCachingDecorator);
} else {
return new MetadataCachingProviderDecorator(cloudProvider);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.cryptomator.cloudaccess;
package org.cryptomator.cloudaccess.requestdecorator;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
Expand All @@ -20,7 +20,7 @@
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;

public class MetadataCachingProviderDecorator implements CloudProvider {
class MetadataCachingProviderDecorator implements CloudProvider {

private final static int DEFAULT_CACHE_TIMEOUT_SECONDS = 10;

Expand Down Expand Up @@ -93,7 +93,7 @@ public CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pa
evictFromItemAndItemListCacheIncludingDescendants(folder);
} else if (delegate.cachingCapability()) {
evictFromItemListCache(entry);
} else if (exception == null) {
} else if (!delegate.cachingCapability() && exception == null) {
evictFromItemAndItemListCacheIncludingDescendants(folder);
assert cloudItemList != null;
cloudItemList.getItems().forEach(metadata -> cachedItemMetadataRequests.put(metadata.getPath(), CompletableFuture.completedFuture(metadata)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.cryptomator.cloudaccess.requestdecorator;

import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.cryptomator.cloudaccess.api.CloudItemList;
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
import org.cryptomator.cloudaccess.api.CloudPath;
import org.cryptomator.cloudaccess.api.CloudProvider;

import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletionStage;

/**
* Decorates an existing CloudProvider by deduplicating identical itemMetadata and list-requests so that the delegate is called only once until the future is completed.
*/
class MetadataRequestDeduplicationDecorator implements CloudProviderDecorator {

// visible for testing
final AsyncCache<CloudPath, CloudItemMetadata> cachedItemMetadataRequests;
final AsyncCache<ItemListEntry, CloudItemList> cachedItemListRequests;

private final CloudProvider delegate;

public MetadataRequestDeduplicationDecorator(CloudProvider delegate) {
this(delegate, //
Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(0)).buildAsync(), //
Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(0)).buildAsync());
}

MetadataRequestDeduplicationDecorator(
CloudProvider delegate, //
AsyncCache<CloudPath, CloudItemMetadata> cachedItemMetadataRequests, //
AsyncCache<ItemListEntry, CloudItemList> cachedItemListRequests) {
this.delegate = delegate;
this.cachedItemMetadataRequests = cachedItemMetadataRequests;
this.cachedItemListRequests = cachedItemListRequests;
}

@Override
public CloudProvider delegate() {
return delegate;
}

@Override
public CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node) {
return cachedItemMetadataRequests.get(node, (key, executor) -> delegate.itemMetadata(key).toCompletableFuture());
}

@Override
public CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pageToken) {
var entry = new ItemListEntry(folder, pageToken);
return cachedItemListRequests.get(entry, (key, executor) -> delegate.list(key.path, key.pageToken).toCompletableFuture());
}

record ItemListEntry(CloudPath path, Optional<String> pageToken) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.cryptomator.cloudaccess.requestdecorator;

import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.cryptomator.cloudaccess.api.CloudPath;
import org.cryptomator.cloudaccess.api.CloudProvider;
import org.cryptomator.cloudaccess.api.Quota;
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
import org.cryptomator.cloudaccess.api.exceptions.QuotaNotAvailableException;

import java.time.Duration;
import java.util.concurrent.CompletionStage;

/**
* Decorates an existing CloudProvider by caching quota-requests for a duration of default 10 seconds (can be set using <code>org.cryptomator.cloudaccess.metadatacachingprovider.timeoutSeconds</code>).
*/
class QuotaRequestCachingDecorator implements CloudProviderDecorator {

private final static int DEFAULT_CACHE_TIMEOUT_SECONDS = 10;

// visible for testing
final AsyncCache<CloudPath, Quota> quotaCache;

private final CloudProvider delegate;

public QuotaRequestCachingDecorator(CloudProvider delegate) {
this(delegate, Caffeine
.newBuilder()
.expireAfterWrite(Duration.ofSeconds(Integer.getInteger("org.cryptomator.cloudaccess.metadatacachingprovider.timeoutSeconds", DEFAULT_CACHE_TIMEOUT_SECONDS)))
.buildAsync());
}

QuotaRequestCachingDecorator(CloudProvider delegate, AsyncCache<CloudPath, Quota> quotaCache) {
this.delegate = delegate;
this.quotaCache = quotaCache;
}

@Override
public CloudProvider delegate() {
return delegate;
}

@Override
public CompletionStage<Quota> quota(CloudPath folder) {
return quotaCache.get(folder, k -> delegate.quota(k).whenComplete((metadata, throwable) -> {
if (throwable != null && !(throwable instanceof NotFoundException) && !(throwable instanceof QuotaNotAvailableException)) {
quotaCache.synchronous().invalidate(folder);
}
}).toCompletableFuture().join());
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.cryptomator.cloudaccess;
package org.cryptomator.cloudaccess.requestdecorator;

import org.cryptomator.cloudaccess.api.CloudItemList;
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
Expand All @@ -8,6 +8,7 @@
import org.cryptomator.cloudaccess.api.ProgressListener;
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
import org.cryptomator.cloudaccess.requestdecorator.MetadataCachingProviderDecorator;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.cryptomator.cloudaccess;
package org.cryptomator.cloudaccess.requestdecorator;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
Expand All @@ -10,6 +10,7 @@
import org.cryptomator.cloudaccess.api.ProgressListener;
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
import org.cryptomator.cloudaccess.requestdecorator.MetadataCachingProviderDecorator;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
Expand Down
Loading