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:
Oleg Kalnichevski 2010-08-27 19:57:57 +00:00
parent 02bb1461d8
commit e664fa132f
7 changed files with 156 additions and 43 deletions

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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

View File

@ -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();
}
}