Skip to content
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
5 changes: 5 additions & 0 deletions httpclient5-cache/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* ====================================================================
* 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache.caffeine;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import com.github.benmanes.caffeine.cache.Cache;

import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.client5.http.impl.cache.AbstractSerializingCacheStorage;
import org.apache.hc.client5.http.impl.cache.CacheConfig;
import org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializer;
import org.apache.hc.client5.http.impl.cache.NoopCacheEntrySerializer;
import org.apache.hc.core5.util.Args;


/**
* <p>This class is a storage backend for cache entries that uses the
* <a href="https://github.com/ben-manes/caffeine">Caffeine</a>
* cache implementation.</p>
*
* <p>The size limits, eviction policy, and expiry policy are configured
* on the underlying Caffeine cache. The setting for
* {@link CacheConfig#getMaxCacheEntries()} is effectively ignored and
* should be enforced via the Caffeine configuration instead.</p>
*
* <p>Please refer to the Caffeine documentation for details on how to
* configure the cache itself.</p>
*
* @since 5.6
*/
public class CaffeineHttpCacheStorage<T> extends AbstractSerializingCacheStorage<T, T> {

/**
* Creates cache that stores {@link HttpCacheStorageEntry}s without direct serialization.
*
* @since 5.6
*/
public static CaffeineHttpCacheStorage<HttpCacheStorageEntry> createObjectCache(
final Cache<String, HttpCacheStorageEntry> cache, final CacheConfig config) {
return new CaffeineHttpCacheStorage<>(cache, config, NoopCacheEntrySerializer.INSTANCE);
}

/**
* Creates cache that stores serialized {@link HttpCacheStorageEntry}s.
*
* @since 5.6
*/
public static CaffeineHttpCacheStorage<byte[]> createSerializedCache(
final Cache<String, byte[]> cache, final CacheConfig config) {
return new CaffeineHttpCacheStorage<>(cache, config, HttpByteArrayCacheEntrySerializer.INSTANCE);
}

private final Cache<String, T> cache;

/**
* Constructs a storage backend using the provided Caffeine cache
* with the given configuration options, but using an alternative
* cache entry serialization strategy.
*
* @param cache where to store cached origin responses
* @param config cache storage configuration options - note that
* the setting for max object size and max entries
* should be configured on the Caffeine cache instead.
* @param serializer alternative serialization mechanism
*/
public CaffeineHttpCacheStorage(
final Cache<String, T> cache,
final CacheConfig config,
final HttpCacheEntrySerializer<T> serializer) {
super((config != null ? config : CacheConfig.DEFAULT).getMaxUpdateRetries(),
Args.notNull(serializer, "Cache entry serializer"));
this.cache = Args.notNull(cache, "Caffeine cache");
}

@Override
protected String digestToStorageKey(final String key) {
return key;
}

@Override
protected void store(final String storageKey, final T storageObject) throws ResourceIOException {
cache.put(storageKey, storageObject);
}

@Override
protected T restore(final String storageKey) throws ResourceIOException {
return cache.getIfPresent(storageKey);
}

@Override
protected T getForUpdateCAS(final String storageKey) throws ResourceIOException {
return cache.getIfPresent(storageKey);
}

@Override
protected T getStorageObject(final T element) throws ResourceIOException {
return element;
}

@Override
protected boolean updateCAS(
final String storageKey, final T oldStorageObject, final T storageObject) throws ResourceIOException {
return cache.asMap().replace(storageKey, oldStorageObject, storageObject);
}

@Override
protected void delete(final String storageKey) throws ResourceIOException {
cache.invalidate(storageKey);
}

@Override
protected Map<String, T> bulkRestore(final Collection<String> storageKeys) throws ResourceIOException {
final Map<String, T> present = cache.getAllPresent(storageKeys);
return new HashMap<>(present);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* ====================================================================
* 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache.caffeine;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.time.Instant;
import java.util.Arrays;
import java.util.Map;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.client5.http.impl.cache.CacheConfig;
import org.apache.hc.client5.http.impl.cache.HeapResource;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.message.HeaderGroup;
import org.junit.jupiter.api.Test;

class TestCaffeineHttpCacheStorage {

private static HttpCacheEntry newEntry(final int status) throws ResourceIOException {
final Instant now = Instant.now();
final Header[] responseHeaders = new Header[0];
final Resource resource = new HeapResource(new byte[]{1, 2, 3});

final HeaderGroup requestHeaderGroup = new HeaderGroup();
final HeaderGroup responseHeaderGroup = new HeaderGroup();
responseHeaderGroup.setHeaders(responseHeaders);

// Use the non-deprecated @Internal constructor
return new HttpCacheEntry(
now,
now,
"GET",
"/",
requestHeaderGroup,
status,
responseHeaderGroup,
resource,
null);
}

private static CacheConfig newConfig() {
return CacheConfig.custom()
.setMaxUpdateRetries(3)
.build();
}

@Test
void testPutGetRemoveObjectCache() throws Exception {
final Cache<String, HttpCacheStorageEntry> cache = Caffeine.newBuilder().build();
final CacheConfig config = newConfig();
final CaffeineHttpCacheStorage<HttpCacheStorageEntry> storage =
CaffeineHttpCacheStorage.createObjectCache(cache, config);

final String key = "foo";
final HttpCacheEntry entry = newEntry(HttpStatus.SC_OK);

storage.putEntry(key, entry);

final HttpCacheEntry result = storage.getEntry(key);
assertNotNull(result);
assertEquals(HttpStatus.SC_OK, result.getStatus());

storage.removeEntry(key);
assertNull(storage.getEntry(key));
}

@Test
void testUpdateEntryObjectCache() throws Exception {
final Cache<String, HttpCacheStorageEntry> cache = Caffeine.newBuilder().build();
final CacheConfig config = newConfig();
final CaffeineHttpCacheStorage<HttpCacheStorageEntry> storage =
CaffeineHttpCacheStorage.createObjectCache(cache, config);

final String key = "bar";
final HttpCacheEntry original = newEntry(HttpStatus.SC_OK);
storage.putEntry(key, original);

final HttpCacheCASOperation casOperation = existing -> {
assertNotNull(existing);

final HeaderGroup requestHeaderGroup = new HeaderGroup();
requestHeaderGroup.setHeaders(existing.requestHeaders().getHeaders());

final HeaderGroup responseHeaderGroup = new HeaderGroup();
responseHeaderGroup.setHeaders(existing.responseHeaders().getHeaders());

return new HttpCacheEntry(
existing.getRequestInstant(),
existing.getResponseInstant(),
existing.getRequestMethod(),
existing.getRequestURI(),
requestHeaderGroup,
HttpStatus.SC_NOT_MODIFIED,
responseHeaderGroup,
existing.getResource(),
existing.getVariants());
};

storage.updateEntry(key, casOperation);

final HttpCacheEntry updated = storage.getEntry(key);
assertNotNull(updated);
assertEquals(HttpStatus.SC_NOT_MODIFIED, updated.getStatus());
}

@Test
void testGetEntriesUsesBulkRestore() throws Exception {
final Cache<String, HttpCacheStorageEntry> cache = Caffeine.newBuilder().build();
final CacheConfig config = newConfig();
final CaffeineHttpCacheStorage<HttpCacheStorageEntry> storage =
CaffeineHttpCacheStorage.createObjectCache(cache, config);

final HttpCacheEntry entry1 = newEntry(HttpStatus.SC_OK);
final HttpCacheEntry entry2 = newEntry(HttpStatus.SC_CREATED);

storage.putEntry("k1", entry1);
storage.putEntry("k2", entry2);

final Map<String, HttpCacheEntry> result =
storage.getEntries(Arrays.asList("k1", "k2", "k3"));

assertEquals(2, result.size());
assertEquals(HttpStatus.SC_OK, result.get("k1").getStatus());
assertEquals(HttpStatus.SC_CREATED, result.get("k2").getStatus());
assertFalse(result.containsKey("k3"));
}

@Test
void testSerializedCacheStoresBytes() throws Exception {
final Cache<String, byte[]> cache = Caffeine.<String, byte[]>newBuilder().build();
final CacheConfig config = newConfig();
final CaffeineHttpCacheStorage<byte[]> storage =
CaffeineHttpCacheStorage.createSerializedCache(cache, config);

final String key = "baz";
final HttpCacheEntry entry = newEntry(HttpStatus.SC_OK);

storage.putEntry(key, entry);

// Underlying cache should contain serialized bytes
final byte[] stored = cache.getIfPresent(key);
assertNotNull(stored);
assertTrue(stored.length > 0);

final HttpCacheEntry result = storage.getEntry(key);
assertNotNull(result);
assertEquals(HttpStatus.SC_OK, result.getStatus());
}

}
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
<micrometer.tracing.version>1.5.2</micrometer.tracing.version>
<otel.version>1.52.0</otel.version> <!-- already used elsewhere -->
<commons-compress.version>1.26.2</commons-compress.version>
<caffeine.version>2.9.3</caffeine.version> <!-- java 8. current version 3.2.3 -->
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -257,6 +258,11 @@
<version>${brotli4j.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down