Skip to content

fix(bookshelf-standard): don't crash startup on index lag - #1266

Open
saiflayouni wants to merge 1 commit into
GoogleCloudPlatform:mainfrom
saiflayouni:fix/448-datastore-session-filter-index
Open

saiflayouni wants to merge 1 commit into
GoogleCloudPlatform:mainfrom
saiflayouni:fix/448-datastore-session-filter-index

Conversation

@saiflayouni

Copy link
Copy Markdown

Summary

DatastoreSessionFilter.init() (in every bookshelf-standard step) queries for sessions stale by more than two days and deletes them, as a best-effort cleanup task run once when the filter is initialized.

On a fresh deployment, Datastore has no index yet for a property it has never seen written — here, lastModified on a SessionVariable entity. Until the very first session is actually written, that query throws DatastoreNeedIndexException, which propagated straight out of init() and took down the whole filter (and therefore the whole app) at startup. This matches #448's report and stack trace exactly (DatastoreSessionFilter.init failing with "no matching index found" on deploy/local run).

Fix

Catch DatastoreNeedIndexException around the cleanup query and log a warning instead of propagating. There's nothing to clean up on a Datastore that has never written a SessionVariable anyway, so skipping the cleanup in that case is harmless — and an optional maintenance task failing should never prevent the whole app from starting.

Applied identically to all four steps that share this file verbatim: 2-structured-data, 3-binary-data, 4-auth, 5-logging.

Test plan

  • javac against each module's full Maven classpath (appengine-api-1.0-sdk, Guava, servlet API, joda-time) — all 4 compile clean.
  • Verified DatastoreNeedIndexException (the exact type the legacy com.google.appengine.api.datastore SDK throws for "no matching index found") is a RuntimeException, consistent with it propagating unchecked out of Filter.init() before this fix.

Note on the other open issues referenced in this pass

While investigating, I also looked at #393, #419, #1236, and #775 (all currently open). None of them are fixable with a code change in this repository:

Only #448 referenced real, currently-existing, reproducible code, so this PR addresses that one.

Fixes #448

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 (GoogleCloudPlatform#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 GoogleCloudPlatform#448
@saiflayouni
saiflayouni requested a review from a team as a code owner October 30, 2026 07:49

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the DatastoreSessionFilter across multiple modules to catch DatastoreNeedIndexException during startup stale-session cleanup, preventing application startup failures on fresh deployments where the Datastore index is not yet ready. The review feedback correctly points out that deleting entities one-by-one in a loop is highly inefficient and can cause startup delays; it recommends batching the delete operations into a single call instead.

Comment on lines +78 to +82
Iterator<Entity> results = datastore.prepare(query).asIterator();
while (results.hasNext()) {
Entity stateEntity = results.next();
datastore.delete(stateEntity.getKey());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performing Datastore delete operations one-by-one in a loop is highly inefficient as it triggers a separate RPC for each entity. Since this cleanup runs synchronously during filter initialization (init()), a large number of stale sessions could cause significant startup delays or timeouts. Batching the deletes into a single datastore.delete(keysToDelete) call is much more efficient and safer.

      Iterable<Entity> results = datastore.prepare(query).asIterable();
      java.util.List<com.google.appengine.api.datastore.Key> keysToDelete = new java.util.ArrayList<>();
      for (Entity stateEntity : results) {
        keysToDelete.add(stateEntity.getKey());
      }
      if (!keysToDelete.isEmpty()) {
        datastore.delete(keysToDelete);
      }

Comment on lines +78 to +82
Iterator<Entity> results = datastore.prepare(query).asIterator();
while (results.hasNext()) {
Entity stateEntity = results.next();
datastore.delete(stateEntity.getKey());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performing Datastore delete operations one-by-one in a loop is highly inefficient as it triggers a separate RPC for each entity. Since this cleanup runs synchronously during filter initialization (init()), a large number of stale sessions could cause significant startup delays or timeouts. Batching the deletes into a single datastore.delete(keysToDelete) call is much more efficient and safer.

      Iterable<Entity> results = datastore.prepare(query).asIterable();
      java.util.List<com.google.appengine.api.datastore.Key> keysToDelete = new java.util.ArrayList<>();
      for (Entity stateEntity : results) {
        keysToDelete.add(stateEntity.getKey());
      }
      if (!keysToDelete.isEmpty()) {
        datastore.delete(keysToDelete);
      }

Comment on lines +78 to +82
Iterator<Entity> results = datastore.prepare(query).asIterator();
while (results.hasNext()) {
Entity stateEntity = results.next();
datastore.delete(stateEntity.getKey());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performing Datastore delete operations one-by-one in a loop is highly inefficient as it triggers a separate RPC for each entity. Since this cleanup runs synchronously during filter initialization (init()), a large number of stale sessions could cause significant startup delays or timeouts. Batching the deletes into a single datastore.delete(keysToDelete) call is much more efficient and safer.

      Iterable<Entity> results = datastore.prepare(query).asIterable();
      java.util.List<com.google.appengine.api.datastore.Key> keysToDelete = new java.util.ArrayList<>();
      for (Entity stateEntity : results) {
        keysToDelete.add(stateEntity.getKey());
      }
      if (!keysToDelete.isEmpty()) {
        datastore.delete(keysToDelete);
      }

Comment on lines +78 to +82
Iterator<Entity> results = datastore.prepare(query).asIterator();
while (results.hasNext()) {
Entity stateEntity = results.next();
datastore.delete(stateEntity.getKey());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performing Datastore delete operations one-by-one in a loop is highly inefficient as it triggers a separate RPC for each entity. Since this cleanup runs synchronously during filter initialization (init()), a large number of stale sessions could cause significant startup delays or timeouts. Batching the deletes into a single datastore.delete(keysToDelete) call is much more efficient and safer.

      Iterable<Entity> results = datastore.prepare(query).asIterable();
      java.util.List<com.google.appengine.api.datastore.Key> keysToDelete = new java.util.ArrayList<>();
      for (Entity stateEntity : results) {
        keysToDelete.add(stateEntity.getKey());
      }
      if (!keysToDelete.isEmpty()) {
        datastore.delete(keysToDelete);
      }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

com.google.cloud.datastore.DatastoreException: no matching index found

1 participant