Skip to content

Commit 919dfe0

Browse files
Introduce Caffeine-based cache backend with JUnit 5 tests. (#756)
Depend on Caffeine 2.9.3 (Java 8 compatible).
1 parent 0663483 commit 919dfe0

File tree

4 files changed

+344
-0
lines changed

4 files changed

+344
-0
lines changed

httpclient5-cache/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@
9191
<artifactId>junit-jupiter</artifactId>
9292
<scope>test</scope>
9393
</dependency>
94+
<dependency>
95+
<groupId>com.github.ben-manes.caffeine</groupId>
96+
<artifactId>caffeine</artifactId>
97+
<optional>true</optional>
98+
</dependency>
9499
</dependencies>
95100

96101
<build>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.impl.cache.caffeine;
28+
29+
import java.util.Collection;
30+
import java.util.HashMap;
31+
import java.util.Map;
32+
33+
import com.github.benmanes.caffeine.cache.Cache;
34+
35+
import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
36+
import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
37+
import org.apache.hc.client5.http.cache.ResourceIOException;
38+
import org.apache.hc.client5.http.impl.cache.AbstractSerializingCacheStorage;
39+
import org.apache.hc.client5.http.impl.cache.CacheConfig;
40+
import org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializer;
41+
import org.apache.hc.client5.http.impl.cache.NoopCacheEntrySerializer;
42+
import org.apache.hc.core5.util.Args;
43+
44+
45+
/**
46+
* <p>This class is a storage backend for cache entries that uses the
47+
* <a href="https://github.com/ben-manes/caffeine">Caffeine</a>
48+
* cache implementation.</p>
49+
*
50+
* <p>The size limits, eviction policy, and expiry policy are configured
51+
* on the underlying Caffeine cache. The setting for
52+
* {@link CacheConfig#getMaxCacheEntries()} is effectively ignored and
53+
* should be enforced via the Caffeine configuration instead.</p>
54+
*
55+
* <p>Please refer to the Caffeine documentation for details on how to
56+
* configure the cache itself.</p>
57+
*
58+
* @since 5.6
59+
*/
60+
public class CaffeineHttpCacheStorage<T> extends AbstractSerializingCacheStorage<T, T> {
61+
62+
/**
63+
* Creates cache that stores {@link HttpCacheStorageEntry}s without direct serialization.
64+
*
65+
* @since 5.6
66+
*/
67+
public static CaffeineHttpCacheStorage<HttpCacheStorageEntry> createObjectCache(
68+
final Cache<String, HttpCacheStorageEntry> cache, final CacheConfig config) {
69+
return new CaffeineHttpCacheStorage<>(cache, config, NoopCacheEntrySerializer.INSTANCE);
70+
}
71+
72+
/**
73+
* Creates cache that stores serialized {@link HttpCacheStorageEntry}s.
74+
*
75+
* @since 5.6
76+
*/
77+
public static CaffeineHttpCacheStorage<byte[]> createSerializedCache(
78+
final Cache<String, byte[]> cache, final CacheConfig config) {
79+
return new CaffeineHttpCacheStorage<>(cache, config, HttpByteArrayCacheEntrySerializer.INSTANCE);
80+
}
81+
82+
private final Cache<String, T> cache;
83+
84+
/**
85+
* Constructs a storage backend using the provided Caffeine cache
86+
* with the given configuration options, but using an alternative
87+
* cache entry serialization strategy.
88+
*
89+
* @param cache where to store cached origin responses
90+
* @param config cache storage configuration options - note that
91+
* the setting for max object size and max entries
92+
* should be configured on the Caffeine cache instead.
93+
* @param serializer alternative serialization mechanism
94+
*/
95+
public CaffeineHttpCacheStorage(
96+
final Cache<String, T> cache,
97+
final CacheConfig config,
98+
final HttpCacheEntrySerializer<T> serializer) {
99+
super((config != null ? config : CacheConfig.DEFAULT).getMaxUpdateRetries(),
100+
Args.notNull(serializer, "Cache entry serializer"));
101+
this.cache = Args.notNull(cache, "Caffeine cache");
102+
}
103+
104+
@Override
105+
protected String digestToStorageKey(final String key) {
106+
return key;
107+
}
108+
109+
@Override
110+
protected void store(final String storageKey, final T storageObject) throws ResourceIOException {
111+
cache.put(storageKey, storageObject);
112+
}
113+
114+
@Override
115+
protected T restore(final String storageKey) throws ResourceIOException {
116+
return cache.getIfPresent(storageKey);
117+
}
118+
119+
@Override
120+
protected T getForUpdateCAS(final String storageKey) throws ResourceIOException {
121+
return cache.getIfPresent(storageKey);
122+
}
123+
124+
@Override
125+
protected T getStorageObject(final T element) throws ResourceIOException {
126+
return element;
127+
}
128+
129+
@Override
130+
protected boolean updateCAS(
131+
final String storageKey, final T oldStorageObject, final T storageObject) throws ResourceIOException {
132+
return cache.asMap().replace(storageKey, oldStorageObject, storageObject);
133+
}
134+
135+
@Override
136+
protected void delete(final String storageKey) throws ResourceIOException {
137+
cache.invalidate(storageKey);
138+
}
139+
140+
@Override
141+
protected Map<String, T> bulkRestore(final Collection<String> storageKeys) throws ResourceIOException {
142+
final Map<String, T> present = cache.getAllPresent(storageKeys);
143+
return new HashMap<>(present);
144+
}
145+
146+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.impl.cache.caffeine;
28+
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.assertFalse;
31+
import static org.junit.jupiter.api.Assertions.assertNotNull;
32+
import static org.junit.jupiter.api.Assertions.assertNull;
33+
import static org.junit.jupiter.api.Assertions.assertTrue;
34+
35+
import java.time.Instant;
36+
import java.util.Arrays;
37+
import java.util.Map;
38+
39+
import com.github.benmanes.caffeine.cache.Cache;
40+
import com.github.benmanes.caffeine.cache.Caffeine;
41+
42+
import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
43+
import org.apache.hc.client5.http.cache.HttpCacheEntry;
44+
import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
45+
import org.apache.hc.client5.http.cache.Resource;
46+
import org.apache.hc.client5.http.cache.ResourceIOException;
47+
import org.apache.hc.client5.http.impl.cache.CacheConfig;
48+
import org.apache.hc.client5.http.impl.cache.HeapResource;
49+
import org.apache.hc.core5.http.Header;
50+
import org.apache.hc.core5.http.HttpStatus;
51+
import org.apache.hc.core5.http.message.HeaderGroup;
52+
import org.junit.jupiter.api.Test;
53+
54+
class TestCaffeineHttpCacheStorage {
55+
56+
private static HttpCacheEntry newEntry(final int status) throws ResourceIOException {
57+
final Instant now = Instant.now();
58+
final Header[] responseHeaders = new Header[0];
59+
final Resource resource = new HeapResource(new byte[]{1, 2, 3});
60+
61+
final HeaderGroup requestHeaderGroup = new HeaderGroup();
62+
final HeaderGroup responseHeaderGroup = new HeaderGroup();
63+
responseHeaderGroup.setHeaders(responseHeaders);
64+
65+
// Use the non-deprecated @Internal constructor
66+
return new HttpCacheEntry(
67+
now,
68+
now,
69+
"GET",
70+
"/",
71+
requestHeaderGroup,
72+
status,
73+
responseHeaderGroup,
74+
resource,
75+
null);
76+
}
77+
78+
private static CacheConfig newConfig() {
79+
return CacheConfig.custom()
80+
.setMaxUpdateRetries(3)
81+
.build();
82+
}
83+
84+
@Test
85+
void testPutGetRemoveObjectCache() throws Exception {
86+
final Cache<String, HttpCacheStorageEntry> cache = Caffeine.newBuilder().build();
87+
final CacheConfig config = newConfig();
88+
final CaffeineHttpCacheStorage<HttpCacheStorageEntry> storage =
89+
CaffeineHttpCacheStorage.createObjectCache(cache, config);
90+
91+
final String key = "foo";
92+
final HttpCacheEntry entry = newEntry(HttpStatus.SC_OK);
93+
94+
storage.putEntry(key, entry);
95+
96+
final HttpCacheEntry result = storage.getEntry(key);
97+
assertNotNull(result);
98+
assertEquals(HttpStatus.SC_OK, result.getStatus());
99+
100+
storage.removeEntry(key);
101+
assertNull(storage.getEntry(key));
102+
}
103+
104+
@Test
105+
void testUpdateEntryObjectCache() throws Exception {
106+
final Cache<String, HttpCacheStorageEntry> cache = Caffeine.newBuilder().build();
107+
final CacheConfig config = newConfig();
108+
final CaffeineHttpCacheStorage<HttpCacheStorageEntry> storage =
109+
CaffeineHttpCacheStorage.createObjectCache(cache, config);
110+
111+
final String key = "bar";
112+
final HttpCacheEntry original = newEntry(HttpStatus.SC_OK);
113+
storage.putEntry(key, original);
114+
115+
final HttpCacheCASOperation casOperation = existing -> {
116+
assertNotNull(existing);
117+
118+
final HeaderGroup requestHeaderGroup = new HeaderGroup();
119+
requestHeaderGroup.setHeaders(existing.requestHeaders().getHeaders());
120+
121+
final HeaderGroup responseHeaderGroup = new HeaderGroup();
122+
responseHeaderGroup.setHeaders(existing.responseHeaders().getHeaders());
123+
124+
return new HttpCacheEntry(
125+
existing.getRequestInstant(),
126+
existing.getResponseInstant(),
127+
existing.getRequestMethod(),
128+
existing.getRequestURI(),
129+
requestHeaderGroup,
130+
HttpStatus.SC_NOT_MODIFIED,
131+
responseHeaderGroup,
132+
existing.getResource(),
133+
existing.getVariants());
134+
};
135+
136+
storage.updateEntry(key, casOperation);
137+
138+
final HttpCacheEntry updated = storage.getEntry(key);
139+
assertNotNull(updated);
140+
assertEquals(HttpStatus.SC_NOT_MODIFIED, updated.getStatus());
141+
}
142+
143+
@Test
144+
void testGetEntriesUsesBulkRestore() throws Exception {
145+
final Cache<String, HttpCacheStorageEntry> cache = Caffeine.newBuilder().build();
146+
final CacheConfig config = newConfig();
147+
final CaffeineHttpCacheStorage<HttpCacheStorageEntry> storage =
148+
CaffeineHttpCacheStorage.createObjectCache(cache, config);
149+
150+
final HttpCacheEntry entry1 = newEntry(HttpStatus.SC_OK);
151+
final HttpCacheEntry entry2 = newEntry(HttpStatus.SC_CREATED);
152+
153+
storage.putEntry("k1", entry1);
154+
storage.putEntry("k2", entry2);
155+
156+
final Map<String, HttpCacheEntry> result =
157+
storage.getEntries(Arrays.asList("k1", "k2", "k3"));
158+
159+
assertEquals(2, result.size());
160+
assertEquals(HttpStatus.SC_OK, result.get("k1").getStatus());
161+
assertEquals(HttpStatus.SC_CREATED, result.get("k2").getStatus());
162+
assertFalse(result.containsKey("k3"));
163+
}
164+
165+
@Test
166+
void testSerializedCacheStoresBytes() throws Exception {
167+
final Cache<String, byte[]> cache = Caffeine.<String, byte[]>newBuilder().build();
168+
final CacheConfig config = newConfig();
169+
final CaffeineHttpCacheStorage<byte[]> storage =
170+
CaffeineHttpCacheStorage.createSerializedCache(cache, config);
171+
172+
final String key = "baz";
173+
final HttpCacheEntry entry = newEntry(HttpStatus.SC_OK);
174+
175+
storage.putEntry(key, entry);
176+
177+
// Underlying cache should contain serialized bytes
178+
final byte[] stored = cache.getIfPresent(key);
179+
assertNotNull(stored);
180+
assertTrue(stored.length > 0);
181+
182+
final HttpCacheEntry result = storage.getEntry(key);
183+
assertNotNull(result);
184+
assertEquals(HttpStatus.SC_OK, result.getStatus());
185+
}
186+
187+
}

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
<micrometer.tracing.version>1.5.5</micrometer.tracing.version>
8585
<otel.version>1.55.0</otel.version> <!-- already used elsewhere -->
8686
<commons-compress.version>1.26.2</commons-compress.version>
87+
<caffeine.version>2.9.3</caffeine.version> <!-- java 8. current version 3.2.3 -->
8788
</properties>
8889

8990
<dependencyManagement>
@@ -257,6 +258,11 @@
257258
<version>${brotli4j.version}</version>
258259
<optional>true</optional>
259260
</dependency>
261+
<dependency>
262+
<groupId>com.github.ben-manes.caffeine</groupId>
263+
<artifactId>caffeine</artifactId>
264+
<version>${caffeine.version}</version>
265+
</dependency>
260266
</dependencies>
261267
</dependencyManagement>
262268

0 commit comments

Comments
 (0)