Skip to content

Commit

Permalink
Merge pull request #143 from restx/review-restx-session
Browse files Browse the repository at this point in the history
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 62f734b
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 68 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
162 changes: 116 additions & 46 deletions restx-core/src/main/java/restx/security/RestxSession.java
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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();
}
}
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import restx.AbstractRouteLifecycleListener;
import restx.RestxContext;
import restx.RestxFilter;
import restx.RestxHandler;
import restx.RestxHandlerMatch;
import restx.RestxRequest;
Expand All @@ -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 {
Expand Down Expand Up @@ -79,11 +76,11 @@ public Optional<RestxHandlerMatch> match(RestxRoute route) {
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 {
Expand Down
Loading

0 comments on commit 62f734b

Please sign in to comment.