diff --git a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreManager.java b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreManager.java index ce620d4..54f9521 100644 --- a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreManager.java +++ b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreManager.java @@ -16,35 +16,18 @@ package com.google.cloud.runtimes.tomcat.session; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import org.apache.catalina.Lifecycle; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.LifecycleState; -import org.apache.catalina.Session; -import org.apache.catalina.Store; -import org.apache.catalina.StoreManager; -import org.apache.catalina.session.ManagerBase; -import org.apache.catalina.session.StandardSession; -import org.apache.catalina.session.StoreBase; -import org.apache.juli.logging.Log; -import org.apache.juli.logging.LogFactory; - /** * Implementation of the {@code org.apache.catalina.Manager} interface which uses * Google Datastore to share sessions across nodes. * - *

This manager should be used in conjunction with {@link DatastoreValve} and can be used - * with {@link DatastoreStore}.
+ *

This manager should be used in conjunction with {@link KeyValuePersistentValve} and can be + * used with {@link DatastoreStore}.
* Example configuration:

* *
  *   {@code
- *   
+ *   
  *   
  *     
  *   
@@ -53,179 +36,10 @@
  *
  * 

The sessions is never stored locally and is always fetched from the Datastore.

*/ -public class DatastoreManager extends ManagerBase implements StoreManager { - - private static final Log log = LogFactory.getLog(DatastoreManager.class); - - /** - * The store will be in charge of all the interactions with the Datastore. - */ - protected Store store = null; - - /** - * {@inheritDoc} - * - *

Ensure that a store is present and initialized.

- * - * @throws LifecycleException If an error occurs during the store initialization - */ - @Override - protected synchronized void startInternal() throws LifecycleException { - super.startInternal(); +public class DatastoreManager extends KeyValuePersistentManager { - if (store == null) { - throw new LifecycleException("No Store configured, persistence disabled"); - } else if (store instanceof Lifecycle) { - ((Lifecycle) store).start(); - } - - setState(LifecycleState.STARTING); - } - - /** - * Search in the store for an existing session with the specified id. - * - * @param id The session id for the session to be returned - * @return The request session or null if a session with the requested ID could not be found - * @throws IOException If an input/output error occurs while processing this request - */ @Override - public Session findSession(String id) throws IOException { - log.debug("Datastore manager is loading session: " + id); - Session session = null; - - try { - session = this.getStore().load(id); - } catch (ClassNotFoundException ex) { - log.warn("An error occurred during session deserialization", ex); - } - - return session; - } - - /** - * {@inheritDoc} - * - *

Note: Sessions are loaded at each request therefore no session is loaded at - * initialization.

- * - * @throws ClassNotFoundException Cannot occurs - * @throws IOException Cannot occurs - */ - @Override - public void load() throws ClassNotFoundException, IOException {} - - /** - * {@inheritDoc} - * - *

Note: Sessions are persisted after each requests but never saved into the local manager, - * therefore no operation is needed during unload.

- * - * @throws IOException Cannot occurs - */ - @Override - public void unload() throws IOException {} - - /** - * Remove the Session from the manager but not from the Datastore. - * - * @param session The session to remove. - */ - @Override - public void removeSuper(Session session) { - super.remove(session); - } - - /** - * Remove this Session from the active Sessions and the Datastore. - * - * @param session The session to remove. - */ - @Override - public void remove(Session session) { - this.removeSuper(session); - - try { - store.remove(session.getId()); - } catch (IOException e) { - log.error("An error occurred while removing session with id: " + session.getId(), e); - } - } - - /** - * Returns the number of sessions present in the Store. - * - *

Note: Aggregation can be slow on the Datastore, cache the result if possible

- * - * @return the session count. - */ - @Override - public int getActiveSessionsFull() { - int sessionCount = 0; - try { - sessionCount = store.getSize(); - } catch (IOException e) { - log.error("An error occurred while counting sessions: ", e); - } - - return sessionCount; - } - - /** - * Returns a set of all sessions IDs or null if an error occurs. - * - *

Note: Listing all the keys can be slow on the Datastore.

- * - * @return The complete set of sessions IDs across the cluster. - */ - @Override - public Set getSessionIdsFull() { - Set sessionsId = null; - try { - String[] keys = this.store.keys(); - sessionsId = new HashSet<>(Arrays.asList(keys)); - } catch (IOException e) { - log.error("An error occurred while listing active sessions: ", e); - } - - return sessionsId; - } - - @Override - protected void stopInternal() throws LifecycleException { - super.stopInternal(); - - if (store instanceof Lifecycle) { - ((Lifecycle) store).stop(); - } - - setState(LifecycleState.STOPPING); - } - - @Override - public void processExpires() { - log.debug("Processing expired sessions"); - if (store instanceof StoreBase) { - ((StoreBase) store).processExpires(); - } - } - - @Override - protected StandardSession getNewSession() { + protected DatastoreSession getNewSession() { return new DatastoreSession(this); } - - public Store getStore() { - return this.store; - } - - /** - * The store will be injected by Tomcat on startup. - * - *

See distributed-sessions.xml for the configuration.

- */ - public void setStore(Store store) { - this.store = store; - store.setManager(this); - } } diff --git a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreSession.java b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreSession.java index 1d40af2..c5249ed 100644 --- a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreSession.java +++ b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreSession.java @@ -20,7 +20,6 @@ import com.google.cloud.datastore.BlobValue; import com.google.cloud.datastore.Entity; import com.google.cloud.datastore.Key; -import com.google.cloud.datastore.KeyFactory; import com.google.common.annotations.VisibleForTesting; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -28,82 +27,42 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.UncheckedIOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.function.Function; import org.apache.catalina.Manager; -import org.apache.catalina.session.StandardSession; /** - * A DatastoreSession have the same behavior as a standard session but provide utilities to interact + * A DatastoreSession has the same behavior as a standard session but provide utilities to interact * with the Datastore, such as helper for attributes and metadata serialization. */ -public class DatastoreSession extends StandardSession { +public class DatastoreSession extends KeyValuePersistentSession { - protected Set accessedAttributes; - protected Set initialAttributes; - - @VisibleForTesting - class SessionMetadata { - public static final String CREATION_TIME = "creationTime"; - public static final String LAST_ACCESSED_TIME = "lastAccessedTime"; - public static final String MAX_INACTIVE_INTERVAL = "maxInactiveInterval"; - public static final String IS_NEW = "isNew"; - public static final String IS_VALID = "isValid"; - public static final String THIS_ACCESSED_TIME = "thisAccessedTime"; - public static final String EXPIRATION_TIME = "expirationTime"; - public static final String ATTRIBUTE_VALUE_NAME = "value"; - } - - /** - * Create a new session which can be stored in the Datastore. - * @param manager The session manager which manage this session. - */ public DatastoreSession(Manager manager) { super(manager); - this.accessedAttributes = new HashSet<>(); - this.initialAttributes = new HashSet<>(); } - /** - * Restore the attributes and metadata of the session from Datastore Entities. - * - * @param entities An iterator of entity, containing the metadata and attributes of the session. - * @throws ClassNotFoundException The class in attempt to be deserialized is not present in the - * application. - * @throws IOException Error during the deserialization of the object. - */ - public void restoreFromEntities(Key sessionKey, Iterable entities) throws - ClassNotFoundException, IOException { - Entity metadataEntity = null; - List attributeEntities = new LinkedList<>(); - for (Entity entity : entities) { - if (entity.getKey().equals(sessionKey)) { - metadataEntity = entity; - } else { - attributeEntities.add(entity); - } - } + @Override + protected Key getKeyForEntity(Entity entity) { + return entity.getKey(); + } - if (metadataEntity == null) { - throw new IOException("The serialized session is missing the metadata entity"); - } + @Override + protected String getNameFromKey(Key key) { + return key.getName(); + } - restoreMetadataFromEntity(metadataEntity); - restoreAttributesFromEntity(attributeEntities); - setId(sessionKey.getName()); - initialAttributes.addAll(Collections.list(getAttributeNames())); + @Override + protected void setAttributeFromEntity(Entity entity) throws IOException, ClassNotFoundException { + String name = entity.getKey().getName(); + Blob value = entity.getBlob(SessionMetadata.ATTRIBUTE_VALUE_NAME); + try (InputStream fis = value.asInputStream(); + ObjectInputStream ois = new ObjectInputStream(fis)) { + Object attribute = ois.readObject(); + setAttribute(name, attribute, false); + } } - /** - * Restore the metadata of a session with the values contains in the entity. - * @param metadata An entity containing the metadata to restore - */ - private void restoreMetadataFromEntity(Entity metadata) { + @Override + protected void restoreMetadataFromEntity(Entity metadata) { creationTime = metadata.getLong(SessionMetadata.CREATION_TIME); lastAccessedTime = metadata.getLong(SessionMetadata.LAST_ACCESSED_TIME); maxInactiveInterval = (int) metadata.getLong(SessionMetadata.MAX_INACTIVE_INTERVAL); @@ -112,47 +71,9 @@ private void restoreMetadataFromEntity(Entity metadata) { thisAccessedTime = metadata.getLong(SessionMetadata.THIS_ACCESSED_TIME); } - /** - * Deserialize the content of each entity and add them as attribute of the session. - * @param entities The entities containing the serialized attributes. - * @throws IOException If an error occur during the deserialization - * @throws ClassNotFoundException If the class being deserialized is not present in this program. - */ - private void restoreAttributesFromEntity(Iterable entities) throws IOException, - ClassNotFoundException { - for (Entity entity : entities) { - String name = entity.getKey().getName(); - Blob value = entity.getBlob(SessionMetadata.ATTRIBUTE_VALUE_NAME); - try (InputStream fis = value.asInputStream(); - ObjectInputStream ois = new ObjectInputStream(fis)) { - Object attribute = ois.readObject(); - setAttribute(name, attribute, false); - } - } - } - - /** - * Serialize the session metadata and attributes into entities storable in the datastore. - * @param sessionKey The key of the serialized session - * @param attributeKeyFactory A key factory containing sessionKey in its ancestors, used to - * generate the key for the attributes. - * @return A list of entities containing the metadata and each attribute. - * @throws IOException If an error occur during the serialization. - */ - public List saveToEntities(Key sessionKey, KeyFactory attributeKeyFactory) throws - IOException { - List entities = saveAttributesToEntity(attributeKeyFactory); - entities.add(saveMetadataToEntity(sessionKey)); - return entities; - } - - /** - * Store the metadata of the session in an entity. - * @param sessionKey Identifier of the session on the Datastore - * @return An entity containing the metadata. - */ @VisibleForTesting - Entity saveMetadataToEntity(Key sessionKey) { + @Override + protected Entity saveMetadataToEntity(Key sessionKey) { Entity.Builder sessionEntity = Entity.newBuilder(sessionKey) .set(SessionMetadata.CREATION_TIME, getCreationTime()) .set(SessionMetadata.LAST_ACCESSED_TIME, getLastAccessedTime()) @@ -170,36 +91,8 @@ Entity saveMetadataToEntity(Key sessionKey) { return sessionEntity.build(); } - /** - * Serialize the session attributes into entities. - * @param attributeKeyFactory The key builder for the entities. - * @return A list of entities where the key correspond to the name of the attribute - and the property `value` to the serialized attribute. - * @throws IOException If an error occur during the serialization. - */ - @VisibleForTesting - List saveAttributesToEntity(KeyFactory attributeKeyFactory) throws - IOException { - Stream entities = Collections.list(getAttributeNames()).stream() - .filter(name -> accessedAttributes.contains(name)) - .filter(name -> isAttributeDistributable(name, getAttribute(name))) - .map(name -> serializeAttribute(attributeKeyFactory, name)); - - try { - return entities.collect(Collectors.toList()); - } catch (UncheckedIOException e) { - throw e.getCause(); - } - } - - /** - * Serialize an attribute an embed it into an entity whose key is generated by the provided - * KeyFactory. - * @param attributeKeyFactory The KeyFactory to use to create the key for the entity. - * @param name The name of the attribute to serialize. - * @return An Entity containing the serialized attribute. - */ - private Entity serializeAttribute(KeyFactory attributeKeyFactory, String name) { + @Override + protected Entity serializeAttribute(Function attributeKeyFunction, String name) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (ObjectOutputStream oos = new ObjectOutputStream(bos)) { oos.writeObject(getAttribute(name)); @@ -207,7 +100,7 @@ private Entity serializeAttribute(KeyFactory attributeKeyFactory, String name) { throw new UncheckedIOException(e); } - return Entity.newBuilder(attributeKeyFactory.newKey(name)) + return Entity.newBuilder(attributeKeyFunction.apply(name)) .set(SessionMetadata.ATTRIBUTE_VALUE_NAME, BlobValue.newBuilder(Blob.copyFrom(bos.toByteArray())) .setExcludeFromIndexes(true) @@ -215,29 +108,4 @@ private Entity serializeAttribute(KeyFactory attributeKeyFactory, String name) { .build(); } - /** - * List the attributes that were present at the beginning of the request and suppressed during - * its execution. This is used to reflect the suppression of attributes in the Datastore (The - * suppressed attributes would be left unchanged in the Datastore otherwise). - * @return A set of the suppressed attributes. - */ - public Set getSuppressedAttributes() { - Set suppressedAttribute = new HashSet<>(initialAttributes); - suppressedAttribute.removeAll(Collections.list(getAttributeNames())); - return suppressedAttribute; - } - - @Override - public Object getAttribute(String name) { - accessedAttributes.add(name); - return super.getAttribute(name); - } - - @Override - public void setAttribute(String name, Object value, boolean notify) { - super.setAttribute(name, value, notify); - if (notify) { - accessedAttributes.add(name); - } - } } diff --git a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreStore.java b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreStore.java index bf026a7..0e0b8b1 100644 --- a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreStore.java +++ b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreStore.java @@ -26,12 +26,7 @@ import com.google.cloud.datastore.Query; import com.google.cloud.datastore.QueryResults; import com.google.cloud.datastore.StructuredQuery.PropertyFilter; -import com.google.cloud.runtimes.tomcat.session.DatastoreSession.SessionMetadata; -import com.google.cloud.trace.Trace; -import com.google.cloud.trace.Tracer; -import com.google.cloud.trace.core.TraceContext; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Lists; import com.google.common.collect.Streams; import java.io.IOException; @@ -40,10 +35,10 @@ import java.util.Iterator; import java.util.List; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Stream; import org.apache.catalina.LifecycleException; -import org.apache.catalina.Session; -import org.apache.catalina.session.StoreBase; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; @@ -58,7 +53,7 @@ * using a manager implementation which is not using aggregations such as * {@link DatastoreManager}

*/ -public class DatastoreStore extends StoreBase { +public class DatastoreStore extends KeyValuePersistentStore { private static final Log log = LogFactory.getLog(DatastoreStore.class); @@ -74,12 +69,47 @@ public class DatastoreStore extends StoreBase { */ private String namespace; - /** - * Whether or not to send traces to Stackdriver for the operations related to session persistence. - */ - private boolean traceRequest = false; + @VisibleForTesting + static Function getAttributeKeyFunctionFromFactory(KeyFactory factory) { + return name -> factory.newKey(name); + } - private Clock clock; + @Override + protected Class getSessionType() { + return DatastoreSession.class; + } + + @Override + protected Key newKey(String name) { + return datastore.newKeyFactory().setKind(sessionKind).newKey(name); + } + + @Override + protected Iterator getEntitiesForKey(Key key) { + return datastore.run(Query.newEntityQueryBuilder() + .setKind(sessionKind) + .setFilter(PropertyFilter.hasAncestor(key)) + .build()); + } + + @Override + protected void putEntitiesToStore(List entities) { + datastore.put(entities.toArray(new FullEntity[0])); + } + + @Override + protected void deleteAttributes(Set names, Function attributeKeyFunction) { + datastore.delete(names.stream() + .map(name -> attributeKeyFunction.apply(name)) + .toArray(Key[]::new)); + } + + @Override + protected Function getAttributeKeyFunction(Key key) { + return getAttributeKeyFunctionFromFactory(datastore.newKeyFactory() + .setKind(sessionKind) + .addAncestor(PathElement.of(sessionKind, key.getName()))); + } /** * {@inheritDoc} @@ -97,10 +127,6 @@ protected synchronized void startInternal() throws LifecycleException { super.startInternal(); } - private Key newKey(String name) { - return datastore.newKeyFactory().setKind(sessionKind).newKey(name); - } - /** * Return the number of Sessions present in this Store. * @@ -147,59 +173,6 @@ public String[] keys() throws IOException { return keys; } - /** - * Load and return the Session associated with the specified session identifier from this Store, - * without removing it. If there is no such stored Session, return null. - * - *

Look in the Datastore for a serialized session and attempt to deserialize it.

- * - *

If the session is successfully deserialized, it is added to the current manager and is - * returned by this method. Otherwise null is returned.

- * - * @param id Session identifier of the session to load - * @return The loaded session instance - * @throws ClassNotFoundException If a deserialization error occurs - */ - @Override - public Session load(String id) throws ClassNotFoundException, IOException { - log.debug("Session " + id + " requested"); - TraceContext context = startSpan("Loading session"); - Key sessionKey = newKey(id); - - DatastoreSession session = deserializeSession(sessionKey); - - endSpan(context); - log.debug("Session " + id + " loaded"); - return session; - } - - /** - * Create a new session usable by Tomcat, from a serialized session in a Datastore Entity. - * @param sessionKey The key associated with the session metadata and attributes. - * @return A new session containing the metadata and attributes stored in the entity. - * @throws ClassNotFoundException Thrown if a class serialized in the entity is not available in - * this context. - * @throws IOException Thrown when an error occur during the deserialization. - */ - private DatastoreSession deserializeSession(Key sessionKey) - throws ClassNotFoundException, IOException { - TraceContext loadingSessionContext = startSpan("Fetching the session from Datastore"); - Iterator entities = datastore.run(Query.newEntityQueryBuilder() - .setKind(sessionKind) - .setFilter(PropertyFilter.hasAncestor(sessionKey)) - .build()); - endSpan(loadingSessionContext); - - DatastoreSession session = null; - if (entities.hasNext()) { - session = (DatastoreSession) manager.createEmptySession(); - TraceContext deserializationContext = startSpan("Deserialization of the session"); - session.restoreFromEntities(sessionKey, Lists.newArrayList(entities)); - endSpan(deserializationContext); - } - return session; - } - /** * Remove the Session with the specified session identifier from this Store. * If no such Session is present, this method takes no action. @@ -223,55 +196,6 @@ public void clear() throws IOException { .toArray(Key[]::new)); } - /** - * Save the specified Session into this Store. Any previously saved information for - * the associated session identifier is replaced. - * - *

Attempt to serialize the session and send it to the datastore.

- * - * @throws IOException If an error occurs during the serialization of the session. - * - * @param session Session to be saved - */ - @Override - public void save(Session session) throws IOException { - log.debug("Persisting session: " + session.getId()); - - if (!(session instanceof DatastoreSession)) { - throw new IOException( - "The session must be an instance of DatastoreSession to be serialized"); - } - DatastoreSession datastoreSession = (DatastoreSession) session; - Key sessionKey = newKey(session.getId()); - KeyFactory attributeKeyFactory = datastore.newKeyFactory() - .setKind(sessionKind) - .addAncestor(PathElement.of(sessionKind, sessionKey.getName())); - - List entities = serializeSession(datastoreSession, sessionKey, attributeKeyFactory); - - TraceContext datastoreSaveContext = startSpan("Storing the session in the Datastore"); - datastore.put(entities.toArray(new FullEntity[0])); - datastore.delete(datastoreSession.getSuppressedAttributes().stream() - .map(attributeKeyFactory::newKey) - .toArray(Key[]::new)); - endSpan(datastoreSaveContext); - } - - /** - * Serialize a session to a list of Entities that can be stored to the Datastore. - * @param session The session to serialize. - * @return A list of one or more entities containing the session and its attributes. - * @throws IOException If the session cannot be serialized. - */ - @VisibleForTesting - List serializeSession(DatastoreSession session, Key sessionKey, - KeyFactory attributeKeyFactory) throws IOException { - TraceContext serializationContext = startSpan("Serialization of the session"); - List entities = session.saveToEntities(sessionKey, attributeKeyFactory); - endSpan(serializationContext); - return entities; - } - /** * Remove expired sessions from the datastore. */ @@ -280,7 +204,7 @@ public void processExpires() { log.debug("Processing expired sessions"); Query query = Query.newKeyQueryBuilder().setKind(sessionKind) - .setFilter(PropertyFilter.le(SessionMetadata.EXPIRATION_TIME, + .setFilter(PropertyFilter.le(KeyValuePersistentSession.SessionMetadata.EXPIRATION_TIME, clock.millis())) .build(); @@ -295,23 +219,6 @@ public void processExpires() { datastore.delete(toDelete.toArray(Key[]::new)); } - @VisibleForTesting - TraceContext startSpan(String spanName) { - TraceContext context = null; - if (traceRequest) { - context = Trace.getTracer().startSpan(spanName); - } - return context; - } - - @VisibleForTesting - private void endSpan(TraceContext context) { - if (context != null) { - Tracer tracer = Trace.getTracer(); - tracer.endSpan(context); - } - } - /** * This property will be injected by Tomcat on startup. * @@ -328,21 +235,10 @@ public void setSessionKind(String sessionKind) { this.sessionKind = sessionKind; } - /** - * This property will be injected by Tomcat on startup. - */ - public void setTraceRequest(boolean traceRequest) { - this.traceRequest = traceRequest; - } @VisibleForTesting void setDatastore(Datastore datastore) { this.datastore = datastore; } - @VisibleForTesting - void setClock(Clock clock) { - this.clock = clock; - } - } diff --git a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentManager.java b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentManager.java new file mode 100644 index 0000000..7b0595b --- /dev/null +++ b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentManager.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.runtimes.tomcat.session; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Session; +import org.apache.catalina.Store; +import org.apache.catalina.StoreManager; +import org.apache.catalina.session.ManagerBase; +import org.apache.catalina.session.StandardSession; +import org.apache.catalina.session.StoreBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + * Implementation of the {@code org.apache.catalina.Manager} interface which uses a persistent + * store to share sessions across nodes. + * + *

This manager should be used in conjunction with {@link KeyValuePersistentValve} and can be + * used with {@link KeyValuePersistentStore}.
Example configuration:

+ * + * @param The type of session specific to a storage system. + */ +public abstract class KeyValuePersistentManager extends + ManagerBase implements StoreManager { + + private final Log log = LogFactory.getLog(this.getClass()); + + /** + * The store will be in charge of all the interactions with the storage. + */ + protected Store store = null; + + @Override + protected abstract S getNewSession(); + + /** + * {@inheritDoc} + * + *

Ensure that a store is present and initialized.

+ * + * @throws LifecycleException If an error occurs during the store initialization + */ + @Override + protected synchronized void startInternal() throws LifecycleException { + super.startInternal(); + + if (store == null) { + throw new LifecycleException("No Store configured, persistence disabled"); + } else if (store instanceof Lifecycle) { + ((Lifecycle) store).start(); + } + + setState(LifecycleState.STARTING); + } + + /** + * Search in the store for an existing session with the specified id. + * + * @param id The session id for the session to be returned + * @return The request session or null if a session with the requested ID could not be found + * @throws IOException If an input/output error occurs while processing this request + */ + @Override + public Session findSession(String id) throws IOException { + log.debug("KeyValuePersistentManager is loading session: " + id); + Session session = null; + + try { + session = this.getStore().load(id); + } catch (ClassNotFoundException ex) { + log.warn("An error occurred during session deserialization", ex); + } + + return session; + } + + /** + * {@inheritDoc} + * + *

Note: Sessions are loaded at each request therefore no session is loaded at + * initialization.

+ * + * @throws ClassNotFoundException Cannot occurs + * @throws IOException Cannot occurs + */ + @Override + public void load() throws ClassNotFoundException, IOException { + } + + /** + * {@inheritDoc} + * + *

Note: Sessions are persisted after each requests but never saved into the local manager, + * therefore no operation is needed during unload.

+ * + * @throws IOException Cannot occurs + */ + @Override + public void unload() throws IOException { + } + + /** + * Remove the Session from the manager but not from the storage. + * + * @param session The session to remove. + */ + @Override + public void removeSuper(Session session) { + super.remove(session); + } + + /** + * Remove this Session from the active Sessions and the storage. + * + * @param session The session to remove. + */ + @Override + public void remove(Session session) { + this.removeSuper(session); + + try { + store.remove(session.getId()); + } catch (IOException e) { + log.error("An error occurred while removing session with id: " + session.getId(), e); + } + } + + /** + * Returns the number of sessions present in the Store. + * + * @return the session count. + */ + @Override + public int getActiveSessionsFull() { + int sessionCount = 0; + try { + sessionCount = store.getSize(); + } catch (IOException e) { + log.error("An error occurred while counting sessions: ", e); + } + + return sessionCount; + } + + /** + * Returns a set of all sessions IDs or null if an error occurs. + * + * @return The complete set of sessions IDs across the cluster. + */ + @Override + public Set getSessionIdsFull() { + Set sessionsId = null; + try { + String[] keys = this.store.keys(); + sessionsId = new HashSet<>(Arrays.asList(keys)); + } catch (IOException e) { + log.error("An error occurred while listing active sessions: ", e); + } + + return sessionsId; + } + + @Override + protected void stopInternal() throws LifecycleException { + super.stopInternal(); + + if (store instanceof Lifecycle) { + ((Lifecycle) store).stop(); + } + + setState(LifecycleState.STOPPING); + } + + @Override + public void processExpires() { + log.debug("Processing expired sessions"); + if (store instanceof StoreBase) { + ((StoreBase) store).processExpires(); + } + } + + public Store getStore() { + return this.store; + } + + /** + * The store will be injected by Tomcat on startup. + * + *

See distributed-sessions.xml for the configuration.

+ */ + public void setStore(Store store) { + this.store = store; + store.setManager(this); + } +} diff --git a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentSession.java b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentSession.java new file mode 100644 index 0000000..bdb1524 --- /dev/null +++ b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentSession.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.runtimes.tomcat.session; + +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.catalina.Manager; +import org.apache.catalina.session.StandardSession; + +/** + * A partial implementation of the {@link StandardSession} meant to work with persistent key-value + * storage. + * + * @param The key type of the storage + * @param The entity type that holds the stored values + */ +public abstract class KeyValuePersistentSession extends StandardSession { + + protected Set accessedAttributes; + protected Set initialAttributes; + + /** + * Create a new session which can be stored in the storage. + * + * @param manager The session manager which manage this session. + */ + public KeyValuePersistentSession(Manager manager) { + super(manager); + this.accessedAttributes = new HashSet<>(); + this.initialAttributes = new HashSet<>(); + } + + protected abstract K getKeyForEntity(E entity); + + protected abstract String getNameFromKey(K key); + + protected abstract void setAttributeFromEntity(E entity) + throws IOException, ClassNotFoundException; + + /** + * Serialize an attribute an embed it into an entity whose key is generated by the provided key + * function. + * + * @param attributeKeyFunction The function to use to create the key for the entity. + * @param name The name of the attribute to serialize. + * @return An Entity containing the serialized attribute. + */ + protected abstract E serializeAttribute(Function attributeKeyFunction, + String name); + + /** + * Restore the metadata of a session with the values contains in the entity. + * + * @param metadata An entity containing the metadata to restore + */ + protected abstract void restoreMetadataFromEntity(E metadata); + + /** + * Store the metadata of the session in an entity. + * + * @param sessionKey Identifier of the session on the storage + * @return An entity containing the metadata. + */ + @VisibleForTesting + protected abstract E saveMetadataToEntity(K sessionKey); + + /** + * Restore the attributes and metadata of the session from storage entities. + * + * @param entities An iterator of entity, containing the metadata and attributes of the session. + * @throws ClassNotFoundException The class in attempt to be deserialized is not present. + * @throws IOException Error during the deserialization of the object. + */ + public void restoreFromEntities(K sessionKey, Iterable entities) throws + ClassNotFoundException, IOException { + E metadataEntity = null; + List attributeEntities = new LinkedList<>(); + for (E entity : entities) { + if (getKeyForEntity(entity).equals(sessionKey)) { + metadataEntity = entity; + } else { + attributeEntities.add(entity); + } + } + + if (metadataEntity == null) { + throw new IOException("The serialized session is missing the metadata entity"); + } + + restoreMetadataFromEntity(metadataEntity); + restoreAttributesFromEntity(attributeEntities); + setId(getNameFromKey(sessionKey)); + initialAttributes.addAll(Collections.list(getAttributeNames())); + } + + /** + * Deserialize the content of each entity and add them as attribute of the session. + * + * @param entities The entities containing the serialized attributes. + * @throws IOException If an error occur during the deserialization + * @throws ClassNotFoundException If the class being deserialized is not present in this program. + */ + private void restoreAttributesFromEntity(Iterable entities) throws IOException, + ClassNotFoundException { + for (E entity : entities) { + setAttributeFromEntity(entity); + } + } + + /** + * Serialize the session metadata and attributes into entities storable in the storage. + * + * @param sessionKey The key of the serialized session + * @param attributeKeyFunction A key function used to generate the key for the attributes. + * @return A list of entities containing the metadata and each attribute. + * @throws IOException If an error occur during the serialization. + */ + public List saveToEntities(K sessionKey, Function attributeKeyFunction) + throws + IOException { + List entities = saveAttributesToEntity(attributeKeyFunction); + entities.add(saveMetadataToEntity(sessionKey)); + return entities; + } + + /** + * Serialize the session attributes into entities. + * + * @param attributeKeyFunction The key function for the entities. + * @return A list of entities where the key correspond to the name of the attribute and the + * property `value` to the serialized attribute. + * @throws IOException If an error occur during the serialization. + */ + @VisibleForTesting + List saveAttributesToEntity(Function attributeKeyFunction) throws + IOException { + Stream entities = Collections.list(getAttributeNames()).stream() + .filter(name -> accessedAttributes.contains(name)) + .filter(name -> isAttributeDistributable(name, getAttribute(name))) + .map(name -> serializeAttribute(attributeKeyFunction, name)); + + try { + return entities.collect(Collectors.toList()); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + + /** + * List the attributes that were present at the beginning of the request and suppressed during its + * execution. This is used to reflect the suppression of attributes in the storage (The suppressed + * attributes would be left unchanged in the storage otherwise). + * + * @return A set of the suppressed attributes. + */ + public Set getRemovedAttributes() { + Set removedAttributes = new HashSet<>(initialAttributes); + removedAttributes.removeAll(Collections.list(getAttributeNames())); + return removedAttributes; + } + + @Override + public Object getAttribute(String name) { + accessedAttributes.add(name); + return super.getAttribute(name); + } + + @Override + public void setAttribute(String name, Object value, boolean notify) { + super.setAttribute(name, value, notify); + if (notify) { + accessedAttributes.add(name); + } + } + + @VisibleForTesting + class SessionMetadata { + + public static final String CREATION_TIME = "creationTime"; + public static final String LAST_ACCESSED_TIME = "lastAccessedTime"; + public static final String MAX_INACTIVE_INTERVAL = "maxInactiveInterval"; + public static final String IS_NEW = "isNew"; + public static final String IS_VALID = "isValid"; + public static final String THIS_ACCESSED_TIME = "thisAccessedTime"; + public static final String EXPIRATION_TIME = "expirationTime"; + public static final String ATTRIBUTE_VALUE_NAME = "value"; + } +} diff --git a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentStore.java b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentStore.java new file mode 100644 index 0000000..22b4c4e --- /dev/null +++ b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentStore.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.runtimes.tomcat.session; + +import com.google.cloud.trace.Trace; +import com.google.cloud.trace.Tracer; +import com.google.cloud.trace.core.TraceContext; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import java.io.IOException; +import java.time.Clock; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.apache.catalina.Session; +import org.apache.catalina.session.StoreBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Partial implementation of {@link StoreBase}. Contains logic common to key-value persistent + * session storage while leaving the specific storage types as parameters. + * @param the key type of the storage + * @param The session type based on the key and entity types + * @param the storage entity type of the storage + */ +public abstract class KeyValuePersistentStore, E> + extends StoreBase { + + private final Log log = LogFactory.getLog(this.getClass()); + protected Clock clock; + /** + * Whether or not to send traces to Stackdriver for the operations related to session + * persistence. + */ + private boolean traceRequest = false; + + protected abstract Class getSessionType(); + + protected abstract K newKey(String name); + + protected abstract Iterator getEntitiesForKey(K key); + + protected abstract void putEntitiesToStore(List entities); + + protected abstract void deleteAttributes(Set names, + Function attributeKeyFunction); + + protected abstract Function getAttributeKeyFunction(K key); + + /** + * Load and return the Session associated with the specified session identifier from this Store, + * without removing it. If there is no such stored Session, return null. + * + *

Look in the storage for a serialized session and attempt to deserialize it.

+ * + *

If the session is successfully deserialized, it is added to the current manager and is + * returned by this method. Otherwise null is returned.

+ * + * @param id Session identifier of the session to load + * @return The loaded session instance + * @throws ClassNotFoundException If a deserialization error occurs + */ + @Override + public Session load(String id) throws ClassNotFoundException, IOException { + log.debug("Session " + id + " requested"); + TraceContext context = startSpan("Loading session"); + K sessionKey = newKey(id); + + S session = deserializeSession(sessionKey); + + endSpan(context); + log.debug("Session " + id + " loaded"); + return session; + } + + /** + * Create a new session usable by Tomcat, from a serialized session in a storage entity. + * + * @param sessionKey The key associated with the session metadata and attributes. + * @return A new session containing the metadata and attributes stored in the entity. + * @throws ClassNotFoundException Thrown if a class serialized in the entity is not available in + * this context. + * @throws IOException Thrown when an error occur during the deserialization. + */ + private S deserializeSession(K sessionKey) + throws ClassNotFoundException, IOException { + TraceContext loadingSessionContext = startSpan( + "Fetching the session from KeyValuePersistentStore"); + Iterator entities = getEntitiesForKey(sessionKey); + endSpan(loadingSessionContext); + + S session = null; + if (entities.hasNext()) { + session = (S) manager.createEmptySession(); + TraceContext deserializationContext = startSpan("Deserialization of the session"); + session.restoreFromEntities(sessionKey, Lists.newArrayList(entities)); + endSpan(deserializationContext); + } + return session; + } + + /** + * Save the specified Session into this Store. Any previously saved information for the associated + * session identifier is replaced. + * + *

Attempt to serialize the session and send it to the storage.

+ * + * @param session Session to be saved + * @throws IOException If an error occurs during the serialization of the session. + */ + @Override + public void save(Session session) throws IOException { + log.debug("Persisting session: " + session.getId()); + + if (!(session.getClass().equals(getSessionType()))) { + throw new IOException( + "The session must be an instance of " + getSessionType().getSimpleName() + + " to be serialized"); + } + S typedSession = (S) session; + K sessionKey = newKey(session.getId()); + Function attributeKeyFunction = getAttributeKeyFunction(sessionKey); + List entities = serializeSession(typedSession, sessionKey, attributeKeyFunction); + + TraceContext datastoreSaveContext = startSpan("Storing the session in the Datastore"); + putEntitiesToStore(entities); + deleteAttributes(typedSession.getRemovedAttributes(), attributeKeyFunction); + endSpan(datastoreSaveContext); + } + + /** + * Serialize a session to a list of Entities that can be stored to the storage. + * + * @param session The session to serialize. + * @return A list of one or more entities containing the session and its attributes. + * @throws IOException If the session cannot be serialized. + */ + @VisibleForTesting + List serializeSession(S session, K sessionKey, + Function attributeKeyFunction) throws IOException { + TraceContext serializationContext = startSpan("Serialization of the session"); + List entities = session.saveToEntities(sessionKey, attributeKeyFunction); + endSpan(serializationContext); + return entities; + } + + @VisibleForTesting + TraceContext startSpan(String spanName) { + TraceContext context = null; + if (traceRequest) { + context = Trace.getTracer().startSpan(spanName); + } + return context; + } + + @VisibleForTesting + private void endSpan(TraceContext context) { + if (context != null) { + Tracer tracer = Trace.getTracer(); + tracer.endSpan(context); + } + } + + /** + * This property will be injected by Tomcat on startup. + */ + public void setTraceRequest(boolean traceRequest) { + this.traceRequest = traceRequest; + } + + @VisibleForTesting + void setClock(Clock clock) { + this.clock = clock; + } + +} diff --git a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreValve.java b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentValve.java similarity index 95% rename from tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreValve.java rename to tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentValve.java index dc352fe..331f96b 100644 --- a/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/DatastoreValve.java +++ b/tomcat-gcp-lib/src/main/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentValve.java @@ -30,13 +30,12 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; - /** * This valve uses the Store Manager to persist the session after each request. */ -public class DatastoreValve extends ValveBase { +public class KeyValuePersistentValve extends ValveBase { - private static final Log log = LogFactory.getLog(DatastoreValve.class); + private static final Log log = LogFactory.getLog(KeyValuePersistentValve.class); private String uriExcludePattern; diff --git a/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreSessionTest.java b/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreSessionTest.java index 5adf9ae..ab5354d 100644 --- a/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreSessionTest.java +++ b/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreSessionTest.java @@ -11,7 +11,6 @@ import com.google.cloud.datastore.Entity; import com.google.cloud.datastore.Key; import com.google.cloud.datastore.KeyFactory; -import com.google.cloud.runtimes.tomcat.session.DatastoreSession.SessionMetadata; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.NotSerializableException; @@ -21,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.catalina.Context; import org.apache.catalina.Manager; @@ -49,12 +49,12 @@ public void setUp() throws Exception { @Test public void testMetadataDeserialization() throws Exception { Entity metadata = Entity.newBuilder(sessionKey) - .set(SessionMetadata.MAX_INACTIVE_INTERVAL, 0) - .set(SessionMetadata.CREATION_TIME, 1) - .set(SessionMetadata.LAST_ACCESSED_TIME, 2) - .set(SessionMetadata.IS_NEW, true) - .set(SessionMetadata.IS_VALID, true) - .set(SessionMetadata.THIS_ACCESSED_TIME, 3) + .set(KeyValuePersistentSession.SessionMetadata.MAX_INACTIVE_INTERVAL, 0) + .set(KeyValuePersistentSession.SessionMetadata.CREATION_TIME, 1) + .set(KeyValuePersistentSession.SessionMetadata.LAST_ACCESSED_TIME, 2) + .set(KeyValuePersistentSession.SessionMetadata.IS_NEW, true) + .set(KeyValuePersistentSession.SessionMetadata.IS_VALID, true) + .set(KeyValuePersistentSession.SessionMetadata.THIS_ACCESSED_TIME, 3) .build(); DatastoreSession session = new DatastoreSession(sessionManager); @@ -98,7 +98,7 @@ public void testAttributesSerializationKey() throws Exception { session.setAttribute("map", new HashMap<>()); KeyFactory factory = new KeyFactory("project").setKind("kind"); - List entities = session.saveAttributesToEntity(factory); + List entities = session.saveAttributesToEntity(DatastoreStore.getAttributeKeyFunctionFromFactory(factory)); assertTrue(entities.stream() .map(BaseEntity::getKey) @@ -115,7 +115,7 @@ public void testSerializationCycle() throws Exception { initialSession.setAttribute("map", Collections.singletonMap("key", "value")); KeyFactory keyFactory = new KeyFactory("project").setKind("kind"); - List attributes = initialSession.saveToEntities(sessionKey, keyFactory); + List attributes = initialSession.saveToEntities(sessionKey,DatastoreStore.getAttributeKeyFunctionFromFactory( keyFactory)); DatastoreSession restoredSession = new DatastoreSession(sessionManager); restoredSession.restoreFromEntities(sessionKey, attributes); @@ -135,7 +135,7 @@ public void testSerializationError() throws Exception { when(session.isAttributeDistributable(any(), any())).thenReturn(true); when(session.getAttribute("count")).thenReturn(sessionManager); - session.saveAttributesToEntity(new KeyFactory("project").setKind("kind")); + session.saveAttributesToEntity(DatastoreStore.getAttributeKeyFunctionFromFactory(new KeyFactory("project").setKind("kind"))); } @Test(expected = IOException.class) diff --git a/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreStoreTest.java b/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreStoreTest.java index 051d1f1..a194f95 100644 --- a/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreStoreTest.java +++ b/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreStoreTest.java @@ -192,7 +192,7 @@ public void testDecomposedSessionLoad() throws Exception { KeyFactory attributeKeyFactory = datastore.newKeyFactory() .setKind("kind") .addAncestor(PathElement.of("kind", key.getName())); - List entities = session.saveToEntities(key, attributeKeyFactory); + List entities = session.saveToEntities(key, DatastoreStore.getAttributeKeyFunctionFromFactory(attributeKeyFactory)); QueryResults queryResults = new IteratorQueryResults<>(entities.iterator()); when(datastore.run(any())).thenReturn(queryResults); @@ -217,7 +217,7 @@ public void testSerializationCycleWithAttributeRemoval() throws Exception { .addAncestor(PathElement.of("kind", key.getName())); List initialSessionEntities = store.serializeSession(initialSession, key, - attributeKeyFactory); + DatastoreStore.getAttributeKeyFunctionFromFactory(attributeKeyFactory)); // Load the session and remove the map attribute when(datastore.run(any())).thenReturn( diff --git a/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreValveTest.java b/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentValveTest.java similarity index 92% rename from tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreValveTest.java rename to tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentValveTest.java index d9cf958..6b6db5f 100644 --- a/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/DatastoreValveTest.java +++ b/tomcat-gcp-lib/src/test/java/com/google/cloud/runtimes/tomcat/session/KeyValuePersistentValveTest.java @@ -34,10 +34,10 @@ import org.mockito.MockitoAnnotations; /** - * Ensures that {@code DatastoreValve} is persisting the session in the store at the end - * of each request. + * Ensures that {@code KeyValuePersistentValve} is persisting the session in the store at the end of + * each request. */ -public class DatastoreValveTest { +public class KeyValuePersistentValveTest { @Mock private Context context; @@ -60,7 +60,7 @@ public class DatastoreValveTest { @Mock private Valve nextValve = mock(Valve.class); - private DatastoreValve valve; + private KeyValuePersistentValve valve; @Before public void setUp() throws Exception { @@ -70,7 +70,7 @@ public void setUp() throws Exception { when(manager.getStore()).thenReturn(store); when(request.getSessionInternal(anyBoolean())).thenReturn(session); when(request.getContext()).thenReturn(context); - valve = new DatastoreValve(); + valve = new KeyValuePersistentValve(); } @Test diff --git a/tomcat/src/main/resources/config/distributed-sessions.xml b/tomcat/src/main/resources/config/distributed-sessions.xml index 1e5164e..8104b69 100644 --- a/tomcat/src/main/resources/config/distributed-sessions.xml +++ b/tomcat/src/main/resources/config/distributed-sessions.xml @@ -1,4 +1,4 @@ -