Skip to content

Commit 62f734b

Browse files
committed
Merge pull request #143 from restx/review-restx-session
Allow to plug other cache mechanisms for RestxSession #141 [breaking] [breaking] RestxSession.Definition.Entry is now an interface. Use DefaultSessionDefinitionEntry to get similar behavior if you were instanciating RestxSession.Definition.Entry before. [breaking] RestxSession#cleanUpCaches has been removed It was misleading, was not invalidating the cache and very implementation dependent. 2 invalidate methods have been added to replace it.
2 parents 679c273 + 378893b commit 62f734b

File tree

6 files changed

+258
-68
lines changed

6 files changed

+258
-68
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package restx.security;
2+
3+
import com.google.common.base.Function;
4+
import com.google.common.base.Optional;
5+
import restx.security.RestxSession.Definition.Entry;
6+
7+
/**
8+
* Date: 20/1/15
9+
* Time: 20:56
10+
*/
11+
public class DefaultSessionDefinitionEntry<T> implements Entry<T> {
12+
private final String sessionDefKey;
13+
private final Function<String, Optional<? extends T>> function;
14+
15+
public DefaultSessionDefinitionEntry(Class<T> clazz, String sessionDefKey, Function<String, Optional<? extends T>> function) {
16+
this.sessionDefKey = sessionDefKey;
17+
this.function = function;
18+
}
19+
20+
@Override
21+
public String getKey() {
22+
return sessionDefKey;
23+
}
24+
25+
@Override
26+
public Optional<? extends T> getValueForId(String valueId) {
27+
return function.apply(valueId);
28+
}
29+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package restx.security;
2+
3+
import com.google.common.base.Optional;
4+
import com.google.common.cache.CacheBuilder;
5+
import com.google.common.cache.CacheLoader;
6+
import com.google.common.cache.LoadingCache;
7+
import restx.security.RestxSession.Definition.CachedEntry;
8+
import restx.security.RestxSession.Definition.Entry;
9+
import restx.security.RestxSession.Definition.EntryCacheManager;
10+
11+
import java.util.concurrent.ExecutionException;
12+
13+
/**
14+
* A restx session entry cache manager based on guava cache.
15+
*
16+
* You can override the cache settings by overriding the getCacheBuilder() method.
17+
*
18+
* Note that Guava Cache is not distributed, so be very careful with cache invalidation
19+
* when using this cache.
20+
*
21+
* This is the default EntryCacheManager, see SecurityModule which provides one.
22+
*/
23+
public class GuavaEntryCacheManager implements EntryCacheManager {
24+
@Override
25+
public <T> CachedEntry<T> getCachedEntry(Entry<T> entry) {
26+
return new GuavaCacheSessionDefinitionEntry<T>(entry.getKey(), getLoadingCacheFor(entry));
27+
}
28+
29+
protected <T> LoadingCache<String, T> getLoadingCacheFor(final Entry<T> entry) {
30+
return getCacheBuilder(entry).build(getCacheLoaderFor(entry));
31+
}
32+
33+
protected <T> CacheLoader<String, T> getCacheLoaderFor(final Entry<T> entry) {
34+
return new CacheLoader<String, T>() {
35+
@Override
36+
public T load(String key) throws Exception {
37+
return entry.getValueForId(key).orNull();
38+
}
39+
};
40+
}
41+
42+
protected <T> CacheBuilder<Object, Object> getCacheBuilder(Entry<T> entry) {
43+
return CacheBuilder.newBuilder().maximumSize(1000);
44+
}
45+
46+
/**
47+
* A session definition entry implementation using Guava Cache.
48+
*/
49+
public static class GuavaCacheSessionDefinitionEntry<T> implements CachedEntry<T> {
50+
private final LoadingCache<String, T> loadingCache;
51+
private final String key;
52+
53+
public GuavaCacheSessionDefinitionEntry(String key, LoadingCache<String, T> loadingCache) {
54+
this.key = key;
55+
this.loadingCache = loadingCache;
56+
}
57+
58+
@Override
59+
public String getKey() {
60+
return key;
61+
}
62+
63+
@Override
64+
public Optional<T> getValueForId(String valueId) {
65+
try {
66+
return Optional.fromNullable(loadingCache.get(valueId));
67+
} catch (CacheLoader.InvalidCacheLoadException e) {
68+
// this exception is raised when cache loader returns null, which may happen if the object behind the key
69+
// is deleted. Therefore we just return an absent value
70+
return Optional.absent();
71+
} catch (ExecutionException e) {
72+
throw new RuntimeException(
73+
"impossible to load object from cache using valueid " + valueId + " for " + key + ": " + e.getMessage(), e);
74+
}
75+
}
76+
77+
@Override
78+
public void invalidateCacheFor(String valueId) {
79+
loadingCache.invalidate(valueId);
80+
}
81+
82+
@Override
83+
public void invalidateCache() {
84+
loadingCache.invalidateAll();
85+
}
86+
}
87+
}

restx-core/src/main/java/restx/security/RestxSession.java

Lines changed: 116 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,31 @@
11
package restx.security;
22

33
import com.google.common.base.Optional;
4-
import com.google.common.cache.CacheBuilder;
5-
import com.google.common.cache.CacheLoader;
6-
import com.google.common.cache.LoadingCache;
74
import com.google.common.collect.ImmutableMap;
5+
import com.google.common.collect.ImmutableSet;
86
import com.google.common.collect.Maps;
97
import org.joda.time.Duration;
108
import restx.factory.Component;
119

1210
import java.util.Map;
13-
import java.util.concurrent.ExecutionException;
11+
import java.util.Map.Entry;
1412

1513
/**
1614
* RestxSession is used to store information which can be used across several HTTP requests from the same client.
1715
*
1816
* It is organized as a Map, information is stored by keys.
1917
*
20-
* It doesn't use the JEE Session mechanism, but a more lightweight system relying on a signed session cookie
21-
* (therefore it cannot be tampered by the client).
18+
* It doesn't use the JEE Session mechanism, but a more lightweight system relying on signed session data stored
19+
* on the client (being signed, it cannot be tampered by the client).
2220
*
23-
* The session cookie doesn't store the whole values (which could put a high load on the network and even cause
24-
* problems related to cookie size limit), but rather stores a a value id for each session key.
21+
* The session data doesn't store the whole values (which could put a high load on the network and even cause
22+
* problems related to data storage limit on the client), but rather stores a a value id for each session key.
2523
*
2624
* A value id MUST identify uniquely a value when used for a given session key, and the session MUST be configured
2725
* with a CacheLoader per key, able to load the value corresponding to the value id for a particular key.
2826
*
2927
* Therefore on the server the session enables to access arbitrary large objects, it will only put pressure on a
30-
* server cache, and on cache loaders if requests are heavily distributed. Indeed the cache is not distributed,
31-
* so a in a large clustered environment cache miss will be very likely and cache loaders will often be called.
32-
* Hence in such environment you should be careful to use very efficient cache loaders if you rely heavily on
33-
* session.
28+
* server cache, and on cache loaders if requests are heavily distributed and depending on cache implementation.
3429
*
3530
* An example (using an arbitrary json like notation):
3631
* <pre>
@@ -39,42 +34,126 @@
3934
* "USER": (valueId) -&gt; { return db.findOne("{_id: #}", valueId).as(User.class); }
4035
* }
4136
* "valueIdsByKeys": {
42-
* "USER": "johndoe@acme.com" // valued from session cookie
37+
* "USER": "johndoe@acme.com" // valued from client session data
4338
* }
4439
* }
4540
* </pre>
4641
* With such a restx session, when you call a #get(User.class, "USER"), the session will first check its
4742
* valueIdsByKeys map to find the corresponding valueId ("johndoe@acme.com"). Then it will check the cache for
4843
* this valueId, and in case of cache miss will use the provided cache loader which will load the user from db.
44+
*
45+
* If you want to define your own session keys, you should define a Definition.Entry component for it allowing
46+
* to load values based on their ids. You don't have to take care of caching in the Entry, caching is performed
47+
* by EntryCacheManager.
4948
*/
5049
public class RestxSession {
5150
@Component
5251
public static class Definition {
53-
public static class Entry<T> {
54-
private final Class<T> clazz;
55-
private final String key;
56-
private final CacheLoader<String, T> loader;
57-
58-
public Entry(Class<T> clazz, String key, CacheLoader<String, T> loader) {
59-
this.clazz = clazz;
60-
this.key = key;
61-
this.loader = loader;
62-
}
52+
/**
53+
* A session definition entry is responsible for loading session values by session value id, for a
54+
* particular definition key.
55+
*
56+
* They don't implement caching themselves, see CachedEntry for entries supporting caching.
57+
*
58+
* @param <T> the type of values this Entry handles.
59+
*/
60+
public static interface Entry<T> {
61+
/**
62+
* Returns the definition key that this entry handles.
63+
* @return the definition key that this entry handles.
64+
*/
65+
String getKey();
66+
67+
/**
68+
* Gives the value corresponding to the given valueId.
69+
*
70+
* @param valueId the id of the value to get.
71+
*
72+
* @return the value, or absent if not found.
73+
*/
74+
Optional<? extends T> getValueForId(String valueId);
6375
}
64-
private final ImmutableMap<String, LoadingCache<String, ?>> caches;
76+
77+
/**
78+
* A cached version of session definition entry.
79+
*
80+
* This does not derive from Entry, because its its getting method does not have the same semantic as the
81+
* original one: here it returns a value which may not be the freshest one, while an Entry should always
82+
* return current one.
83+
*
84+
* CachedEntry instances are usually created from a Entry instance using a EntryCacheManager.
85+
*
86+
* @param <T> the type of values this CachedEntry handles.
87+
*/
88+
public static interface CachedEntry<T> {
89+
/**
90+
* Returns the definition key that this entry handles.
91+
* @return the definition key that this entry handles.
92+
*/
93+
String getKey();
94+
95+
/**
96+
* Gives the value corresponding to the given valueId.
97+
* This value may come from a cache, so its freshness depends on configuration and implementation.
98+
*
99+
* @param valueId the id of the value to get.
100+
*
101+
* @return the value, or absent if not found.
102+
*/
103+
Optional<? extends T> getValueForId(String valueId);
104+
105+
/**
106+
* Invalidates the cache for a single value id.
107+
*
108+
* @param valueId the value id for which cache should be invalidated.
109+
*/
110+
void invalidateCacheFor(String valueId);
111+
112+
/**
113+
* Invalidates the full cache of this entry.
114+
*
115+
* This may impact more than this single entry if this entry cache is backed by a broader cache.
116+
*/
117+
void invalidateCache();
118+
}
119+
120+
/**
121+
* A cache manager for session definition entry.
122+
*
123+
* It transforms Entry into CachedEntry
124+
*/
125+
public static interface EntryCacheManager {
126+
<T> CachedEntry<T> getCachedEntry(Entry<T> entry);
127+
}
128+
129+
private final ImmutableMap<String, CachedEntry<?>> entries;
65130

66131
@SuppressWarnings("unchecked") // can't use Iterable<Entry<?> as parameter in injectable constructor ATM
67-
public Definition(Iterable<Entry> entries) {
68-
ImmutableMap.Builder<String, LoadingCache<String, ?>> builder = ImmutableMap.builder();
132+
public Definition(EntryCacheManager cacheManager, Iterable<Entry> entries) {
133+
ImmutableMap.Builder<String, CachedEntry<?>> builder = ImmutableMap.builder();
69134
for (Entry<?> entry : entries) {
70-
builder.put(entry.key, CacheBuilder.newBuilder().maximumSize(1000).build(entry.loader));
135+
builder.put(entry.getKey(), cacheManager.getCachedEntry(entry));
71136
}
72-
caches = builder.build();
137+
this.entries = builder.build();
138+
}
139+
140+
public ImmutableSet<String> entriesKeySet() {
141+
return entries.keySet();
142+
}
143+
144+
public boolean hasEntryForKey(String key) {
145+
return entries.containsKey(key);
73146
}
74147

75148
@SuppressWarnings("unchecked")
76-
public <T> LoadingCache<String, T> getCache(Class<T> clazz, String key) {
77-
return (LoadingCache<String, T>) caches.get(key);
149+
public <T> Optional<CachedEntry<T>> getEntry(String key) {
150+
return Optional.fromNullable((CachedEntry<T>) entries.get(key));
151+
}
152+
153+
public void invalidateAllCaches() {
154+
for (CachedEntry<?> entry : entries.values()) {
155+
entry.invalidateCache();
156+
}
78157
}
79158
}
80159

@@ -105,33 +184,24 @@ public static RestxSession current() {
105184
this.expires = expires;
106185
}
107186

108-
public RestxSession cleanUpCaches() {
109-
for (LoadingCache<String, ?> cache : definition.caches.values()) {
110-
cache.cleanUp();
187+
public RestxSession invalidateCaches() {
188+
for (Entry<String, String> entry : valueidsByKey.entrySet()) {
189+
definition.getEntry(entry.getKey()).get().invalidateCacheFor(entry.getValue());
111190
}
112191
return this;
113192
}
114193

115-
116194
public <T> Optional<T> get(Class<T> clazz, String key) {
117195
return getValue(definition, clazz, key, valueidsByKey.get(key));
118196
}
119197

198+
@SuppressWarnings("unchecked")
120199
static <T> Optional<T> getValue(Definition definition, Class<T> clazz, String key, String valueid) {
121200
if (valueid == null) {
122201
return Optional.absent();
123202
}
124203

125-
try {
126-
return Optional.fromNullable(definition.getCache(clazz, key).get(valueid));
127-
} catch (CacheLoader.InvalidCacheLoadException e) {
128-
// this exception is raised when cache loader returns null, which may happen if the object behind the key
129-
// is deleted. Therefore we just return an absent value
130-
return Optional.absent();
131-
} catch (ExecutionException e) {
132-
throw new RuntimeException(
133-
"impossible to load object from cache using valueid " + valueid + " for " + key + ": " + e.getMessage(), e);
134-
}
204+
return (Optional<T>) definition.getEntry(key).get().getValueForId(valueid);
135205
}
136206

137207
public RestxSession expires(Duration duration) {
@@ -143,9 +213,9 @@ public Duration getExpires() {
143213
}
144214

145215
public <T> RestxSession define(Class<T> clazz, String key, String valueid) {
146-
if (!definition.caches.containsKey(key)) {
216+
if (!definition.hasEntryForKey(key)) {
147217
throw new IllegalArgumentException("undefined context key: " + key + "." +
148-
" Keys defined are: " + definition.caches.keySet());
218+
" Keys defined are: " + definition.entriesKeySet());
149219
}
150220
// create new map by using a mutable map, not a builder, in case the the given entry overrides a previous one
151221
Map<String,String> newValueidsByKey = Maps.newHashMap();

restx-core/src/main/java/restx/security/RestxSessionCookieFilter.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
import restx.AbstractRouteLifecycleListener;
2222
import restx.RestxContext;
23-
import restx.RestxFilter;
2423
import restx.RestxHandler;
2524
import restx.RestxHandlerMatch;
2625
import restx.RestxRequest;
@@ -37,9 +36,7 @@
3736
import restx.jackson.FrontObjectMapperFactory;
3837

3938
/**
40-
* User: xavierhanin
41-
* Date: 2/8/13
42-
* Time: 8:59 PM
39+
* This filter is used to store and get a RestxSession in Cookies (one for data, one for signature).
4340
*/
4441
@Component(priority = -200)
4542
public class RestxSessionCookieFilter implements RestxRouteFilter, RestxHandler {
@@ -79,11 +76,11 @@ public Optional<RestxHandlerMatch> match(RestxRoute route) {
7976
public void handle(RestxRequestMatch match, RestxRequest req, final RestxResponse resp, RestxContext ctx) throws IOException {
8077
final RestxSession session = buildContextFromRequest(req);
8178
if (RestxContext.Modes.RECORDING.equals(ctx.getMode())) {
82-
// we clean up caches in recording mode so that each request records the cache loading
79+
// we invalidate caches in recording mode so that each request records the cache loading
8380
// Note: having this piece of code here is not a very nice isolation of responsibilities
8481
// we could put it in a separate filter, but then it's not easy to be sure it's called right after this
8582
// filter. Until such a feature is introduced, the easy solution to put it here is used.
86-
session.cleanUpCaches();
83+
sessionDefinition.invalidateAllCaches();
8784
}
8885
RestxSession.setCurrent(session);
8986
try {

0 commit comments

Comments
 (0)