Permalink
Browse files

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.
  • Loading branch information...
xhanin committed Jan 20, 2015
2 parents 679c273 + 378893b commit 62f734baea8bb1d79f7fa137d3c373e9b79114ae
@@ -0,0 +1,29 @@
+package restx.security;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import restx.security.RestxSession.Definition.Entry;
+
+/**
+ * Date: 20/1/15
+ * Time: 20:56
+ */
+public class DefaultSessionDefinitionEntry<T> implements Entry<T> {
+ private final String sessionDefKey;
+ private final Function<String, Optional<? extends T>> function;
+
+ public DefaultSessionDefinitionEntry(Class<T> clazz, String sessionDefKey, Function<String, Optional<? extends T>> function) {
+ this.sessionDefKey = sessionDefKey;
+ this.function = function;
+ }
+
+ @Override
+ public String getKey() {
+ return sessionDefKey;
+ }
+
+ @Override
+ public Optional<? extends T> getValueForId(String valueId) {
+ return function.apply(valueId);
+ }
+}
@@ -0,0 +1,87 @@
+package restx.security;
+
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import restx.security.RestxSession.Definition.CachedEntry;
+import restx.security.RestxSession.Definition.Entry;
+import restx.security.RestxSession.Definition.EntryCacheManager;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A restx session entry cache manager based on guava cache.
+ *
+ * You can override the cache settings by overriding the getCacheBuilder() method.
+ *
+ * Note that Guava Cache is not distributed, so be very careful with cache invalidation
+ * when using this cache.
+ *
+ * This is the default EntryCacheManager, see SecurityModule which provides one.
+ */
+public class GuavaEntryCacheManager implements EntryCacheManager {
+ @Override
+ public <T> CachedEntry<T> getCachedEntry(Entry<T> entry) {
+ return new GuavaCacheSessionDefinitionEntry<T>(entry.getKey(), getLoadingCacheFor(entry));
+ }
+
+ protected <T> LoadingCache<String, T> getLoadingCacheFor(final Entry<T> entry) {
+ return getCacheBuilder(entry).build(getCacheLoaderFor(entry));
+ }
+
+ protected <T> CacheLoader<String, T> getCacheLoaderFor(final Entry<T> entry) {
+ return new CacheLoader<String, T>() {
+ @Override
+ public T load(String key) throws Exception {
+ return entry.getValueForId(key).orNull();
+ }
+ };
+ }
+
+ protected <T> CacheBuilder<Object, Object> getCacheBuilder(Entry<T> entry) {
+ return CacheBuilder.newBuilder().maximumSize(1000);
+ }
+
+ /**
+ * A session definition entry implementation using Guava Cache.
+ */
+ public static class GuavaCacheSessionDefinitionEntry<T> implements CachedEntry<T> {
+ private final LoadingCache<String, T> loadingCache;
+ private final String key;
+
+ public GuavaCacheSessionDefinitionEntry(String key, LoadingCache<String, T> loadingCache) {
+ this.key = key;
+ this.loadingCache = loadingCache;
+ }
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public Optional<T> getValueForId(String valueId) {
+ try {
+ return Optional.fromNullable(loadingCache.get(valueId));
+ } catch (CacheLoader.InvalidCacheLoadException e) {
+ // this exception is raised when cache loader returns null, which may happen if the object behind the key
+ // is deleted. Therefore we just return an absent value
+ return Optional.absent();
+ } catch (ExecutionException e) {
+ throw new RuntimeException(
+ "impossible to load object from cache using valueid " + valueId + " for " + key + ": " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void invalidateCacheFor(String valueId) {
+ loadingCache.invalidate(valueId);
+ }
+
+ @Override
+ public void invalidateCache() {
+ loadingCache.invalidateAll();
+ }
+ }
+}
@@ -1,36 +1,31 @@
package restx.security;
import com.google.common.base.Optional;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import org.joda.time.Duration;
import restx.factory.Component;
import java.util.Map;
-import java.util.concurrent.ExecutionException;
+import java.util.Map.Entry;
/**
* RestxSession is used to store information which can be used across several HTTP requests from the same client.
*
* It is organized as a Map, information is stored by keys.
*
- * It doesn't use the JEE Session mechanism, but a more lightweight system relying on a signed session cookie
- * (therefore it cannot be tampered by the client).
+ * It doesn't use the JEE Session mechanism, but a more lightweight system relying on signed session data stored
+ * on the client (being signed, it cannot be tampered by the client).
*
- * The session cookie doesn't store the whole values (which could put a high load on the network and even cause
- * problems related to cookie size limit), but rather stores a a value id for each session key.
+ * The session data doesn't store the whole values (which could put a high load on the network and even cause
+ * problems related to data storage limit on the client), but rather stores a a value id for each session key.
*
* A value id MUST identify uniquely a value when used for a given session key, and the session MUST be configured
* with a CacheLoader per key, able to load the value corresponding to the value id for a particular key.
*
* Therefore on the server the session enables to access arbitrary large objects, it will only put pressure on a
- * server cache, and on cache loaders if requests are heavily distributed. Indeed the cache is not distributed,
- * so a in a large clustered environment cache miss will be very likely and cache loaders will often be called.
- * Hence in such environment you should be careful to use very efficient cache loaders if you rely heavily on
- * session.
+ * server cache, and on cache loaders if requests are heavily distributed and depending on cache implementation.
*
* An example (using an arbitrary json like notation):
* <pre>
@@ -39,42 +34,126 @@
* "USER": (valueId) -&gt; { return db.findOne("{_id: #}", valueId).as(User.class); }
* }
* "valueIdsByKeys": {
- * "USER": "johndoe@acme.com" // valued from session cookie
+ * "USER": "johndoe@acme.com" // valued from client session data
* }
* }
* </pre>
* With such a restx session, when you call a #get(User.class, "USER"), the session will first check its
* valueIdsByKeys map to find the corresponding valueId ("johndoe@acme.com"). Then it will check the cache for
* this valueId, and in case of cache miss will use the provided cache loader which will load the user from db.
+ *
+ * If you want to define your own session keys, you should define a Definition.Entry component for it allowing
+ * to load values based on their ids. You don't have to take care of caching in the Entry, caching is performed
+ * by EntryCacheManager.
*/
public class RestxSession {
@Component
public static class Definition {
- public static class Entry<T> {
- private final Class<T> clazz;
- private final String key;
- private final CacheLoader<String, T> loader;
-
- public Entry(Class<T> clazz, String key, CacheLoader<String, T> loader) {
- this.clazz = clazz;
- this.key = key;
- this.loader = loader;
- }
+ /**
+ * A session definition entry is responsible for loading session values by session value id, for a
+ * particular definition key.
+ *
+ * They don't implement caching themselves, see CachedEntry for entries supporting caching.
+ *
+ * @param <T> the type of values this Entry handles.
+ */
+ public static interface Entry<T> {
+ /**
+ * Returns the definition key that this entry handles.
+ * @return the definition key that this entry handles.
+ */
+ String getKey();
+
+ /**
+ * Gives the value corresponding to the given valueId.
+ *
+ * @param valueId the id of the value to get.
+ *
+ * @return the value, or absent if not found.
+ */
+ Optional<? extends T> getValueForId(String valueId);
}
- private final ImmutableMap<String, LoadingCache<String, ?>> caches;
+
+ /**
+ * A cached version of session definition entry.
+ *
+ * This does not derive from Entry, because its its getting method does not have the same semantic as the
+ * original one: here it returns a value which may not be the freshest one, while an Entry should always
+ * return current one.
+ *
+ * CachedEntry instances are usually created from a Entry instance using a EntryCacheManager.
+ *
+ * @param <T> the type of values this CachedEntry handles.
+ */
+ public static interface CachedEntry<T> {
+ /**
+ * Returns the definition key that this entry handles.
+ * @return the definition key that this entry handles.
+ */
+ String getKey();
+
+ /**
+ * Gives the value corresponding to the given valueId.
+ * This value may come from a cache, so its freshness depends on configuration and implementation.
+ *
+ * @param valueId the id of the value to get.
+ *
+ * @return the value, or absent if not found.
+ */
+ Optional<? extends T> getValueForId(String valueId);
+
+ /**
+ * Invalidates the cache for a single value id.
+ *
+ * @param valueId the value id for which cache should be invalidated.
+ */
+ void invalidateCacheFor(String valueId);
+
+ /**
+ * Invalidates the full cache of this entry.
+ *
+ * This may impact more than this single entry if this entry cache is backed by a broader cache.
+ */
+ void invalidateCache();
+ }
+
+ /**
+ * A cache manager for session definition entry.
+ *
+ * It transforms Entry into CachedEntry
+ */
+ public static interface EntryCacheManager {
+ <T> CachedEntry<T> getCachedEntry(Entry<T> entry);
+ }
+
+ private final ImmutableMap<String, CachedEntry<?>> entries;
@SuppressWarnings("unchecked") // can't use Iterable<Entry<?> as parameter in injectable constructor ATM
- public Definition(Iterable<Entry> entries) {
- ImmutableMap.Builder<String, LoadingCache<String, ?>> builder = ImmutableMap.builder();
+ public Definition(EntryCacheManager cacheManager, Iterable<Entry> entries) {
+ ImmutableMap.Builder<String, CachedEntry<?>> builder = ImmutableMap.builder();
for (Entry<?> entry : entries) {
- builder.put(entry.key, CacheBuilder.newBuilder().maximumSize(1000).build(entry.loader));
+ builder.put(entry.getKey(), cacheManager.getCachedEntry(entry));
}
- caches = builder.build();
+ this.entries = builder.build();
+ }
+
+ public ImmutableSet<String> entriesKeySet() {
+ return entries.keySet();
+ }
+
+ public boolean hasEntryForKey(String key) {
+ return entries.containsKey(key);
}
@SuppressWarnings("unchecked")
- public <T> LoadingCache<String, T> getCache(Class<T> clazz, String key) {
- return (LoadingCache<String, T>) caches.get(key);
+ public <T> Optional<CachedEntry<T>> getEntry(String key) {
+ return Optional.fromNullable((CachedEntry<T>) entries.get(key));
+ }
+
+ public void invalidateAllCaches() {
+ for (CachedEntry<?> entry : entries.values()) {
+ entry.invalidateCache();
+ }
}
}
@@ -105,33 +184,24 @@ public static RestxSession current() {
this.expires = expires;
}
- public RestxSession cleanUpCaches() {
- for (LoadingCache<String, ?> cache : definition.caches.values()) {
- cache.cleanUp();
+ public RestxSession invalidateCaches() {
+ for (Entry<String, String> entry : valueidsByKey.entrySet()) {
+ definition.getEntry(entry.getKey()).get().invalidateCacheFor(entry.getValue());
}
return this;
}
-
public <T> Optional<T> get(Class<T> clazz, String key) {
return getValue(definition, clazz, key, valueidsByKey.get(key));
}
+ @SuppressWarnings("unchecked")
static <T> Optional<T> getValue(Definition definition, Class<T> clazz, String key, String valueid) {
if (valueid == null) {
return Optional.absent();
}
- try {
- return Optional.fromNullable(definition.getCache(clazz, key).get(valueid));
- } catch (CacheLoader.InvalidCacheLoadException e) {
- // this exception is raised when cache loader returns null, which may happen if the object behind the key
- // is deleted. Therefore we just return an absent value
- return Optional.absent();
- } catch (ExecutionException e) {
- throw new RuntimeException(
- "impossible to load object from cache using valueid " + valueid + " for " + key + ": " + e.getMessage(), e);
- }
+ return (Optional<T>) definition.getEntry(key).get().getValueForId(valueid);
}
public RestxSession expires(Duration duration) {
@@ -143,9 +213,9 @@ public Duration getExpires() {
}
public <T> RestxSession define(Class<T> clazz, String key, String valueid) {
- if (!definition.caches.containsKey(key)) {
+ if (!definition.hasEntryForKey(key)) {
throw new IllegalArgumentException("undefined context key: " + key + "." +
- " Keys defined are: " + definition.caches.keySet());
+ " Keys defined are: " + definition.entriesKeySet());
}
// create new map by using a mutable map, not a builder, in case the the given entry overrides a previous one
Map<String,String> newValueidsByKey = Maps.newHashMap();
@@ -20,7 +20,6 @@
import restx.AbstractRouteLifecycleListener;
import restx.RestxContext;
-import restx.RestxFilter;
import restx.RestxHandler;
import restx.RestxHandlerMatch;
import restx.RestxRequest;
@@ -37,9 +36,7 @@
import restx.jackson.FrontObjectMapperFactory;
/**
- * User: xavierhanin
- * Date: 2/8/13
- * Time: 8:59 PM
+ * This filter is used to store and get a RestxSession in Cookies (one for data, one for signature).
*/
@Component(priority = -200)
public class RestxSessionCookieFilter implements RestxRouteFilter, RestxHandler {
@@ -79,11 +76,11 @@ public RestxSessionCookieFilter(
public void handle(RestxRequestMatch match, RestxRequest req, final RestxResponse resp, RestxContext ctx) throws IOException {
final RestxSession session = buildContextFromRequest(req);
if (RestxContext.Modes.RECORDING.equals(ctx.getMode())) {
- // we clean up caches in recording mode so that each request records the cache loading
+ // we invalidate caches in recording mode so that each request records the cache loading
// Note: having this piece of code here is not a very nice isolation of responsibilities
// we could put it in a separate filter, but then it's not easy to be sure it's called right after this
// filter. Until such a feature is introduced, the easy solution to put it here is used.
- session.cleanUpCaches();
+ sessionDefinition.invalidateAllCaches();
}
RestxSession.setCurrent(session);
try {
Oops, something went wrong.

0 comments on commit 62f734b

Please sign in to comment.