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 @@
-