Index: src/main/java/org/apache/http/impl/client/cache/memcached/MemcachedHttpCacheStorage.java =================================================================== --- src/main/java/org/apache/http/impl/client/cache/memcached/MemcachedHttpCacheStorage.java (revision 0) +++ src/main/java/org/apache/http/impl/client/cache/memcached/MemcachedHttpCacheStorage.java (revision 0) @@ -0,0 +1,94 @@ +package org.apache.http.impl.client.cache.memcached; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; + +import net.spy.memcached.CASResponse; +import net.spy.memcached.CASValue; +import net.spy.memcached.MemcachedClient; +import net.spy.memcached.MemcachedClientIF; + +import org.apache.http.client.cache.HttpCacheEntry; +import org.apache.http.client.cache.HttpCacheEntrySerializer; +import org.apache.http.client.cache.HttpCacheUpdateException; +import org.apache.http.client.cache.HttpCacheStorage; +import org.apache.http.client.cache.HttpCacheUpdateCallback; +import org.apache.http.impl.client.cache.CacheConfig; +import org.apache.http.impl.client.cache.DefaultHttpCacheEntrySerializer; + +public class MemcachedHttpCacheStorage implements HttpCacheStorage { + + private MemcachedClientIF client; + private HttpCacheEntrySerializer serializer; + private final int maxUpdateRetries; + + public MemcachedHttpCacheStorage(InetSocketAddress address) throws IOException { + this(new MemcachedClient(address)); + } + + public MemcachedHttpCacheStorage(MemcachedClientIF cache) { + this(cache, new CacheConfig(), new DefaultHttpCacheEntrySerializer()); + } + + public MemcachedHttpCacheStorage(MemcachedClientIF client, CacheConfig config, + HttpCacheEntrySerializer serializer) { + this.client = client; + this.maxUpdateRetries = config.getMaxUpdateRetries(); + this.serializer = serializer; + } + + public void putEntry(String url, HttpCacheEntry entry) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + serializer.writeTo(entry, bos); + client.set(url, 0, bos.toByteArray()); + } + + public HttpCacheEntry getEntry(String url) throws IOException { + byte[] data = (byte[]) client.get(url); + if (null == data) + return null; + InputStream bis = new ByteArrayInputStream(data); + return (HttpCacheEntry) serializer.readFrom(bis); + } + + public void removeEntry(String url) throws IOException { + client.delete(url); + } + + public void updateEntry(String url, HttpCacheUpdateCallback callback) + throws HttpCacheUpdateException, IOException { + int numRetries = 0; + do{ + + CASValue v = client.gets(url); + byte[] oldBytes = (v != null) ? (byte[]) v.getValue() : null; + HttpCacheEntry existingEntry = null; + if (oldBytes != null) { + ByteArrayInputStream bis = new ByteArrayInputStream(oldBytes); + existingEntry = serializer.readFrom(bis); + } + HttpCacheEntry updatedEntry = callback.update(existingEntry); + + if (v == null) { + putEntry(url, updatedEntry); + return; + + } else { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + serializer.writeTo(updatedEntry, bos); + CASResponse casResult = client.cas(url, v.getCas(), bos + .toByteArray()); + if (casResult != CASResponse.OK) { + numRetries++; + //throw new HttpCacheUpdateException("Failed to update"); + } + else return; + } + + }while(numRetries <= maxUpdateRetries); + throw new HttpCacheUpdateException("Failed to update"); +} +} \ No newline at end of file Index: src/test/java/org/apache/http/impl/client/cache/memcached/TestMemcachedHttpCacheStorage.java =================================================================== --- src/test/java/org/apache/http/impl/client/cache/memcached/TestMemcachedHttpCacheStorage.java (revision 0) +++ src/test/java/org/apache/http/impl/client/cache/memcached/TestMemcachedHttpCacheStorage.java (revision 0) @@ -0,0 +1,241 @@ +package org.apache.http.impl.client.cache.memcached; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import junit.framework.TestCase; +import net.spy.memcached.CASResponse; +import net.spy.memcached.CASValue; +import net.spy.memcached.MemcachedClientIF; + +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.Before; +import org.junit.Test; + +public class TestMemcachedHttpCacheStorage extends TestCase { + private MemcachedHttpCacheStorage impl; + private MemcachedClientIF mockMemcachedClient; + private HttpCacheEntrySerializer mockSerializer; + + @Before + public void setUp() throws Exception { + mockMemcachedClient = EasyMock.createMock(MemcachedClientIF.class); + mockSerializer = EasyMock.createMock(HttpCacheEntrySerializer.class); + CacheConfig config = new CacheConfig(); + config.setMaxUpdateRetries(1); + impl = new MemcachedHttpCacheStorage(mockMemcachedClient, config, + mockSerializer); + } + + private void replayMocks() { + EasyMock.replay(mockMemcachedClient); + EasyMock.replay(mockSerializer); + } + + private void verifyMocks() { + EasyMock.verify(mockMemcachedClient); + EasyMock.verify(mockSerializer); + } + + @Test + public void testCachePut() throws IOException, HttpCacheUpdateException { + final String url = "foo"; + final HttpCacheEntry value = new CacheEntry(); + mockSerializer.writeTo(EasyMock.isA(HttpCacheEntry.class), EasyMock + .isA(OutputStream.class)); + EasyMock.expect( + mockMemcachedClient.set(EasyMock.eq(url), EasyMock.eq(0), + EasyMock.aryEq(new byte[0]))).andReturn(null); + replayMocks(); + impl.putEntry(url, value); + verifyMocks(); + } + + @Test + public void testCacheGet() throws UnsupportedEncodingException, + IOException, HttpCacheUpdateException { + final String url = "foo"; + final HttpCacheEntry cacheEntry = new CacheEntry(); + EasyMock.expect(mockMemcachedClient.get(url)).andReturn(new byte[] {}); + EasyMock.expect( + mockSerializer.readFrom(EasyMock.isA(InputStream.class))) + .andReturn(cacheEntry); + replayMocks(); + HttpCacheEntry resultingEntry = impl.getEntry(url); + verifyMocks(); + assertSame(cacheEntry, resultingEntry); + } + + @Test + public void testCacheGetNullEntry() throws IOException { + final String url = "foo"; + + EasyMock.expect(mockMemcachedClient.get(url)).andReturn(null); + + replayMocks(); + HttpCacheEntry resultingEntry = impl.getEntry(url); + verifyMocks(); + + assertNull(resultingEntry); + } + + @Test + public void testCacheRemove() throws IOException, HttpCacheUpdateException { + final String url = "foo"; + EasyMock.expect(mockMemcachedClient.delete(url)).andReturn(null); + replayMocks(); + impl.removeEntry(url); + verifyMocks(); + } + + @Test + public void testCacheUpdateNullEntry() throws IOException, + HttpCacheUpdateException { + final String url = "foo"; + final HttpCacheEntry updatedValue = new CacheEntry(); + + HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() { + public HttpCacheEntry update(HttpCacheEntry old) { + assertNull(old); + return updatedValue; + } + }; + + // get empty old entry + EasyMock.expect(mockMemcachedClient.gets(url)).andReturn(null); + // EasyMock.expect(mockCache.get(key)).andReturn(null); + + // put new entry + mockSerializer.writeTo(EasyMock.same(updatedValue), EasyMock + .isA(OutputStream.class)); + EasyMock.expect( + mockMemcachedClient.set(EasyMock.eq(url), EasyMock.eq(0), + EasyMock.aryEq(new byte[0]))).andReturn(null); + + replayMocks(); + impl.updateEntry(url, callback); + verifyMocks(); + } + + @Test + public void testCacheUpdate() throws IOException, HttpCacheUpdateException { + final String url = "foo"; + final HttpCacheEntry existingValue = new CacheEntry(); + final HttpCacheEntry updatedValue = new CacheEntry(); + + CASValue v = new CASValue(1234, new byte[] {}); + + HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() { + public HttpCacheEntry update(HttpCacheEntry old) { + assertEquals(existingValue, old); + return updatedValue; + } + }; + + // get existing old entry + EasyMock.expect(mockMemcachedClient.gets(url)).andReturn(v); + EasyMock.expect( + mockSerializer.readFrom(EasyMock.isA(InputStream.class))) + .andReturn(existingValue); + + // update + EasyMock.expect( + mockMemcachedClient.cas(EasyMock.eq(url), EasyMock.eq(v + .getCas()), EasyMock.aryEq(new byte[0]))).andReturn( + CASResponse.OK); + mockSerializer.writeTo(EasyMock.same(updatedValue), EasyMock + .isA(OutputStream.class)); + + replayMocks(); + impl.updateEntry(url, callback); + verifyMocks(); + } + + @Test + public void testSingleCacheUpdateRetry() throws IOException, + HttpCacheUpdateException { + final String url = "foo"; + final HttpCacheEntry existingValue = new CacheEntry(); + final HttpCacheEntry updatedValue = new CacheEntry(); + CASValue v = new CASValue(1234, 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(mockMemcachedClient.gets(url)).andReturn(v).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( + mockMemcachedClient.cas(EasyMock.eq(url), EasyMock.eq(v + .getCas()), EasyMock.aryEq(new byte[0]))).andReturn( + CASResponse.NOT_FOUND); + + // update again and succeed + EasyMock.expect( + mockMemcachedClient.cas(EasyMock.eq(url), EasyMock.eq(v + .getCas()), EasyMock.aryEq(new byte[0]))).andReturn( + CASResponse.OK); + + replayMocks(); + impl.updateEntry(url, callback); + verifyMocks(); + } + + @Test + public void testCacheUpdateFail() throws IOException, + HttpCacheUpdateException { + final String url = "foo"; + final HttpCacheEntry existingValue = new CacheEntry(); + final HttpCacheEntry updatedValue = new CacheEntry(); + CASValue v = new CASValue(1234, new byte[] {}); + + HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() { + public HttpCacheEntry update(HttpCacheEntry old) { + assertEquals(existingValue, old); + return updatedValue; + } + }; + + // get existing old entry + EasyMock.expect(mockMemcachedClient.gets(url)).andReturn(v).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( + mockMemcachedClient.cas(EasyMock.eq(url), EasyMock.eq(v + .getCas()), EasyMock.aryEq(new byte[0]))).andReturn( + CASResponse.NOT_FOUND).times(2); + + replayMocks(); + try { + impl.updateEntry(url, callback); + fail("Expected HttpCacheUpdateException"); + } catch (HttpCacheUpdateException e) { + } + verifyMocks(); + } + +}