Skip to content

Commit 93f7321

Browse files
committed
Make memcache implementations pluggable
Fixes #359
1 parent 97c22b8 commit 93f7321

9 files changed

Lines changed: 192 additions & 146 deletions

File tree

src/main/java/com/googlecode/objectify/ObjectifyFactory.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import com.google.cloud.datastore.IncompleteKey;
66
import com.googlecode.objectify.cache.CachingAsyncDatastore;
77
import com.googlecode.objectify.cache.EntityMemcache;
8+
import com.googlecode.objectify.cache.MemcacheService;
9+
import com.googlecode.objectify.cache.spymemcached.SpyMemcacheService;
810
import com.googlecode.objectify.impl.AsyncDatastore;
911
import com.googlecode.objectify.impl.AsyncDatastoreImpl;
1012
import com.googlecode.objectify.impl.CacheControlImpl;
@@ -18,11 +20,9 @@
1820
import com.googlecode.objectify.impl.TransactorSupplier;
1921
import com.googlecode.objectify.impl.TypeUtils;
2022
import com.googlecode.objectify.impl.translate.Translators;
21-
import lombok.SneakyThrows;
2223
import net.spy.memcached.MemcachedClient;
2324

2425
import java.lang.reflect.Constructor;
25-
import java.net.InetSocketAddress;
2626
import java.util.ArrayDeque;
2727
import java.util.ArrayList;
2828
import java.util.Arrays;
@@ -61,8 +61,8 @@ public class ObjectifyFactory implements Forge
6161
/** The raw interface to the datastore from the Cloud SDK */
6262
protected Datastore datastore;
6363

64-
/** The raw interface to memcache */
65-
protected MemcachedClient memcache;
64+
/** The low-level interface to memcache */
65+
protected MemcacheService memcache;
6666

6767
/** Encapsulates entity registration info */
6868
protected Registrar registrar;
@@ -76,31 +76,40 @@ public class ObjectifyFactory implements Forge
7676
/** */
7777
protected EntityMemcacheStats memcacheStats = new EntityMemcacheStats();
7878

79-
/** Manages caching of entities at a low level; might be null to indicate "no cache" */
79+
/** Manages caching of entities; might be null to indicate "no cache" */
8080
protected EntityMemcache entityMemcache;
8181

8282
/** Uses default datastore, no memcache */
8383
public ObjectifyFactory() {
8484
this(DatastoreOptions.getDefaultInstance().getService());
8585
}
8686

87-
@SneakyThrows
88-
private static MemcachedClient defaultMemcachedClient() {
89-
return new MemcachedClient(new InetSocketAddress("localhost", 11211));
90-
}
91-
92-
/** */
87+
/**
88+
* No memcache
89+
*/
9390
public ObjectifyFactory(final Datastore datastore) {
94-
this(datastore, null);
91+
this(datastore, (MemcacheService)null);
9592
}
9693

9794
/** Uses default datastore */
9895
public ObjectifyFactory(final MemcachedClient memcache) {
9996
this(DatastoreOptions.getDefaultInstance().getService(), memcache);
10097
}
10198

102-
/** */
99+
/** Uses default datastore */
100+
public ObjectifyFactory(final MemcacheService memcache) {
101+
this(DatastoreOptions.getDefaultInstance().getService(), memcache);
102+
}
103+
104+
/**
105+
*/
103106
public ObjectifyFactory(final Datastore datastore, final MemcachedClient memcache) {
107+
this(datastore, new SpyMemcacheService(memcache));
108+
}
109+
110+
/**
111+
*/
112+
public ObjectifyFactory(final Datastore datastore, final MemcacheService memcache) {
104113
this.datastore = datastore;
105114
this.registrar = new Registrar(this);
106115
this.keys = new Keys(datastore, registrar);
@@ -116,7 +125,7 @@ public Datastore datastore() {
116125
}
117126

118127
/** */
119-
public MemcachedClient memcache() {
128+
public MemcacheService memcache() {
120129
return this.memcache;
121130
}
122131

src/main/java/com/googlecode/objectify/cache/EntityMemcache.java

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import lombok.EqualsAndHashCode;
77
import lombok.Getter;
88
import lombok.extern.slf4j.Slf4j;
9-
import net.spy.memcached.CASValue;
10-
import net.spy.memcached.MemcachedClient;
119

1210
import java.util.Collection;
1311
import java.util.HashMap;
@@ -53,7 +51,7 @@ public class Bucket {
5351
* If null, this means the key is uncacheable (possibly because the cache is down).
5452
* If not null, the IV holds the Entity or NEGATIVE.
5553
*/
56-
private final CASValue<Object> casValue;
54+
private final IdentifiableValue identifiableValue;
5755

5856
/**
5957
* The Entity to store in this bucket in a put(). Can be null to indicate a negative cache
@@ -70,21 +68,21 @@ public Bucket(final Key key)
7068
}
7169

7270
/**
73-
* @param casValue can be null to indicate an uncacheable key
71+
* @param identifiableValue can be null to indicate an uncacheable key
7472
*/
75-
public Bucket(final Key key, final CASValue<Object> casValue) {
73+
public Bucket(final Key key, final IdentifiableValue identifiableValue) {
7674
this.key = key;
77-
this.casValue = casValue;
75+
this.identifiableValue = identifiableValue;
7876
}
7977

8078
/** */
8179
public Key getKey() { return this.key; }
8280

8381
/** @return true if we can cache this bucket; false if the key isn't cacheable or the memcache was down when we created the bucket */
84-
public boolean isCacheable() { return this.casValue != null; }
82+
public boolean isCacheable() { return this.identifiableValue != null; }
8583

8684
/** @return true if this is a negative cache result */
87-
public boolean isNegative() { return this.isCacheable() && NEGATIVE.equals(casValue.getValue()); }
85+
public boolean isNegative() { return this.isCacheable() && NEGATIVE.equals(identifiableValue.getValue()); }
8886

8987
/**
9088
* "Empty" means we don't know the value - it could be null, it could be uncacheable, or we could have some
@@ -94,13 +92,13 @@ public Bucket(final Key key, final CASValue<Object> casValue) {
9492
* @return true if this is empty or uncacheable or something other than a nice entity or negative result.
9593
*/
9694
public boolean isEmpty() {
97-
return !this.isCacheable() || (!this.isNegative() && !(casValue.getValue() instanceof Entity));
95+
return !this.isCacheable() || (!this.isNegative() && !(identifiableValue.getValue() instanceof Entity));
9896
}
9997

10098
/** Get the entity stored at this bucket, possibly the one that was set */
10199
public Entity getEntity() {
102-
if (casValue != null && casValue.getValue() instanceof Entity)
103-
return (Entity)casValue.getValue();
100+
if (identifiableValue != null && identifiableValue.getValue() instanceof Entity)
101+
return (Entity)identifiableValue.getValue();
104102
else
105103
return null;
106104
}
@@ -142,31 +140,29 @@ private Object getNextToStore() {
142140
/**
143141
* Creates a memcache which caches everything without expiry and doesn't record statistics.
144142
*/
145-
public EntityMemcache(final MemcachedClient memcache, final String namespace) {
143+
public EntityMemcache(final MemcacheService memcache, final String namespace) {
146144
this(memcache, namespace, key -> 0);
147145
}
148146

149147
/**
150148
* Creates a memcache which doesn't record stats
151149
*/
152-
public EntityMemcache(final MemcachedClient memcache, final String namespace, final CacheControl cacheControl) {
150+
public EntityMemcache(final MemcacheService memcache, final String namespace, final CacheControl cacheControl) {
153151
this(memcache, namespace, cacheControl, new MemcacheStats() {
154152
@Override public void recordHit(Key key) { }
155153
@Override public void recordMiss(Key key) { }
156154
});
157155
}
158156

159157
public EntityMemcache(
160-
final MemcachedClient memcachedClient,
158+
final MemcacheService memcacheService,
161159
final String namespace,
162160
final CacheControl cacheControl,
163161
final MemcacheStats stats) {
164162

165-
final MemcacheServiceImpl service = new MemcacheServiceImpl(memcachedClient);
166-
167163
this.namespace = namespace;
168-
this.memcache = new KeyMemcacheService(service);
169-
this.memcacheWithRetry = new KeyMemcacheService(MemcacheServiceRetryProxy.createProxy(service));
164+
this.memcache = new KeyMemcacheService(memcacheService);
165+
this.memcacheWithRetry = new KeyMemcacheService(MemcacheServiceRetryProxy.createProxy(memcacheService));
170166
this.stats = stats;
171167
this.cacheControl = cacheControl;
172168
}
@@ -198,7 +194,7 @@ public Map<Key, Bucket> getAll(final Iterable<Key> keys) {
198194
potentials.add(key);
199195
}
200196

201-
Map<Key, CASValue<Object>> casValues;
197+
Map<Key, IdentifiableValue> casValues;
202198
try {
203199
casValues = this.memcache.getIdentifiables(potentials);
204200
} catch (Exception ex) {
@@ -210,7 +206,7 @@ public Map<Key, Bucket> getAll(final Iterable<Key> keys) {
210206

211207
// Now create the remaining buckets
212208
for (final Key key: keys) {
213-
final CASValue<Object> casValue = casValues.get(key); // Might be null, which means uncacheable
209+
final IdentifiableValue casValue = casValues.get(key); // Might be null, which means uncacheable
214210
final Bucket buck = new Bucket(key, casValue);
215211
result.put(key, buck);
216212

@@ -291,7 +287,7 @@ private Set<Key> cachePutIfUntouched(final Iterable<Bucket> buckets) {
291287
continue;
292288
}
293289

294-
payload.put(buck.getKey(), new CasPut(buck.casValue, buck.getNextToStore(), expirySeconds));
290+
payload.put(buck.getKey(), new CasPut(buck.identifiableValue, buck.getNextToStore(), expirySeconds));
295291
}
296292

297293
successes.addAll(this.memcache.putIfUntouched(payload));
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.googlecode.objectify.cache;
2+
3+
public interface IdentifiableValue {
4+
Object getValue();
5+
6+
IdentifiableValue withValue(final Object value);
7+
}

src/main/java/com/googlecode/objectify/cache/KeyMemcacheService.java

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.google.common.collect.Collections2;
55
import com.googlecode.objectify.cache.MemcacheService.CasPut;
66
import lombok.RequiredArgsConstructor;
7-
import net.spy.memcached.CASValue;
87

98
import java.util.Collection;
109
import java.util.Collections;
@@ -16,17 +15,13 @@
1615

1716
/**
1817
* Like MemcacheService but translates keys and values into forms more palatable to the low level service. Also protects
19-
* against no-ops (empty collections). Also stores a sentinel value for null and replaces it with null on fetch.
20-
* Memcached doesn't store nulls (the old GAE SDK hid this from us).
18+
* against no-ops (empty collections).
2119
*
2220
* @author Jeff Schnitzer <jeff@infohazard.org>
2321
*/
2422
@RequiredArgsConstructor
2523
public class KeyMemcacheService
2624
{
27-
/** Stored as a value to indicate that this is a null; memcached doesn't store actual nulls */
28-
static final String NULL_VALUE = "";
29-
3025
/** */
3126
private final MemcacheService service;
3227

@@ -42,27 +37,14 @@ private Collection<String> toCacheKeys(final Collection<Key> keys) {
4237
return Collections2.transform(keys, this::toCacheKey);
4338
}
4439

45-
private Object toCacheValue(final Object thing) {
46-
return thing == null ? NULL_VALUE : thing;
47-
}
48-
49-
private Object fromCacheValue(final Object thing) {
50-
return NULL_VALUE.equals(thing) ? null : thing;
51-
}
52-
53-
public Map<Key, CASValue<Object>> getIdentifiables(final Collection<Key> keys) {
40+
public Map<Key, IdentifiableValue> getIdentifiables(final Collection<Key> keys) {
5441
if (keys.isEmpty())
5542
return Collections.emptyMap();
5643

57-
final Map<String, CASValue<Object>> map = service.getIdentifiables(toCacheKeys(keys));
44+
final Map<String, IdentifiableValue> map = service.getIdentifiables(toCacheKeys(keys));
5845

59-
final Map<Key, CASValue<Object>> dataForApp = new LinkedHashMap<>();
60-
map.forEach((key, value) -> {
61-
final CASValue<Object> transformedValue = (value == null)
62-
? null
63-
: new CASValue<>(value.getCas(), fromCacheValue(value.getValue()));
64-
dataForApp.put(fromCacheKey(key), transformedValue);
65-
});
46+
final Map<Key, IdentifiableValue> dataForApp = new LinkedHashMap<>();
47+
map.forEach((key, value) -> dataForApp.put(fromCacheKey(key), value));
6648
return dataForApp;
6749
}
6850

@@ -73,7 +55,7 @@ public Map<Key, Object> getAll(final Collection<Key> keys) {
7355
final Map<String, Object> map = service.getAll(toCacheKeys(keys));
7456

7557
final Map<Key, Object> dataForApp = new LinkedHashMap<>();
76-
map.forEach((key, value) -> dataForApp.put(fromCacheKey(key), fromCacheValue(value)));
58+
map.forEach((key, value) -> dataForApp.put(fromCacheKey(key), value));
7759
return dataForApp;
7860
}
7961

@@ -82,7 +64,7 @@ public void putAll(final Map<Key, Object> map) {
8264
return;
8365

8466
final Map<String, Object> dataForCache = new LinkedHashMap<>();
85-
map.forEach((key, value) -> dataForCache.put(toCacheKey(key), toCacheValue(value)));
67+
map.forEach((key, value) -> dataForCache.put(toCacheKey(key), value));
8668

8769
service.putAll(dataForCache);
8870
}
@@ -93,7 +75,7 @@ public Set<Key> putIfUntouched(final Map<Key, CasPut> map) {
9375

9476
final Map<String, CasPut> dataForCache = new LinkedHashMap<>();
9577
map.forEach((key, value) -> {
96-
final CasPut actualPut = new CasPut(value.getIv(), toCacheValue(value.getNextToStore()), value.getExpirationSeconds());
78+
final CasPut actualPut = new CasPut(value.getIv(), value.getNextToStore(), value.getExpirationSeconds());
9779
dataForCache.put(toCacheKey(key), actualPut);
9880
});
9981

src/main/java/com/googlecode/objectify/cache/MemcacheService.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,46 @@
11
package com.googlecode.objectify.cache;
22

33
import lombok.Data;
4-
import net.spy.memcached.CASValue;
54

65
import java.util.Collection;
76
import java.util.Map;
87
import java.util.Set;
98

9+
/**
10+
* The interface that all memory cache services must implement. In theory you could write a redis (or whatnot)
11+
* based implementation, but this was designed around memcached.
12+
*
13+
* The implementation must handle 'null' as a value. Note that memcached doesn't handle this natively; the impl
14+
* takes care of translating it to something that can be stored.
15+
*/
1016
public interface MemcacheService {
1117

1218
@Data
1319
class CasPut {
14-
private final CASValue<Object> iv;
20+
private final IdentifiableValue iv;
1521
private final Object nextToStore;
1622
private final int expirationSeconds;
1723
}
1824

1925
Object get(final String key);
2026

2127
/**
22-
* A special property of this method is that if the cache is cold for the keys, we bootstrap an initial
23-
* value so that we can get a CAS value. That doesn't mean the result will always be a value; sometimes
24-
* the bootstrap may fail (for whatever reason) and the resulting map value for a key will be null.
28+
* For cache implementations that don't handle a cold cache for a key (eg memcached), the implementation
29+
* of this method needs to hide that behavior (ie, bootstrap an initial value so we can get a CAS value).
30+
* That doesn't mean the result will always be a value; the bootstrap may fail (for whatever reason) and
31+
* the resulting map value for a key will be null.
2532
*/
26-
Map<String, CASValue<Object>> getIdentifiables(final Collection<String> keys);
33+
Map<String, IdentifiableValue> getIdentifiables(final Collection<String> keys);
2734

2835
Map<String, Object> getAll(final Collection<String> keys);
2936

37+
/** Values can be null */
3038
void put(final String key, final Object thing);
3139

40+
/** Values can be null */
3241
void putAll(final Map<String, Object> values);
3342

43+
/** Values can be null */
3444
Set<String> putIfUntouched(final Map<String, CasPut> values);
3545

3646
void deleteAll(final Collection<String> keys);

0 commit comments

Comments
 (0)