Switch to ephemeral storage when FeedStore reports success but is missing a
number of items that exceeds the configurable threshold. Also report errors
when editing content in FeedSessionManagerImpl.

PiperOrigin-RevId: 258432483
Change-Id: Ied6152ef1ea8d79f175486362fc0e161bc4469d1
diff --git a/src/main/java/com/google/android/libraries/feed/api/client/scope/ProcessScopeBuilder.java b/src/main/java/com/google/android/libraries/feed/api/client/scope/ProcessScopeBuilder.java
index 0a5910a..fbde1df 100644
--- a/src/main/java/com/google/android/libraries/feed/api/client/scope/ProcessScopeBuilder.java
+++ b/src/main/java/com/google/android/libraries/feed/api/client/scope/ProcessScopeBuilder.java
@@ -268,7 +268,9 @@
             schedulerApi,
             configuration,
             clock,
-            lifecycleListener);
+            lifecycleListener,
+            mainThreadRunner,
+            basicLoggingApi);
     FeedSessionManager feedSessionManager = fsmFactory.create();
     RequestManagerImpl clientRequestManager =
         new RequestManagerImpl(feedRequestManager, feedSessionManager);
diff --git a/src/main/java/com/google/android/libraries/feed/api/host/config/Configuration.java b/src/main/java/com/google/android/libraries/feed/api/host/config/Configuration.java
index ff510c7..5aefa7e 100644
--- a/src/main/java/com/google/android/libraries/feed/api/host/config/Configuration.java
+++ b/src/main/java/com/google/android/libraries/feed/api/host/config/Configuration.java
@@ -83,6 +83,8 @@
     // Minimum time before a spinner should show before disappearing. Only used for feed wide
     // spinners, not for more button spinners.
     ConfigKey.SPINNER_MINIMUM_SHOW_TIME_MS,
+    // The number of items that can be missing from a call to FeedStore before failing.
+    ConfigKey.STORAGE_MISS_THRESHOLD,
     // Time in ms for the length of the timeout
     ConfigKey.TIMEOUT_TIMEOUT_MS,
     ConfigKey.TRIGGER_IMMEDIATE_PAGINATION,
@@ -135,6 +137,7 @@
     String SNIPPETS_ENABLED = "snippets_enabled";
     String SPINNER_DELAY_MS = "spinner_delay";
     String SPINNER_MINIMUM_SHOW_TIME_MS = "spinner_minimum_show_time";
+    String STORAGE_MISS_THRESHOLD = "storage_miss_threshold";
     String TIMEOUT_TIMEOUT_MS = "timeout_timeout_ms";
     String TRIGGER_IMMEDIATE_PAGINATION = "trigger_immediate_pagination_bool";
     String UNDOABLE_ACTIONS_ENABLED = "undoable_actions_enabled";
diff --git a/src/main/java/com/google/android/libraries/feed/api/host/logging/InternalFeedError.java b/src/main/java/com/google/android/libraries/feed/api/host/logging/InternalFeedError.java
index 22888ab..e566d22 100644
--- a/src/main/java/com/google/android/libraries/feed/api/host/logging/InternalFeedError.java
+++ b/src/main/java/com/google/android/libraries/feed/api/host/logging/InternalFeedError.java
@@ -37,6 +37,10 @@
   InternalFeedError.FAILED_TO_CREATE_LEAF,
   InternalFeedError.UNHANDLED_TOKEN,
   InternalFeedError.TASK_QUEUE_STARVATION,
+  InternalFeedError.CONTENT_STORAGE_MISSING_ITEM,
+  InternalFeedError.ITEM_NOT_PARSED,
+  InternalFeedError.STORAGE_MISS_BEYOND_THRESHOLD,
+  InternalFeedError.CONTENT_MUTATION_FAILED,
   InternalFeedError.NEXT_VALUE
 })
 public @interface InternalFeedError {
@@ -89,6 +93,15 @@
   /** Represents bytes from ContentStorage that cannot be parsed into the proto. */
   int ITEM_NOT_PARSED = 14;
 
+  /**
+   * Represents a storage miss where FeedStore reports success but is missing a number of items that
+   * exceeds the configurable threshold.
+   */
+  int STORAGE_MISS_BEYOND_THRESHOLD = 15;
+
+  /** Represents a failed content mutation in FeedSessionManager. */
+  int CONTENT_MUTATION_FAILED = 16;
+
   /** The next value that should be used when adding additional values to the IntDef. */
-  int NEXT_VALUE = 15;
+  int NEXT_VALUE = 17;
 }
diff --git a/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java b/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java
index a2057a7..0697bbb 100644
--- a/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java
+++ b/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java
@@ -138,7 +138,9 @@
                 schedulerApi,
                 configuration,
                 fakeClock,
-                appLifecycleListener)
+                appLifecycleListener,
+                fakeMainThreadRunner,
+                fakeBasicLoggingApi)
             .create();
     new ClearAllListener(
         taskQueue, feedSessionManager, store, fakeThreadUtils, appLifecycleListener);
diff --git a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerFactory.java b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerFactory.java
index 22bcf53..505bde8 100644
--- a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerFactory.java
+++ b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerFactory.java
@@ -16,6 +16,7 @@
 
 import com.google.android.libraries.feed.api.host.config.Configuration;
 import com.google.android.libraries.feed.api.host.config.Configuration.ConfigKey;
+import com.google.android.libraries.feed.api.host.logging.BasicLoggingApi;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.api.internal.common.ThreadUtils;
 import com.google.android.libraries.feed.api.internal.protocoladapter.ProtocolAdapter;
@@ -23,6 +24,7 @@
 import com.google.android.libraries.feed.api.internal.requestmanager.FeedRequestManager;
 import com.google.android.libraries.feed.api.internal.sessionmanager.FeedSessionManager;
 import com.google.android.libraries.feed.api.internal.store.Store;
+import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
 import com.google.android.libraries.feed.common.concurrent.TaskQueue;
 import com.google.android.libraries.feed.common.feedobservable.FeedObservable;
 import com.google.android.libraries.feed.common.time.Clock;
@@ -53,6 +55,8 @@
   private final Configuration configuration;
   private final Clock clock;
   private final FeedObservable<FeedLifecycleListener> lifecycleListenerObservable;
+  private final MainThreadRunner mainThreadRunner;
+  private final BasicLoggingApi basicLoggingApi;
 
   public FeedSessionManagerFactory(
       TaskQueue taskQueue,
@@ -65,7 +69,9 @@
       SchedulerApi schedulerApi,
       Configuration configuration,
       Clock clock,
-      FeedObservable<FeedLifecycleListener> lifecycleListenerObservable) {
+      FeedObservable<FeedLifecycleListener> lifecycleListenerObservable,
+      MainThreadRunner mainThreadRunner,
+      BasicLoggingApi basicLoggingApi) {
     this.taskQueue = taskQueue;
     this.store = store;
     this.timingUtils = timingUtils;
@@ -77,6 +83,8 @@
     this.configuration = configuration;
     this.clock = clock;
     this.lifecycleListenerObservable = lifecycleListenerObservable;
+    this.mainThreadRunner = mainThreadRunner;
+    this.basicLoggingApi = basicLoggingApi;
   }
 
   /** Creates a new FeedSessionManager and initializes it */
@@ -98,7 +106,9 @@
             schedulerApi,
             threadUtils,
             timingUtils,
-            clock);
+            clock,
+            mainThreadRunner,
+            basicLoggingApi);
 
     return new FeedSessionManagerImpl(
         taskQueue,
@@ -115,6 +125,8 @@
         schedulerApi,
         configuration,
         clock,
-        lifecycleListenerObservable);
+        lifecycleListenerObservable,
+        mainThreadRunner,
+        basicLoggingApi);
   }
 }
diff --git a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImpl.java b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImpl.java
index 6e9cf4c..4d83dd0 100644
--- a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImpl.java
+++ b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImpl.java
@@ -20,6 +20,8 @@
 import com.google.android.libraries.feed.api.common.MutationContext;
 import com.google.android.libraries.feed.api.host.config.Configuration;
 import com.google.android.libraries.feed.api.host.config.Configuration.ConfigKey;
+import com.google.android.libraries.feed.api.host.logging.BasicLoggingApi;
+import com.google.android.libraries.feed.api.host.logging.InternalFeedError;
 import com.google.android.libraries.feed.api.host.logging.RequestReason;
 import com.google.android.libraries.feed.api.host.logging.Task;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
@@ -40,6 +42,7 @@
 import com.google.android.libraries.feed.api.internal.store.StoreListener;
 import com.google.android.libraries.feed.common.Result;
 import com.google.android.libraries.feed.common.Validators;
+import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
 import com.google.android.libraries.feed.common.concurrent.TaskQueue;
 import com.google.android.libraries.feed.common.concurrent.TaskQueue.TaskType;
 import com.google.android.libraries.feed.common.feedobservable.FeedObservable;
@@ -135,6 +138,9 @@
   private final SchedulerApi schedulerApi;
   private final TaskQueue taskQueue;
   private final Clock clock;
+  private final Configuration configuration;
+  private final MainThreadRunner mainThreadRunner;
+  private final BasicLoggingApi basicLoggingApi;
   private final long sessionPopulationTimeoutMs;
   private final boolean uploadingActionsEnabled;
 
@@ -162,7 +168,9 @@
       SchedulerApi schedulerApi,
       Configuration configuration,
       Clock clock,
-      FeedObservable<FeedLifecycleListener> lifecycleListenerObservable) {
+      FeedObservable<FeedLifecycleListener> lifecycleListenerObservable,
+      MainThreadRunner mainThreadRunner,
+      BasicLoggingApi basicLoggingApi) {
     this.taskQueue = taskQueue;
     this.sessionFactory = sessionFactory;
     this.sessionCache = sessionCache;
@@ -177,6 +185,9 @@
     this.actionUploadRequestManager = actionUploadRequestManager;
     this.schedulerApi = schedulerApi;
     this.clock = clock;
+    this.configuration = configuration;
+    this.mainThreadRunner = mainThreadRunner;
+    this.basicLoggingApi = basicLoggingApi;
     uploadingActionsEnabled =
         configuration.getValueOrDefault(ConfigKey.UNDOABLE_ACTIONS_ENABLED, false);
     sessionPopulationTimeoutMs =
@@ -635,9 +646,32 @@
 
     if (!cacheMisses.isEmpty()) {
       Result<List<PayloadWithId>> contentResult = store.getPayloads(cacheMisses);
-      if (contentResult.isSuccessful()) {
+      boolean successfulRead =
+          contentResult.isSuccessful()
+              && (contentResult.getValue().size()
+                      + configuration.getValueOrDefault(ConfigKey.STORAGE_MISS_THRESHOLD, 4L)
+                  >= cacheMisses.size());
+      if (successfulRead) {
+        Logger.i(
+            TAG,
+            "getStreamFeatures; requestedItems(%d), result(%d)",
+            cacheMisses.size(),
+            contentResult.getValue().size());
         results.addAll(contentResult.getValue());
       } else {
+        if (contentResult.isSuccessful()) {
+          Logger.e(
+              TAG,
+              "Storage miss beyond threshold; requestedItems(%d), returned(%d)",
+              cacheMisses.size(),
+              contentResult.getValue().size());
+          mainThreadRunner.execute(
+              "STORAGE_MISS_BEYOND_THRESHOLD",
+              () -> {
+                basicLoggingApi.onInternalError(InternalFeedError.STORAGE_MISS_BEYOND_THRESHOLD);
+              });
+        }
+
         // since we couldn't populate the content, switch to ephemeral mode
         switchToEphemeralMode("Unable to get the payloads in getStreamFeatures");
         return Result.failure();
diff --git a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutation.java b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutation.java
index d6777be..02bccb0 100644
--- a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutation.java
+++ b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutation.java
@@ -18,6 +18,8 @@
 import android.text.TextUtils;
 import com.google.android.libraries.feed.api.client.knowncontent.KnownContent;
 import com.google.android.libraries.feed.api.common.MutationContext;
+import com.google.android.libraries.feed.api.host.logging.BasicLoggingApi;
+import com.google.android.libraries.feed.api.host.logging.InternalFeedError;
 import com.google.android.libraries.feed.api.host.logging.Task;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.api.host.storage.CommitResult;
@@ -31,6 +33,7 @@
 import com.google.android.libraries.feed.api.internal.store.SemanticPropertiesMutation;
 import com.google.android.libraries.feed.api.internal.store.Store;
 import com.google.android.libraries.feed.common.Result;
+import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
 import com.google.android.libraries.feed.common.concurrent.TaskQueue;
 import com.google.android.libraries.feed.common.concurrent.TaskQueue.TaskType;
 import com.google.android.libraries.feed.common.functional.Consumer;
@@ -68,6 +71,8 @@
   private final ThreadUtils threadUtils;
   private final TimingUtils timingUtils;
   private final Clock clock;
+  private final MainThreadRunner mainThreadRunner;
+  private final BasicLoggingApi basicLoggingApi;
 
   // operation counts for the dumper
   private int createCount = 0;
@@ -89,7 +94,9 @@
       SchedulerApi schedulerApi,
       ThreadUtils threadUtils,
       TimingUtils timingUtils,
-      Clock clock) {
+      Clock clock,
+      MainThreadRunner mainThreadRunner,
+      BasicLoggingApi basicLoggingApi) {
     this.store = store;
     this.sessionCache = sessionCache;
     this.contentCache = contentCache;
@@ -98,6 +105,8 @@
     this.threadUtils = threadUtils;
     this.timingUtils = timingUtils;
     this.clock = clock;
+    this.mainThreadRunner = mainThreadRunner;
+    this.basicLoggingApi = basicLoggingApi;
   }
 
   /**
@@ -110,7 +119,13 @@
       ModelErrorObserver modelErrorObserver,
       KnownContent./*@Nullable*/ Listener knownContentListener) {
     createCount++;
-    return new MutationCommitter(task, mutationContext, modelErrorObserver, knownContentListener);
+    return new MutationCommitter(
+        task,
+        mutationContext,
+        modelErrorObserver,
+        knownContentListener,
+        mainThreadRunner,
+        basicLoggingApi);
   }
 
   public void resetHead() {
@@ -217,6 +232,8 @@
     private final KnownContent./*@Nullable*/ Listener knownContentListener;
 
     private final List<StreamStructure> streamStructures = new ArrayList<>();
+    private final MainThreadRunner mainThreadRunner;
+    private final BasicLoggingApi basicLoggingApi;
 
     @VisibleForTesting boolean clearedHead = false;
     private Model model;
@@ -225,11 +242,15 @@
         String task,
         MutationContext mutationContext,
         ModelErrorObserver modelErrorObserver,
-        KnownContent./*@Nullable*/ Listener knownContentListener) {
+        KnownContent./*@Nullable*/ Listener knownContentListener,
+        MainThreadRunner mainThreadRunner,
+        BasicLoggingApi basicLoggingApi) {
       this.task = task;
       this.mutationContext = mutationContext;
       this.modelErrorObserver = modelErrorObserver;
       this.knownContentListener = knownContentListener;
+      this.mainThreadRunner = mainThreadRunner;
+      this.basicLoggingApi = basicLoggingApi;
     }
 
     @Override
@@ -348,6 +369,11 @@
             if (contentMutation.commit().getResult() == CommitResult.Result.FAILURE) {
               contentCommitErrorCount++;
               Logger.e(TAG, "contentMutation failed");
+              mainThreadRunner.execute(
+                  "CONTENT_MUTATION_FAILED",
+                  () -> {
+                    basicLoggingApi.onInternalError(InternalFeedError.CONTENT_MUTATION_FAILED);
+                  });
             }
             if (semanticPropertiesMutation.commit().getResult() == CommitResult.Result.FAILURE) {
               semanticPropertiesCommitErrorCount++;
diff --git a/src/main/java/com/google/android/libraries/feed/feedstore/FeedStore.java b/src/main/java/com/google/android/libraries/feed/feedstore/FeedStore.java
index 9cec44f..30fe916 100644
--- a/src/main/java/com/google/android/libraries/feed/feedstore/FeedStore.java
+++ b/src/main/java/com/google/android/libraries/feed/feedstore/FeedStore.java
@@ -87,6 +87,9 @@
 
   private boolean isEphemeralMode = false;
 
+  protected final ContentStorageDirect contentStorage;
+  protected final JournalStorageDirect journalStorage;
+
   public FeedStore(
       TimingUtils timingUtils,
       FeedExtensionRegistry extensionRegistry,
@@ -103,6 +106,8 @@
     this.threadUtils = threadUtils;
     this.basicLoggingApi = basicLoggingApi;
     this.mainThreadRunner = mainThreadRunner;
+    this.contentStorage = contentStorage;
+    this.journalStorage = journalStorage;
 
     this.persistentStore =
         new PersistentFeedStore(
diff --git a/src/main/java/com/google/android/libraries/feed/testing/store/BUILD b/src/main/java/com/google/android/libraries/feed/testing/store/BUILD
index 9631384..8673c55 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/store/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/testing/store/BUILD
@@ -7,6 +7,7 @@
     testonly = True,
     srcs = glob(["*.java"]),
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/host/storage",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/store",
         "//src/main/java/com/google/android/libraries/feed/common",
diff --git a/src/main/java/com/google/android/libraries/feed/testing/store/FakeStore.java b/src/main/java/com/google/android/libraries/feed/testing/store/FakeStore.java
index d7a96f7..5674398 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/store/FakeStore.java
+++ b/src/main/java/com/google/android/libraries/feed/testing/store/FakeStore.java
@@ -14,6 +14,7 @@
 
 package com.google.android.libraries.feed.testing.store;
 
+import com.google.android.libraries.feed.api.host.storage.CommitResult;
 import com.google.android.libraries.feed.api.internal.common.PayloadWithId;
 import com.google.android.libraries.feed.api.internal.store.ContentMutation;
 import com.google.android.libraries.feed.api.internal.store.SessionMutation;
@@ -41,9 +42,11 @@
 public final class FakeStore extends FeedStore {
   private final FakeThreadUtils fakeThreadUtils;
   private boolean allowCreateNewSession = true;
+  private boolean allowEditContent = true;
   private boolean allowGetPayloads = true;
   private boolean allowGetStreamStructures = true;
   private boolean allowGetSharedStates = true;
+  private boolean clearHeadCalled = false;
 
   public FakeStore(FakeThreadUtils fakeThreadUtils, TaskQueue taskQueue, Clock clock) {
     super(
@@ -78,6 +81,12 @@
   }
 
   @Override
+  public void clearHead() {
+    clearHeadCalled = true;
+    super.clearHead();
+  }
+
+  @Override
   public Result<String> createNewSession() {
     if (!allowCreateNewSession) {
       return Result.failure();
@@ -95,12 +104,51 @@
     return super.getSharedStates();
   }
 
+  @Override
+  public ContentMutation editContent() {
+    if (!allowEditContent) {
+      return new ContentMutation() {
+        @Override
+        public ContentMutation add(String contentId, StreamPayload payload) {
+          return this;
+        }
+
+        @Override
+        public CommitResult commit() {
+          return CommitResult.FAILURE;
+        }
+      };
+    }
+
+    return super.editContent();
+  }
+
+  /** Returns if {@link FeedStore#clearAll()} was called. */
+  public boolean getClearHeadCalled() {
+    return clearHeadCalled;
+  }
+
+  /** Clears all content storage. */
+  public FakeStore clearContent() {
+    contentStorage.commit(
+        new com.google.android.libraries.feed.api.host.storage.ContentMutation.Builder()
+            .deleteAll()
+            .build());
+    return this;
+  }
+
   /** Sets whether to fail on calls to {@link getStreamStructures(String)}. */
   public FakeStore setAllowGetStreamStructures(boolean value) {
     allowGetStreamStructures = value;
     return this;
   }
 
+  /** Sets whether to fail on calls to {@link editContent()}. */
+  public FakeStore setAllowEditContent(boolean value) {
+    allowEditContent = value;
+    return this;
+  }
+
   /** Sets whether to fail on calls to {@link createNewSession()}. */
   public FakeStore setAllowCreateNewSession(boolean value) {
     allowCreateNewSession = value;
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/BUILD b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/BUILD
index 9b03cee..21f80a2 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/BUILD
@@ -29,6 +29,7 @@
         "//src/main/java/com/google/android/libraries/feed/feedmodelprovider",
         "//src/main/java/com/google/android/libraries/feed/feedsessionmanager",
         "//src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal",
+        "//src/main/java/com/google/android/libraries/feed/testing/host/logging",
         "//src/main/java/com/google/android/libraries/feed/testing/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/testing/requestmanager",
         "//src/main/java/com/google/android/libraries/feed/testing/store",
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImplTest.java b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImplTest.java
index 18704ec..4b40168 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImplTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImplTest.java
@@ -25,6 +25,7 @@
 import com.google.android.libraries.feed.api.common.MutationContext;
 import com.google.android.libraries.feed.api.host.config.Configuration;
 import com.google.android.libraries.feed.api.host.config.Configuration.ConfigKey;
+import com.google.android.libraries.feed.api.host.logging.InternalFeedError;
 import com.google.android.libraries.feed.api.host.logging.RequestReason;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi.RequestBehavior;
@@ -56,6 +57,7 @@
 import com.google.android.libraries.feed.feedsessionmanager.internal.HeadSessionImpl;
 import com.google.android.libraries.feed.feedsessionmanager.internal.Session;
 import com.google.android.libraries.feed.feedsessionmanager.internal.SessionCache;
+import com.google.android.libraries.feed.testing.host.logging.FakeBasicLoggingApi;
 import com.google.android.libraries.feed.testing.proto.UiContextForTestProto.UiContextForTest;
 import com.google.android.libraries.feed.testing.protocoladapter.FakeProtocolAdapter;
 import com.google.android.libraries.feed.testing.requestmanager.FakeActionUploadRequestManager;
@@ -102,6 +104,7 @@
           .setTable("piet-shared-state")
           .build();
   private static final String SESSION_ID = "session:1";
+  private static final int STORAGE_MISS_THRESHOLD = 4;
 
   private final ContentIdGenerators contentIdGenerators = new ContentIdGenerators();
   private final ContentIdGenerators idGenerators = new ContentIdGenerators();
@@ -111,6 +114,7 @@
 
   private Configuration configuration;
   private FakeActionUploadRequestManager fakeActionUploadRequestManager;
+  private FakeBasicLoggingApi fakeBasicLoggingApi;
   private FakeMainThreadRunner fakeMainThreadRunner;
   private FakeProtocolAdapter fakeProtocolAdapter;
   private FakeFeedRequestManager fakeRequestManager;
@@ -134,7 +138,9 @@
     configuration =
         new Configuration.Builder()
             .put(ConfigKey.UNDOABLE_ACTIONS_ENABLED, uploadingActionsEnabled)
+            .put(ConfigKey.STORAGE_MISS_THRESHOLD, STORAGE_MISS_THRESHOLD)
             .build();
+    fakeBasicLoggingApi = new FakeBasicLoggingApi();
     fakeThreadUtils = FakeThreadUtils.withThreadChecks();
     fakeMainThreadRunner =
         FakeMainThreadRunner.runTasksImmediatelyWithThreadChecks(fakeThreadUtils);
@@ -351,6 +357,30 @@
   }
 
   @Test
+  public void testMissingFeaturesBeyondThreshold_switchToEphemeralMode() {
+    FeedSessionManagerImpl sessionManager = getInitializedSessionManager();
+    populateSession(sessionManager, STORAGE_MISS_THRESHOLD, 1, true, null);
+    fakeStore.clearContent();
+
+    ModelProvider modelProvider = getModelProvider(sessionManager);
+    assertThat(modelProvider).isNotNull();
+    assertThat(fakeStore.isEphemeralMode()).isTrue();
+    assertThat(fakeBasicLoggingApi.lastInternalError)
+        .isEqualTo(InternalFeedError.STORAGE_MISS_BEYOND_THRESHOLD);
+  }
+
+  @Test
+  public void testMissingFeaturesAtThreshold_doesNotSwitchToEphemeralMode() {
+    FeedSessionManagerImpl sessionManager = getInitializedSessionManager();
+    populateSession(sessionManager, STORAGE_MISS_THRESHOLD - 1, 1, true, null);
+    fakeStore.clearContent();
+
+    ModelProvider modelProvider = getModelProvider(sessionManager);
+    assertThat(modelProvider).isNotNull();
+    assertThat(fakeStore.isEphemeralMode()).isFalse();
+  }
+
+  @Test
   public void testNoCardsError() {
     FeedSessionManagerImpl sessionManager = getInitializedSessionManager();
     sessionManager.getUpdateConsumer(EMPTY_MUTATION).accept(Result.failure());
@@ -802,7 +832,9 @@
             schedulerApi,
             configuration,
             fakeClock,
-            appLifecycleListener)
+            appLifecycleListener,
+            fakeMainThreadRunner,
+            fakeBasicLoggingApi)
         .create();
   }
 }
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/BUILD b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/BUILD
index 5460249..b289e18 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/BUILD
@@ -82,23 +82,19 @@
         "//src/main/java/com/google/android/libraries/feed/api/client/knowncontent",
         "//src/main/java/com/google/android/libraries/feed/api/common",
         "//src/main/java/com/google/android/libraries/feed/api/host/config",
+        "//src/main/java/com/google/android/libraries/feed/api/host/logging",
         "//src/main/java/com/google/android/libraries/feed/api/host/scheduler",
-        "//src/main/java/com/google/android/libraries/feed/api/host/storage",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common/testing",
         "//src/main/java/com/google/android/libraries/feed/api/internal/modelprovider",
-        "//src/main/java/com/google/android/libraries/feed/api/internal/store",
         "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
         "//src/main/java/com/google/android/libraries/feed/common/functional",
-        "//src/main/java/com/google/android/libraries/feed/common/protoextensions",
         "//src/main/java/com/google/android/libraries/feed/common/time",
         "//src/main/java/com/google/android/libraries/feed/common/time/testing",
         "//src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal",
-        "//src/main/java/com/google/android/libraries/feed/feedstore",
-        "//src/main/java/com/google/android/libraries/feed/feedstore/testing",
-        "//src/main/java/com/google/android/libraries/feed/hostimpl/storage/testing",
         "//src/main/java/com/google/android/libraries/feed/testing/host/logging",
+        "//src/main/java/com/google/android/libraries/feed/testing/store",
         "//src/main/proto/com/google/android/libraries/feed/api/internal/proto:client_feed_java_proto_lite",
         "//third_party:robolectric",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutationTest.java b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutationTest.java
index bd8f1fc..7084545 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutationTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutationTest.java
@@ -19,7 +19,6 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
@@ -28,8 +27,8 @@
 import com.google.android.libraries.feed.api.client.knowncontent.KnownContent;
 import com.google.android.libraries.feed.api.common.MutationContext;
 import com.google.android.libraries.feed.api.host.config.Configuration;
+import com.google.android.libraries.feed.api.host.logging.InternalFeedError;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
-import com.google.android.libraries.feed.api.host.storage.ContentStorageDirect;
 import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.common.PayloadWithId;
 import com.google.android.libraries.feed.api.internal.common.SemanticPropertiesWithId;
@@ -38,21 +37,16 @@
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelError.ErrorType;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelProvider;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelProvider.State;
-import com.google.android.libraries.feed.api.internal.store.Store;
 import com.google.android.libraries.feed.common.Result;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeTaskQueue;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
 import com.google.android.libraries.feed.common.functional.Supplier;
-import com.google.android.libraries.feed.common.protoextensions.FeedExtensionRegistry;
 import com.google.android.libraries.feed.common.time.TimingUtils;
 import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.feedsessionmanager.internal.SessionManagerMutation.MutationCommitter;
-import com.google.android.libraries.feed.feedstore.FeedStore;
-import com.google.android.libraries.feed.feedstore.testing.DelegatingStore;
-import com.google.android.libraries.feed.hostimpl.storage.testing.InMemoryContentStorage;
-import com.google.android.libraries.feed.hostimpl.storage.testing.InMemoryJournalStorage;
 import com.google.android.libraries.feed.testing.host.logging.FakeBasicLoggingApi;
+import com.google.android.libraries.feed.testing.store.FakeStore;
 import com.google.common.collect.ImmutableList;
 import com.google.protobuf.ByteString;
 import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
@@ -78,11 +72,11 @@
 public class SessionManagerMutationTest {
   private final Configuration configuration = new Configuration.Builder().build();
   private final ContentIdGenerators idGenerators = new ContentIdGenerators();
-  private final ContentStorageDirect contentStorage = new InMemoryContentStorage();
   private final FakeBasicLoggingApi fakeBasicLoggingApi = new FakeBasicLoggingApi();
   private final FakeClock fakeClock = new FakeClock();
+  private final FakeMainThreadRunner fakeMainThreadRunner =
+      FakeMainThreadRunner.runTasksImmediately();
   private final FakeThreadUtils fakeThreadUtils = FakeThreadUtils.withThreadChecks();
-  private final FeedExtensionRegistry extensionRegistry = new FeedExtensionRegistry(ArrayList::new);
   private final String rootContentId = idGenerators.createRootContentId(0);
   private final TimingUtils timingUtils = new TimingUtils();
 
@@ -93,33 +87,21 @@
   private ModelError notifyError;
   private Session notifySession;
   private SessionCache sessionCache;
-  private Store storeSpy;
+  private FakeStore fakeStore;
 
   @Before
   public void setUp() {
     initMocks(this);
-    storeSpy = null;
     notifySession = null;
     fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeThreadUtils);
     fakeTaskQueue.initialize(() -> {});
     fakeThreadUtils.enforceMainThread(false);
-    Store store =
-        new FeedStore(
-            timingUtils,
-            extensionRegistry,
-            contentStorage,
-            new InMemoryJournalStorage(),
-            fakeThreadUtils,
-            fakeTaskQueue,
-            fakeClock,
-            fakeBasicLoggingApi,
-            FakeMainThreadRunner.runTasksImmediately());
-    storeSpy = spy(new DelegatingStore(store));
+    fakeStore = new FakeStore(fakeThreadUtils, fakeTaskQueue, fakeClock);
     SessionFactory sessionFactory =
-        new SessionFactory(storeSpy, fakeTaskQueue, timingUtils, fakeThreadUtils, configuration);
+        new SessionFactory(fakeStore, fakeTaskQueue, timingUtils, fakeThreadUtils, configuration);
     sessionCache =
         new SessionCache(
-            storeSpy, fakeTaskQueue, sessionFactory, 10L, timingUtils, fakeThreadUtils, fakeClock);
+            fakeStore, fakeTaskQueue, sessionFactory, 10L, timingUtils, fakeThreadUtils, fakeClock);
     sessionCache.initialize();
     contentCache = new ContentCache();
   }
@@ -202,7 +184,7 @@
     MutationCommitter mutationCommitter = getMutationCommitter(EMPTY_CONTEXT);
     mutationCommitter.accept(Result.success(Model.of(dataOperations)));
 
-    Result<List<PayloadWithId>> result = storeSpy.getPayloads(contentIds);
+    Result<List<PayloadWithId>> result = fakeStore.getPayloads(contentIds);
     assertThat(result.isSuccessful()).isTrue();
     assertThat(result.getValue()).hasSize(contentIds.size());
     for (PayloadWithId payload : result.getValue()) {
@@ -214,6 +196,29 @@
   }
 
   @Test
+  public void testUpdateContent_failedCommitLogsError() {
+    fakeStore.setAllowEditContent(false);
+
+    List<String> contentIds = getContentIds(3);
+    List<Pair<StreamStructure, StreamPayload>> features =
+        getFeatures(
+            contentIds,
+            () ->
+                StreamPayload.newBuilder()
+                    .setStreamFeature(StreamFeature.getDefaultInstance())
+                    .build());
+    List<StreamDataOperation> dataOperations = new ArrayList<>();
+    for (Pair<StreamStructure, StreamPayload> feature : features) {
+      dataOperations.add(getStreamDataOperation(feature.first, feature.second));
+    }
+    MutationCommitter mutationCommitter = getMutationCommitter(EMPTY_CONTEXT);
+    mutationCommitter.accept(Result.success(Model.of(dataOperations)));
+
+    assertThat(fakeBasicLoggingApi.lastInternalError)
+        .isEqualTo(InternalFeedError.CONTENT_MUTATION_FAILED);
+  }
+
+  @Test
   public void testSemanticData() {
     List<String> contentIds = getContentIds(2);
     List<Pair<StreamStructure, StreamPayload>> features =
@@ -230,7 +235,7 @@
     MutationCommitter mutationCommitter = getMutationCommitter(EMPTY_CONTEXT);
     mutationCommitter.accept(Result.success(Model.of(dataOperations)));
 
-    Result<List<SemanticPropertiesWithId>> result = storeSpy.getSemanticProperties(contentIds);
+    Result<List<SemanticPropertiesWithId>> result = fakeStore.getSemanticProperties(contentIds);
     assertThat(result.isSuccessful()).isTrue();
     assertThat(result.getValue()).hasSize(contentIds.size());
     for (SemanticPropertiesWithId payload : result.getValue()) {
@@ -274,7 +279,7 @@
   public void testInvalidateHead() {
     MutationCommitter mutationCommitter = getMutationCommitter(EMPTY_CONTEXT);
     mutationCommitter.resetHead(null);
-    verify(storeSpy).clearHead();
+    assertThat(fakeStore.getClearHeadCalled()).isTrue();
   }
 
   @Test
@@ -332,14 +337,16 @@
   private MutationCommitter getMutationCommitter(MutationContext mutationContext) {
     SessionManagerMutation mutation =
         new SessionManagerMutation(
-            storeSpy,
+            fakeStore,
             sessionCache,
             contentCache,
             fakeTaskQueue,
             schedulerApi,
             fakeThreadUtils,
             timingUtils,
-            fakeClock);
+            fakeClock,
+            fakeMainThreadRunner,
+            fakeBasicLoggingApi);
     return (MutationCommitter)
         mutation.createCommitter(
             "task", mutationContext, this::notifySessionError, knownContentListener);