HTTPCLIENT-978: HTTP cache update exception handling
Contributed by Michajlo Matijkiw <michajlo_matijkiw at comcast.com> git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@990244 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
02bb1461d8
commit
e664fa132f
|
@ -40,6 +40,6 @@ public interface HttpCacheStorage {
|
|||
void removeEntry(String key) throws IOException;
|
||||
|
||||
void updateEntry(
|
||||
String key, HttpCacheUpdateCallback callback) throws IOException;
|
||||
String key, HttpCacheUpdateCallback callback) throws IOException, HttpCacheUpdateException;
|
||||
|
||||
}
|
||||
|
|
|
@ -26,22 +26,20 @@
|
|||
*/
|
||||
package org.apache.http.client.cache;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Signals that {@link HttpCacheStorage} encountered an error performing an caching operation.
|
||||
* Signals that {@link HttpCacheStorage} encountered an error performing an update operation.
|
||||
*
|
||||
* @since 4.1
|
||||
*/
|
||||
public class HttpCacheOperationException extends IOException {
|
||||
public class HttpCacheUpdateException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 823573584868632876L;
|
||||
|
||||
public HttpCacheOperationException(String message) {
|
||||
public HttpCacheUpdateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HttpCacheOperationException(String message, Throwable cause) {
|
||||
public HttpCacheUpdateException(String message, Throwable cause) {
|
||||
super(message);
|
||||
initCause(cause);
|
||||
}
|
|
@ -5,6 +5,8 @@ import java.util.Date;
|
|||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.HttpRequest;
|
||||
|
@ -15,6 +17,7 @@ import org.apache.http.client.cache.HttpCache;
|
|||
import org.apache.http.client.cache.HttpCacheEntry;
|
||||
import org.apache.http.client.cache.HttpCacheStorage;
|
||||
import org.apache.http.client.cache.HttpCacheUpdateCallback;
|
||||
import org.apache.http.client.cache.HttpCacheUpdateException;
|
||||
import org.apache.http.client.cache.Resource;
|
||||
import org.apache.http.client.cache.ResourceFactory;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
|
@ -30,6 +33,8 @@ public class BasicHttpCache implements HttpCache {
|
|||
private final CacheInvalidator cacheInvalidator;
|
||||
private final HttpCacheStorage storage;
|
||||
|
||||
private final Log log = LogFactory.getLog(getClass());
|
||||
|
||||
public BasicHttpCache(ResourceFactory resourceFactory, HttpCacheStorage storage, CacheConfig config) {
|
||||
this.resourceFactory = resourceFactory;
|
||||
this.uriExtractor = new URIExtractor();
|
||||
|
@ -85,7 +90,12 @@ public class BasicHttpCache implements HttpCache {
|
|||
}
|
||||
|
||||
};
|
||||
storage.updateEntry(parentURI, callback);
|
||||
|
||||
try {
|
||||
storage.updateEntry(parentURI, callback);
|
||||
} catch (HttpCacheUpdateException e) {
|
||||
log.warn("Could not update key [" + parentURI + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -42,8 +42,14 @@ public class CacheConfig {
|
|||
*/
|
||||
public final static int DEFAULT_MAX_CACHE_ENTRIES = 1000;
|
||||
|
||||
/** Default setting for the number of retries on a failed
|
||||
* cache update
|
||||
*/
|
||||
public final static int DEFAULT_MAX_UPDATE_RETRIES = 1;
|
||||
|
||||
private int maxObjectSizeBytes = DEFAULT_MAX_OBJECT_SIZE_BYTES;
|
||||
private int maxCacheEntries = DEFAULT_MAX_CACHE_ENTRIES;
|
||||
private int maxUpdateRetries = DEFAULT_MAX_UPDATE_RETRIES;
|
||||
|
||||
private boolean isSharedCache = true;
|
||||
|
||||
|
@ -96,4 +102,20 @@ public class CacheConfig {
|
|||
public void setMaxCacheEntries(int maxCacheEntries) {
|
||||
this.maxCacheEntries = maxCacheEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of times to retry a cache update on failure
|
||||
* @return int
|
||||
*/
|
||||
public int getMaxUpdateRetries(){
|
||||
return maxUpdateRetries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of times to retry a cache update on failure
|
||||
* @param maxUpdateRetries int
|
||||
*/
|
||||
public void setMaxUpdateRetries(int maxUpdateRetries){
|
||||
this.maxUpdateRetries = maxUpdateRetries;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,25 +35,33 @@ import net.sf.ehcache.Element;
|
|||
|
||||
import org.apache.http.client.cache.HttpCacheEntry;
|
||||
import org.apache.http.client.cache.HttpCacheEntrySerializer;
|
||||
import org.apache.http.client.cache.HttpCacheOperationException;
|
||||
import org.apache.http.client.cache.HttpCacheStorage;
|
||||
import org.apache.http.client.cache.HttpCacheUpdateCallback;
|
||||
import org.apache.http.client.cache.HttpCacheUpdateException;
|
||||
import org.apache.http.impl.client.cache.CacheConfig;
|
||||
import org.apache.http.impl.client.cache.DefaultHttpCacheEntrySerializer;
|
||||
|
||||
public class EhcacheHttpCacheStorage implements HttpCacheStorage {
|
||||
|
||||
private final Ehcache cache;
|
||||
private final HttpCacheEntrySerializer serializer;
|
||||
private final int maxUpdateRetries;
|
||||
|
||||
public EhcacheHttpCacheStorage(Ehcache cache) {
|
||||
this(cache, new DefaultHttpCacheEntrySerializer());
|
||||
this(cache, new CacheConfig(), new DefaultHttpCacheEntrySerializer());
|
||||
}
|
||||
|
||||
public EhcacheHttpCacheStorage(Ehcache cache, HttpCacheEntrySerializer serializer){
|
||||
public EhcacheHttpCacheStorage(Ehcache cache, CacheConfig config){
|
||||
this(cache, config, new DefaultHttpCacheEntrySerializer());
|
||||
}
|
||||
|
||||
public EhcacheHttpCacheStorage(Ehcache cache, CacheConfig config, HttpCacheEntrySerializer serializer){
|
||||
this.cache = cache;
|
||||
this.maxUpdateRetries = config.getMaxUpdateRetries();
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
|
||||
public synchronized void putEntry(String key, HttpCacheEntry entry) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
serializer.writeTo(entry, bos);
|
||||
|
@ -75,29 +83,36 @@ public class EhcacheHttpCacheStorage implements HttpCacheStorage {
|
|||
}
|
||||
|
||||
public synchronized void updateEntry(String key, HttpCacheUpdateCallback callback)
|
||||
throws IOException {
|
||||
Element oldElement = cache.get(key);
|
||||
throws IOException, HttpCacheUpdateException {
|
||||
int numRetries = 0;
|
||||
do{
|
||||
Element oldElement = cache.get(key);
|
||||
|
||||
HttpCacheEntry existingEntry = null;
|
||||
if(oldElement != null){
|
||||
byte[] data = (byte[])oldElement.getValue();
|
||||
existingEntry = serializer.readFrom(new ByteArrayInputStream(data));
|
||||
}
|
||||
|
||||
HttpCacheEntry updatedEntry = callback.update(existingEntry);
|
||||
|
||||
if (existingEntry == null) {
|
||||
putEntry(key, updatedEntry);
|
||||
} else {
|
||||
// Attempt to do a CAS replace, if we fail throw an IOException for now
|
||||
// While this operation should work fine within this instance, multiple instances
|
||||
// could trample each others' data
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
serializer.writeTo(updatedEntry, bos);
|
||||
Element newElement = new Element(key, bos.toByteArray());
|
||||
if (!cache.replace(oldElement, newElement)) {
|
||||
throw new HttpCacheOperationException("Replace operation failed");
|
||||
HttpCacheEntry existingEntry = null;
|
||||
if(oldElement != null){
|
||||
byte[] data = (byte[])oldElement.getValue();
|
||||
existingEntry = serializer.readFrom(new ByteArrayInputStream(data));
|
||||
}
|
||||
}
|
||||
|
||||
HttpCacheEntry updatedEntry = callback.update(existingEntry);
|
||||
|
||||
if (existingEntry == null) {
|
||||
putEntry(key, updatedEntry);
|
||||
return;
|
||||
} else {
|
||||
// Attempt to do a CAS replace, if we fail then retry
|
||||
// While this operation should work fine within this instance, multiple instances
|
||||
// could trample each others' data
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
serializer.writeTo(updatedEntry, bos);
|
||||
Element newElement = new Element(key, bos.toByteArray());
|
||||
if (cache.replace(oldElement, newElement)) {
|
||||
return;
|
||||
}else{
|
||||
numRetries++;
|
||||
}
|
||||
}
|
||||
}while(numRetries <= maxUpdateRetries);
|
||||
throw new HttpCacheUpdateException("Failed to update");
|
||||
}
|
||||
}
|
|
@ -739,7 +739,7 @@ public class TestCachingHttpClient {
|
|||
|
||||
impl.execute(host, req, context);
|
||||
Assert.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE,
|
||||
context.getAttribute("http.cache.response.context"));
|
||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -752,7 +752,7 @@ public class TestCachingHttpClient {
|
|||
|
||||
impl.execute(host, req, context);
|
||||
Assert.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE,
|
||||
context.getAttribute("http.cache.response.context"));
|
||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -771,7 +771,7 @@ public class TestCachingHttpClient {
|
|||
impl.execute(host, req, context);
|
||||
verifyMocks();
|
||||
Assert.assertEquals(CacheResponseStatus.CACHE_MISS,
|
||||
context.getAttribute("http.cache.response.context"));
|
||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -796,7 +796,7 @@ public class TestCachingHttpClient {
|
|||
impl.execute(host, req2, context);
|
||||
verifyMocks();
|
||||
Assert.assertEquals(CacheResponseStatus.CACHE_HIT,
|
||||
context.getAttribute("http.cache.response.context"));
|
||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -835,7 +835,7 @@ public class TestCachingHttpClient {
|
|||
impl.execute(host, req2, context);
|
||||
verifyMocks();
|
||||
Assert.assertEquals(CacheResponseStatus.VALIDATED,
|
||||
context.getAttribute("http.cache.response.context"));
|
||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -867,7 +867,7 @@ public class TestCachingHttpClient {
|
|||
impl.execute(host, req2, context);
|
||||
verifyMocks();
|
||||
Assert.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE,
|
||||
context.getAttribute("http.cache.response.context"));
|
||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -899,7 +899,7 @@ public class TestCachingHttpClient {
|
|||
impl.execute(host, req2, context);
|
||||
verifyMocks();
|
||||
Assert.assertEquals(CacheResponseStatus.CACHE_HIT,
|
||||
context.getAttribute("http.cache.response.context"));
|
||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -37,6 +37,8 @@ import net.sf.ehcache.Element;
|
|||
import org.apache.http.client.cache.HttpCacheEntry;
|
||||
import org.apache.http.client.cache.HttpCacheEntrySerializer;
|
||||
import org.apache.http.client.cache.HttpCacheUpdateCallback;
|
||||
import org.apache.http.client.cache.HttpCacheUpdateException;
|
||||
import org.apache.http.impl.client.cache.CacheConfig;
|
||||
import org.apache.http.impl.client.cache.CacheEntry;
|
||||
import org.easymock.EasyMock;
|
||||
import org.junit.Test;
|
||||
|
@ -49,8 +51,10 @@ public class TestEhcacheHttpCacheStorage extends TestCase {
|
|||
|
||||
public void setUp() {
|
||||
mockCache = EasyMock.createMock(Ehcache.class);
|
||||
CacheConfig config = new CacheConfig();
|
||||
config.setMaxUpdateRetries(1);
|
||||
mockSerializer = EasyMock.createMock(HttpCacheEntrySerializer.class);
|
||||
impl = new EhcacheHttpCacheStorage(mockCache, mockSerializer);
|
||||
impl = new EhcacheHttpCacheStorage(mockCache, config, mockSerializer);
|
||||
}
|
||||
|
||||
private void replayMocks(){
|
||||
|
@ -122,7 +126,7 @@ public class TestEhcacheHttpCacheStorage extends TestCase {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testCacheUpdateNullEntry() throws IOException {
|
||||
public void testCacheUpdateNullEntry() throws IOException, HttpCacheUpdateException {
|
||||
final String key = "foo";
|
||||
final HttpCacheEntry updatedValue = new CacheEntry();
|
||||
|
||||
|
@ -148,7 +152,7 @@ public class TestEhcacheHttpCacheStorage extends TestCase {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testCacheUpdate() throws IOException {
|
||||
public void testCacheUpdate() throws IOException, HttpCacheUpdateException {
|
||||
final String key = "foo";
|
||||
final HttpCacheEntry existingValue = new CacheEntry();
|
||||
final HttpCacheEntry updatedValue = new CacheEntry();
|
||||
|
@ -174,4 +178,68 @@ public class TestEhcacheHttpCacheStorage extends TestCase {
|
|||
impl.updateEntry(key, callback);
|
||||
verifyMocks();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleCacheUpdateRetry() throws IOException, HttpCacheUpdateException {
|
||||
final String key = "foo";
|
||||
final HttpCacheEntry existingValue = new CacheEntry();
|
||||
final HttpCacheEntry updatedValue = new CacheEntry();
|
||||
|
||||
Element existingElement = new Element(key, new byte[]{});
|
||||
|
||||
HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback(){
|
||||
public HttpCacheEntry update(HttpCacheEntry old){
|
||||
assertEquals(existingValue, old);
|
||||
return updatedValue;
|
||||
}
|
||||
};
|
||||
|
||||
// get existing old entry, will happen twice
|
||||
EasyMock.expect(mockCache.get(key)).andReturn(existingElement).times(2);
|
||||
EasyMock.expect(mockSerializer.readFrom(EasyMock.isA(InputStream.class))).andReturn(existingValue).times(2);
|
||||
|
||||
// update but fail
|
||||
mockSerializer.writeTo(EasyMock.same(updatedValue), EasyMock.isA(OutputStream.class));
|
||||
EasyMock.expectLastCall().times(2);
|
||||
EasyMock.expect(mockCache.replace(EasyMock.same(existingElement), EasyMock.isA(Element.class))).andReturn(false);
|
||||
|
||||
// update again and succeed
|
||||
EasyMock.expect(mockCache.replace(EasyMock.same(existingElement), EasyMock.isA(Element.class))).andReturn(true);
|
||||
|
||||
replayMocks();
|
||||
impl.updateEntry(key, callback);
|
||||
verifyMocks();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheUpdateFail() throws IOException, HttpCacheUpdateException {
|
||||
final String key = "foo";
|
||||
final HttpCacheEntry existingValue = new CacheEntry();
|
||||
final HttpCacheEntry updatedValue = new CacheEntry();
|
||||
|
||||
Element existingElement = new Element(key, new byte[]{});
|
||||
|
||||
HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback(){
|
||||
public HttpCacheEntry update(HttpCacheEntry old){
|
||||
assertEquals(existingValue, old);
|
||||
return updatedValue;
|
||||
}
|
||||
};
|
||||
|
||||
// get existing old entry
|
||||
EasyMock.expect(mockCache.get(key)).andReturn(existingElement).times(2);
|
||||
EasyMock.expect(mockSerializer.readFrom(EasyMock.isA(InputStream.class))).andReturn(existingValue).times(2);
|
||||
|
||||
// update but fail
|
||||
mockSerializer.writeTo(EasyMock.same(updatedValue), EasyMock.isA(OutputStream.class));
|
||||
EasyMock.expectLastCall().times(2);
|
||||
EasyMock.expect(mockCache.replace(EasyMock.same(existingElement), EasyMock.isA(Element.class))).andReturn(false).times(2);
|
||||
|
||||
replayMocks();
|
||||
try{
|
||||
impl.updateEntry(key, callback);
|
||||
fail("Expected HttpCacheUpdateException");
|
||||
} catch (HttpCacheUpdateException e) { }
|
||||
verifyMocks();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue