From 5651e6620db5d7051b51d12bb06c05face1fc25c Mon Sep 17 00:00:00 2001 From: saiflayouni Date: Fri, 30 Oct 2026 08:48:14 +0100 Subject: [PATCH] fix(bookshelf-standard): don't crash startup on index lag DatastoreSessionFilter.init() queries for sessions stale by more than two days and deletes them, as a best-effort cleanup task. On a fresh deployment, Datastore has no index yet for a property ("lastModified") it has never seen written, so this query throws DatastoreNeedIndexException until the first session is actually written - which took down the whole filter, and therefore the whole app, at startup (#448). Catch DatastoreNeedIndexException and log a warning instead: there's nothing to clean up on a Datastore that has never written a session anyway, so skipping the cleanup in that case is harmless, and letting an optional maintenance task fail shouldn't prevent the app from starting. Same fix applied identically across all four bookshelf-standard steps (2-structured-data, 3-binary-data, 4-auth, 5-logging), which share this file verbatim. Fixes #448 --- .../util/DatastoreSessionFilter.java | 28 +++++++++++++------ .../util/DatastoreSessionFilter.java | 28 +++++++++++++------ .../util/DatastoreSessionFilter.java | 28 +++++++++++++------ .../util/DatastoreSessionFilter.java | 28 +++++++++++++------ 4 files changed, 80 insertions(+), 32 deletions(-) diff --git a/bookshelf-standard/2-structured-data/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java b/bookshelf-standard/2-structured-data/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java index 28bfb19d9..3c7653d7c 100644 --- a/bookshelf-standard/2-structured-data/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java +++ b/bookshelf-standard/2-structured-data/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java @@ -15,6 +15,7 @@ package com.example.getstarted.util; +import com.google.appengine.api.datastore.DatastoreNeedIndexException; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; @@ -35,6 +36,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -53,6 +56,8 @@ // [START init] public class DatastoreSessionFilter implements Filter { + private static final Logger logger = Logger.getLogger(DatastoreSessionFilter.class.getName()); + private static DatastoreService datastore; private static final DateTimeFormatter DTF = DateTimeFormat.forPattern("yyyyMMddHHmmssSSS"); private static final String SESSION_KIND = "SessionVariable"; @@ -62,14 +67,21 @@ public void init(FilterConfig config) throws ServletException { // initialize local copy of datastore session variables datastore = DatastoreServiceFactory.getDatastoreService(); - // Delete all sessions unmodified for over two days - DateTime dt = DateTime.now(DateTimeZone.UTC); - Query query = new Query(SESSION_KIND).setFilter(new FilterPredicate( - "lastModified", FilterOperator.LESS_THAN_OR_EQUAL, dt.minusDays(2).toString(DTF))); - Iterator results = datastore.prepare(query).asIterator(); - while (results.hasNext()) { - Entity stateEntity = results.next(); - datastore.delete(stateEntity.getKey()); + // Delete all sessions unmodified for over two days. Best-effort: on a fresh + // deployment Datastore has no index yet for a property it has never seen + // written, so this throws DatastoreNeedIndexException until the first session + // is written. Don't let that take down the whole filter (and app) at startup. + try { + DateTime dt = DateTime.now(DateTimeZone.UTC); + Query query = new Query(SESSION_KIND).setFilter(new FilterPredicate( + "lastModified", FilterOperator.LESS_THAN_OR_EQUAL, dt.minusDays(2).toString(DTF))); + Iterator results = datastore.prepare(query).asIterator(); + while (results.hasNext()) { + Entity stateEntity = results.next(); + datastore.delete(stateEntity.getKey()); + } + } catch (DatastoreNeedIndexException e) { + logger.log(Level.WARNING, "Skipping stale-session cleanup; index not ready yet", e); } } // [END init] diff --git a/bookshelf-standard/3-binary-data/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java b/bookshelf-standard/3-binary-data/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java index 28bfb19d9..3c7653d7c 100644 --- a/bookshelf-standard/3-binary-data/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java +++ b/bookshelf-standard/3-binary-data/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java @@ -15,6 +15,7 @@ package com.example.getstarted.util; +import com.google.appengine.api.datastore.DatastoreNeedIndexException; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; @@ -35,6 +36,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -53,6 +56,8 @@ // [START init] public class DatastoreSessionFilter implements Filter { + private static final Logger logger = Logger.getLogger(DatastoreSessionFilter.class.getName()); + private static DatastoreService datastore; private static final DateTimeFormatter DTF = DateTimeFormat.forPattern("yyyyMMddHHmmssSSS"); private static final String SESSION_KIND = "SessionVariable"; @@ -62,14 +67,21 @@ public void init(FilterConfig config) throws ServletException { // initialize local copy of datastore session variables datastore = DatastoreServiceFactory.getDatastoreService(); - // Delete all sessions unmodified for over two days - DateTime dt = DateTime.now(DateTimeZone.UTC); - Query query = new Query(SESSION_KIND).setFilter(new FilterPredicate( - "lastModified", FilterOperator.LESS_THAN_OR_EQUAL, dt.minusDays(2).toString(DTF))); - Iterator results = datastore.prepare(query).asIterator(); - while (results.hasNext()) { - Entity stateEntity = results.next(); - datastore.delete(stateEntity.getKey()); + // Delete all sessions unmodified for over two days. Best-effort: on a fresh + // deployment Datastore has no index yet for a property it has never seen + // written, so this throws DatastoreNeedIndexException until the first session + // is written. Don't let that take down the whole filter (and app) at startup. + try { + DateTime dt = DateTime.now(DateTimeZone.UTC); + Query query = new Query(SESSION_KIND).setFilter(new FilterPredicate( + "lastModified", FilterOperator.LESS_THAN_OR_EQUAL, dt.minusDays(2).toString(DTF))); + Iterator results = datastore.prepare(query).asIterator(); + while (results.hasNext()) { + Entity stateEntity = results.next(); + datastore.delete(stateEntity.getKey()); + } + } catch (DatastoreNeedIndexException e) { + logger.log(Level.WARNING, "Skipping stale-session cleanup; index not ready yet", e); } } // [END init] diff --git a/bookshelf-standard/4-auth/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java b/bookshelf-standard/4-auth/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java index 28bfb19d9..3c7653d7c 100644 --- a/bookshelf-standard/4-auth/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java +++ b/bookshelf-standard/4-auth/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java @@ -15,6 +15,7 @@ package com.example.getstarted.util; +import com.google.appengine.api.datastore.DatastoreNeedIndexException; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; @@ -35,6 +36,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -53,6 +56,8 @@ // [START init] public class DatastoreSessionFilter implements Filter { + private static final Logger logger = Logger.getLogger(DatastoreSessionFilter.class.getName()); + private static DatastoreService datastore; private static final DateTimeFormatter DTF = DateTimeFormat.forPattern("yyyyMMddHHmmssSSS"); private static final String SESSION_KIND = "SessionVariable"; @@ -62,14 +67,21 @@ public void init(FilterConfig config) throws ServletException { // initialize local copy of datastore session variables datastore = DatastoreServiceFactory.getDatastoreService(); - // Delete all sessions unmodified for over two days - DateTime dt = DateTime.now(DateTimeZone.UTC); - Query query = new Query(SESSION_KIND).setFilter(new FilterPredicate( - "lastModified", FilterOperator.LESS_THAN_OR_EQUAL, dt.minusDays(2).toString(DTF))); - Iterator results = datastore.prepare(query).asIterator(); - while (results.hasNext()) { - Entity stateEntity = results.next(); - datastore.delete(stateEntity.getKey()); + // Delete all sessions unmodified for over two days. Best-effort: on a fresh + // deployment Datastore has no index yet for a property it has never seen + // written, so this throws DatastoreNeedIndexException until the first session + // is written. Don't let that take down the whole filter (and app) at startup. + try { + DateTime dt = DateTime.now(DateTimeZone.UTC); + Query query = new Query(SESSION_KIND).setFilter(new FilterPredicate( + "lastModified", FilterOperator.LESS_THAN_OR_EQUAL, dt.minusDays(2).toString(DTF))); + Iterator results = datastore.prepare(query).asIterator(); + while (results.hasNext()) { + Entity stateEntity = results.next(); + datastore.delete(stateEntity.getKey()); + } + } catch (DatastoreNeedIndexException e) { + logger.log(Level.WARNING, "Skipping stale-session cleanup; index not ready yet", e); } } // [END init] diff --git a/bookshelf-standard/5-logging/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java b/bookshelf-standard/5-logging/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java index 28bfb19d9..3c7653d7c 100644 --- a/bookshelf-standard/5-logging/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java +++ b/bookshelf-standard/5-logging/src/main/java/com/example/getstarted/util/DatastoreSessionFilter.java @@ -15,6 +15,7 @@ package com.example.getstarted.util; +import com.google.appengine.api.datastore.DatastoreNeedIndexException; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; @@ -35,6 +36,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -53,6 +56,8 @@ // [START init] public class DatastoreSessionFilter implements Filter { + private static final Logger logger = Logger.getLogger(DatastoreSessionFilter.class.getName()); + private static DatastoreService datastore; private static final DateTimeFormatter DTF = DateTimeFormat.forPattern("yyyyMMddHHmmssSSS"); private static final String SESSION_KIND = "SessionVariable"; @@ -62,14 +67,21 @@ public void init(FilterConfig config) throws ServletException { // initialize local copy of datastore session variables datastore = DatastoreServiceFactory.getDatastoreService(); - // Delete all sessions unmodified for over two days - DateTime dt = DateTime.now(DateTimeZone.UTC); - Query query = new Query(SESSION_KIND).setFilter(new FilterPredicate( - "lastModified", FilterOperator.LESS_THAN_OR_EQUAL, dt.minusDays(2).toString(DTF))); - Iterator results = datastore.prepare(query).asIterator(); - while (results.hasNext()) { - Entity stateEntity = results.next(); - datastore.delete(stateEntity.getKey()); + // Delete all sessions unmodified for over two days. Best-effort: on a fresh + // deployment Datastore has no index yet for a property it has never seen + // written, so this throws DatastoreNeedIndexException until the first session + // is written. Don't let that take down the whole filter (and app) at startup. + try { + DateTime dt = DateTime.now(DateTimeZone.UTC); + Query query = new Query(SESSION_KIND).setFilter(new FilterPredicate( + "lastModified", FilterOperator.LESS_THAN_OR_EQUAL, dt.minusDays(2).toString(DTF))); + Iterator results = datastore.prepare(query).asIterator(); + while (results.hasNext()) { + Entity stateEntity = results.next(); + datastore.delete(stateEntity.getKey()); + } + } catch (DatastoreNeedIndexException e) { + logger.log(Level.WARNING, "Skipping stale-session cleanup; index not ready yet", e); } } // [END init]