A visual aid to help you understand the issue.
I am baffled because this issue has occurred 100% of the time on every application that I've tried to launch, and yet I can't find any information about this issue, and I can't find anybody else on the Internet who has had this issue.
I've been learning web development with Java using Google Cloud Platform for about a year now, and I have a problem that I've been stuck on for a couple months now. I can't seem to find ANY mention of this problem anywhere online.
I have a web application built on Java servlets, and it uses sessions to keep track of the logged-in user. Everything in the application works fine, but when I deployed the app to App Engine, the user kept randomly getting logged in and out with subsequent server requests. I watched in the development window and saw that the session ID kept changing between two different values.
Because app engine uses multiple instances of an app for scaling with traffic and resource management, it may receive a request on one instance and send it back to the client from another instance. This makes no noticeable difference on the client side, but I discovered that each instance had a different session ID for my client, causing different session attributes to get trapped in each instance.
Surely enough, when I reduced the app down to one instance, my sessions worked fine. But that won't work in production, I need my app to be able to scale with traffic and utilize the app engine resources as they were intended.
Google CLAIMS that sessions on app engine are handled automatically using Memcache and Datastore, and that all you need to do to access the session is use request.getSession()
, so I don't understand why this problem is occurring. If I can't understand the problem, I will never find a solution :(
[Edit]: The app uses a pre-existing Web Filter called DatastoreSessionFilter to keep track of the session variables using cookies and datastore, but it doesn't seem to be working properly, since my session variables are getting trapped in individual instances. Here is the DatastoreSessionFilter class:
@WebFilter(filterName = "DatastoreSessionFilter",
urlPatterns = { "",
"/LoginEmail",
"/Logout",
"/ResetPassword",
"/SignupEmail",
"/VerifyEmail",
"/About",
"/BulkUpload",
"/DeleteAlbum",
"/DeletePost",
"/EditAlbum",
"/EditPost",
"/Events",
"/EventsScroll",
"/Home",
"/HomeScroll",
"/Info",
"/ListAlbums",
"/ListAlbumsScroll",
"/NewAlbum",
"/NewPost",
"/Support",
"/Videos",
"/VideosScroll",
"/ViewAlbum",
"/ViewAlbumScroll",
"/ViewPost",
"/ViewProfile",
})
public class DatastoreSessionFilter implements Filter {
private static Datastore datastore;
private static KeyFactory keyFactory;
private static final DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyyMMddHHmmssSSS");
@Override
public void init(FilterConfig config) throws ServletException {
// initialize local copy of datastore session variables
datastore = DatastoreOptions.getDefaultInstance().getService();
keyFactory = datastore.newKeyFactory().setKind("SessionVariable");
// Delete all sessions unmodified for over two days
DateTime dt = DateTime.now(DateTimeZone.UTC);
Query<Entity> query = Query.newEntityQueryBuilder().setKind("SessionVariable")
.setFilter(PropertyFilter.le("lastModified", dt.minusDays(2).toString(dtf))).build();
QueryResults<Entity> resultList = datastore.run(query);
while (resultList.hasNext()) {
Entity stateEntity = resultList.next();
datastore.delete(stateEntity.getKey());
}
}
// [END init]
@Override
public void doFilter(ServletRequest servletReq, ServletResponse servletResp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletReq;
HttpServletResponse resp = (HttpServletResponse) servletResp;
// Check if the session cookie is there, if not there, make a session cookie using a unique
// identifier.
String sessionId = getCookieValue(req, "bookshelfSessionId");
if (sessionId.equals("")) {
String sessionNum = new BigInteger(130, new SecureRandom()).toString(32);
Cookie session = new Cookie("bookshelfSessionId", sessionNum);
session.setPath("/");
resp.addCookie(session);
}
Map<String, String> datastoreMap = loadSessionVariables(req); // session variables for request
chain.doFilter(servletReq, servletResp); // Allow the servlet to process request and response
HttpSession session = req.getSession(); // Create session map
Map<String, String> sessionMap = new HashMap<>();
Enumeration<String> attrNames = session.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = attrNames.nextElement();
String sessName = session.getAttribute(attrName).toString();
sessionMap.put(attrName, sessName);
// DEFAULT: sessionMap.put(attrName, (String) session.getAttribute(attrName));
}
// Create a diff between the new session variables and the existing session variables
// to minimize datastore access
MapDifference<String, String> diff = Maps.difference(sessionMap, datastoreMap);
Map<String, String> setMap = diff.entriesOnlyOnLeft();
Map<String, String> deleteMap = diff.entriesOnlyOnRight();
// Apply the diff
setSessionVariables(sessionId, setMap);
deleteSessionVariables(sessionId, FluentIterable.from(deleteMap.keySet()).toArray(String.class));
}
@SuppressWarnings("unused")
private String mapToString(Map<String, String> map) {
StringBuffer names = new StringBuffer();
for (String name : map.keySet()) {
names.append(name + " ");
}
return names.toString();
}
@Override
public void destroy() {
}
protected String getCookieValue(HttpServletRequest req, String cookieName) {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
}
return "";
}
// [START deleteSessionVariables]
/**
* Delete a value stored in the project's datastore.
* @param sessionId Request from which the session is extracted.
*/
protected void deleteSessionVariables(String sessionId, String... varNames) {
if (sessionId.equals("")) {
return;
}
Key key = keyFactory.newKey(sessionId);
Transaction transaction = datastore.newTransaction();
try {
Entity stateEntity = transaction.get(key);
if (stateEntity != null) {
Entity.Builder builder = Entity.newBuilder(stateEntity);
StringBuilder delNames = new StringBuilder();
for (String varName : varNames) {
delNames.append(varName + " ");
builder = builder.remove(varName);
}
datastore.update(builder.build());
}
} finally {
if (transaction.isActive()) {
transaction.rollback();
}
}
}
// [END deleteSessionVariables]
protected void deleteSessionWithValue(String varName, String varValue) {
Transaction transaction = datastore.newTransaction();
try {
Query<Entity> query = Query.newEntityQueryBuilder().setKind("SessionVariable")
.setFilter(PropertyFilter.eq(varName, varValue)).build();
QueryResults<Entity> resultList = transaction.run(query);
while (resultList.hasNext()) {
Entity stateEntity = resultList.next();
transaction.delete(stateEntity.getKey());
}
transaction.commit();
} finally {
if (transaction.isActive()) {
transaction.rollback();
}
}
}
// [START setSessionVariables]
/**
* Stores the state value in each key-value pair in the project's datastore.
* @param sessionId Request from which to extract session.
* @param varName the name of the desired session variable
* @param varValue the value of the desired session variable
*/
protected void setSessionVariables(String sessionId, Map<String, String> setMap) {
if (sessionId.equals("")) {
return;
}
Key key = keyFactory.newKey(sessionId);
Transaction transaction = datastore.newTransaction();
DateTime dt = DateTime.now(DateTimeZone.UTC);
dt.toString(dtf);
try {
Entity stateEntity = transaction.get(key);
Entity.Builder seBuilder;
if (stateEntity == null) {
seBuilder = Entity.newBuilder(key);
} else {
seBuilder = Entity.newBuilder(stateEntity);
}
for (String varName : setMap.keySet()) {
seBuilder.set(varName, setMap.get(varName));
}
transaction.put(seBuilder.set("lastModified", dt.toString(dtf)).build());
transaction.commit();
} finally {
if (transaction.isActive()) {
transaction.rollback();
}
}
}
// [END setSessionVariables]
// [START loadSessionVariables]
/**
* Take an HttpServletRequest, and copy all of the current session variables over to it
* @param req Request from which to extract session.
* @return a map of strings containing all the session variables loaded or an empty map.
*/
protected Map<String, String> loadSessionVariables(HttpServletRequest req) throws ServletException {
Map<String, String> datastoreMap = new HashMap<>();
String sessionId = getCookieValue(req, "bookshelfSessionId");
if (sessionId.equals("")) {
return datastoreMap;
}
Key key = keyFactory.newKey(sessionId);
Transaction transaction = datastore.newTransaction();
try {
Entity stateEntity = transaction.get(key);
StringBuilder logNames = new StringBuilder();
if (stateEntity != null) {
for (String varName : stateEntity.getNames()) {
req.getSession().setAttribute(varName, stateEntity.getString(varName));
datastoreMap.put(varName, stateEntity.getString(varName));
logNames.append(varName + " ");
}
}
} finally {
if (transaction.isActive()) {
transaction.rollback();
}
}
return datastoreMap;
}
// [END loadSessionVariables]
}