Delay garbage collection while other work is enqueued to run.

PiperOrigin-RevId: 263950827
Change-Id: I7688cb2b960632d485e0891625c3a4827e528866
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 fbde1df..e213172 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
@@ -212,6 +212,7 @@
         new TaskQueue(basicLoggingApi, singleThreadExecutor, mainThreadRunner, clock);
     FeedStore store =
         new FeedStore(
+            configuration,
             timingUtils,
             extensionRegistry,
             contentStorage,
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 aa46daa..54c3bf7 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
@@ -73,6 +73,8 @@
     // Boolean which if true, will ask the server for an menu option to launch the interest
     // management customize page.
     ConfigKey.MANAGE_INTERESTS_ENABLED,
+    // The maximum number of times that the GC task can re-enqueue itself.
+    ConfigKey.MAXIMUM_GC_ATTEMPTS,
     ConfigKey.MINIMUM_VALID_ACTION_RATIO,
     ConfigKey.NON_CACHED_MIN_PAGE_SIZE,
     ConfigKey.NON_CACHED_PAGE_SIZE,
@@ -133,6 +135,7 @@
     String LIMIT_PAGE_UPDATES_IN_HEAD = "limit_page_updates_in_head";
     String LOGGING_IMMEDIATE_CONTENT_THRESHOLD_MS = "logging_immediate_content_threshold_ms";
     String MANAGE_INTERESTS_ENABLED = "manage_interests_enabled";
+    String MAXIMUM_GC_ATTEMPTS = "maximum_gc_attempts";
     String MINIMUM_VALID_ACTION_RATIO = "minimum_valid_action_ratio";
     String NON_CACHED_MIN_PAGE_SIZE = "non_cached_min_page_size";
     String NON_CACHED_PAGE_SIZE = "non_cached_page_size";
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 749e022..ce79346 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
@@ -109,6 +109,7 @@
         new TaskQueue(fakeBasicLoggingApi, fakeDirectExecutor, fakeMainThreadRunner, fakeClock);
     store =
         new FeedStore(
+            configuration,
             timingUtils,
             extensionRegistry,
             contentStorage,
diff --git a/src/main/java/com/google/android/libraries/feed/feedstore/BUILD b/src/main/java/com/google/android/libraries/feed/feedstore/BUILD
index e3eaf77..ac7f5b9 100644
--- a/src/main/java/com/google/android/libraries/feed/feedstore/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/feedstore/BUILD
@@ -6,6 +6,7 @@
     name = "feedstore",
     srcs = glob(["*.java"]),
     deps = [
+        "//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/storage",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common",
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 30fe916..6d0c827 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
@@ -14,6 +14,7 @@
 
 package com.google.android.libraries.feed.feedstore;
 
+import com.google.android.libraries.feed.api.host.config.Configuration;
 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;
@@ -91,6 +92,7 @@
   protected final JournalStorageDirect journalStorage;
 
   public FeedStore(
+      Configuration configuration,
       TimingUtils timingUtils,
       FeedExtensionRegistry extensionRegistry,
       ContentStorageDirect contentStorage,
@@ -111,10 +113,12 @@
 
     this.persistentStore =
         new PersistentFeedStore(
+            configuration,
             this.timingUtils,
             extensionRegistry,
             contentStorage,
             journalStorage,
+            this.taskQueue,
             threadUtils,
             this.clock,
             this.storeHelper,
diff --git a/src/main/java/com/google/android/libraries/feed/feedstore/internal/BUILD b/src/main/java/com/google/android/libraries/feed/feedstore/internal/BUILD
index e918abc..872dbda 100644
--- a/src/main/java/com/google/android/libraries/feed/feedstore/internal/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/feedstore/internal/BUILD
@@ -6,6 +6,7 @@
     name = "internal",
     srcs = glob(["*.java"]),
     deps = [
+        "//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/storage",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common",
diff --git a/src/main/java/com/google/android/libraries/feed/feedstore/internal/ContentGc.java b/src/main/java/com/google/android/libraries/feed/feedstore/internal/ContentGc.java
index 425e9d1..f7b615e 100644
--- a/src/main/java/com/google/android/libraries/feed/feedstore/internal/ContentGc.java
+++ b/src/main/java/com/google/android/libraries/feed/feedstore/internal/ContentGc.java
@@ -18,10 +18,15 @@
 import static com.google.android.libraries.feed.feedstore.internal.FeedStoreConstants.SHARED_STATE_PREFIX;
 import static com.google.android.libraries.feed.feedstore.internal.FeedStoreConstants.UPLOADABLE_ACTION_PREFIX;
 
+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.Task;
 import com.google.android.libraries.feed.api.host.storage.CommitResult;
 import com.google.android.libraries.feed.api.host.storage.ContentMutation;
 import com.google.android.libraries.feed.api.host.storage.ContentStorageDirect;
 import com.google.android.libraries.feed.common.Result;
+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.Supplier;
 import com.google.android.libraries.feed.common.logging.Logger;
 import com.google.android.libraries.feed.common.time.TimingUtils;
@@ -41,24 +46,39 @@
   private final Supplier<Set<StreamLocalAction>> actionsSupplier;
   private final ContentStorageDirect contentStorageDirect;
   private final TimingUtils timingUtils;
+  private final TaskQueue taskQueue;
   private final boolean keepSharedStates;
+  private final long maxAllowedGcAttempts;
+
+  private int contentGcAttempts = 0;
 
   ContentGc(
+      Configuration configuration,
       Supplier<Set<String>> accessibleContentSupplier,
       Set<String> reservedContentIds,
       Supplier<Set<StreamLocalAction>> actionsSupplier,
       ContentStorageDirect contentStorageDirect,
+      TaskQueue taskQueue,
       TimingUtils timingUtils,
       boolean keepSharedStates) {
     this.accessibleContentSupplier = accessibleContentSupplier;
     this.reservedContentIds = reservedContentIds;
     this.actionsSupplier = actionsSupplier;
     this.contentStorageDirect = contentStorageDirect;
+    this.taskQueue = taskQueue;
     this.timingUtils = timingUtils;
     this.keepSharedStates = keepSharedStates;
+    maxAllowedGcAttempts = configuration.getValueOrDefault(ConfigKey.MAXIMUM_GC_ATTEMPTS, 10L);
   }
 
   void gc() {
+    if (taskQueue.hasBacklog() && contentGcAttempts < maxAllowedGcAttempts) {
+      Logger.i(TAG, "Re-enqueuing triggerContentGc; attempts(%d)", contentGcAttempts);
+      contentGcAttempts++;
+      taskQueue.execute(Task.GARBAGE_COLLECT_CONTENT, TaskType.BACKGROUND, this::gc);
+      return;
+    }
+
     ElapsedTimeTracker tracker = timingUtils.getElapsedTimeTracker(TAG);
     Set<String> population = getPopulation();
     // remove the items in the population that are accessible, reserved, or semantic properties
diff --git a/src/main/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStore.java b/src/main/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStore.java
index f26a5f2..4ae415f 100644
--- a/src/main/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStore.java
+++ b/src/main/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStore.java
@@ -20,6 +20,7 @@
 import static com.google.android.libraries.feed.feedstore.internal.FeedStoreConstants.UPLOADABLE_ACTION_PREFIX;
 
 import android.util.Base64;
+import com.google.android.libraries.feed.api.host.config.Configuration;
 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.storage.CommitResult;
@@ -41,6 +42,7 @@
 import com.google.android.libraries.feed.api.internal.store.UploadableActionMutation;
 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.functional.Supplier;
 import com.google.android.libraries.feed.common.intern.Interner;
 import com.google.android.libraries.feed.common.intern.InternerWithStats;
@@ -76,10 +78,12 @@
 
   private static final String TAG = "PersistentFeedStore";
 
+  private final Configuration configuration;
   private final TimingUtils timingUtils;
   private final FeedExtensionRegistry extensionRegistry;
   private final ContentStorageDirect contentStorageDirect;
   private final JournalStorageDirect journalStorageDirect;
+  private final TaskQueue taskQueue;
   private final ThreadUtils threadUtils;
   private final Clock clock;
   private final FeedStoreHelper storeHelper;
@@ -96,19 +100,23 @@
       new StreamPayloadInterner(contentIdStringInterner);
 
   public PersistentFeedStore(
+      Configuration configuration,
       TimingUtils timingUtils,
       FeedExtensionRegistry extensionRegistry,
       ContentStorageDirect contentStorageDirect,
       JournalStorageDirect journalStorageDirect,
+      TaskQueue taskQueue,
       ThreadUtils threadUtils,
       Clock clock,
       FeedStoreHelper storeHelper,
       BasicLoggingApi basicLoggingApi,
       MainThreadRunner mainThreadRunner) {
+    this.configuration = configuration;
     this.timingUtils = timingUtils;
     this.extensionRegistry = extensionRegistry;
     this.contentStorageDirect = contentStorageDirect;
     this.journalStorageDirect = journalStorageDirect;
+    this.taskQueue = taskQueue;
     this.threadUtils = threadUtils;
     this.clock = clock;
     this.storeHelper = storeHelper;
@@ -395,10 +403,12 @@
         };
 
     return new ContentGc(
+            configuration,
             accessibleContent,
             reservedContentIds,
             dismissActionSupplier,
             contentStorageDirect,
+            taskQueue,
             timingUtils,
             keepSharedStates)
         ::gc;
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 8673c55..2db5f8e 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/config",
         "//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",
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 5674398..4029786 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.config.Configuration;
 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;
@@ -48,8 +49,13 @@
   private boolean allowGetSharedStates = true;
   private boolean clearHeadCalled = false;
 
-  public FakeStore(FakeThreadUtils fakeThreadUtils, TaskQueue taskQueue, Clock clock) {
+  public FakeStore(
+      Configuration configuration,
+      FakeThreadUtils fakeThreadUtils,
+      TaskQueue taskQueue,
+      Clock clock) {
     super(
+        configuration,
         new TimingUtils(),
         new FeedExtensionRegistry(ArrayList::new),
         new InMemoryContentStorage(),
diff --git a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManagerTest.java b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManagerTest.java
index 771a021..744062e 100644
--- a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManagerTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManagerTest.java
@@ -102,7 +102,7 @@
     fakeNetworkClient = new FakeNetworkClient(fakeThreadUtils);
     fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeThreadUtils);
     fakeProtocolAdapter = new FakeProtocolAdapter();
-    fakeStore = new FakeStore(fakeThreadUtils, fakeTaskQueue, fakeClock);
+    fakeStore = new FakeStore(configuration, fakeThreadUtils, fakeTaskQueue, fakeClock);
     consumer =
         new RequiredConsumer<>(
             input -> {
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 0a884ea..a9f2779 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
@@ -147,7 +147,7 @@
     fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeThreadUtils);
     appLifecycleListener = new FeedAppLifecycleListener(fakeThreadUtils);
     fakeActionUploadRequestManager = new FakeActionUploadRequestManager(fakeThreadUtils);
-    fakeStore = new FakeStore(fakeThreadUtils, fakeTaskQueue, fakeClock);
+    fakeStore = new FakeStore(configuration, fakeThreadUtils, fakeTaskQueue, fakeClock);
     fakeProtocolAdapter = new FakeProtocolAdapter();
     fakeRequestManager =
         new FakeFeedRequestManager(
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 9789d80..4aeb76e 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
@@ -28,6 +28,7 @@
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
         "//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/internal/common/testing",
         "//src/main/java/com/google/android/libraries/feed/api/internal/store",
         "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
@@ -51,6 +52,7 @@
     srcs = ["HeadAsStructureTest.java"],
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/host/config",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common/testing",
         "//src/main/java/com/google/android/libraries/feed/api/internal/store",
         "//src/main/java/com/google/android/libraries/feed/common",
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/HeadAsStructureTest.java b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/HeadAsStructureTest.java
index 056481d..6f18315 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/HeadAsStructureTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/HeadAsStructureTest.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.MockitoAnnotations.initMocks;
 
+import com.google.android.libraries.feed.api.host.config.Configuration;
 import com.google.android.libraries.feed.api.internal.common.testing.ContentIdGenerators;
 import com.google.android.libraries.feed.api.internal.common.testing.InternalProtocolBuilder;
 import com.google.android.libraries.feed.api.internal.store.Store;
@@ -52,7 +53,11 @@
   public void setUp() {
     initMocks(this);
     fakeStore =
-        new FakeStore(fakeThreadUtils, new FakeTaskQueue(fakeClock, fakeThreadUtils), fakeClock);
+        new FakeStore(
+            Configuration.getDefaultInstance(),
+            fakeThreadUtils,
+            new FakeTaskQueue(fakeClock, fakeThreadUtils),
+            fakeClock);
     fakeThreadUtils.enforceMainThread(false);
   }
 
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/HeadSessionImplTest.java b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/HeadSessionImplTest.java
index b5bcb85..2de6149 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/HeadSessionImplTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/HeadSessionImplTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 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.internal.common.testing.ContentIdGenerators;
 import com.google.android.libraries.feed.api.internal.common.testing.InternalProtocolBuilder;
 import com.google.android.libraries.feed.api.internal.store.Store;
@@ -44,7 +45,11 @@
   private final FakeThreadUtils fakeThreadUtils = FakeThreadUtils.withoutThreadChecks();
   private final TimingUtils timingUtils = new TimingUtils();
   private final FakeStore fakeStore =
-      new FakeStore(fakeThreadUtils, new FakeTaskQueue(fakeClock, fakeThreadUtils), fakeClock);
+      new FakeStore(
+          Configuration.getDefaultInstance(),
+          fakeThreadUtils,
+          new FakeTaskQueue(fakeClock, fakeThreadUtils),
+          fakeClock);
   private final HeadSessionImpl headSession =
       new HeadSessionImpl(fakeStore, timingUtils, /* limitPageUpdatesInHead= */ false);
 
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCacheTest.java b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCacheTest.java
index 5a504e4..00f1928 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCacheTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCacheTest.java
@@ -70,7 +70,7 @@
   public void setUp() {
     initMocks(this);
     fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeThreadUtils);
-    fakeStore = new FakeStore(fakeThreadUtils, fakeTaskQueue, fakeClock);
+    fakeStore = new FakeStore(configuration, fakeThreadUtils, fakeTaskQueue, fakeClock);
     fakeThreadUtils.enforceMainThread(false);
     fakeTaskQueue.initialize(() -> {});
     sessionCache = getSessionCache();
@@ -106,7 +106,7 @@
   public void testInitialization_accessibleContentShouldBeDeterminedAtGcTime() {
     FakeDirectExecutor fakeDirectExecutor = FakeDirectExecutor.queueAllTasks(fakeThreadUtils);
     fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeDirectExecutor);
-    fakeStore = new FakeStore(fakeThreadUtils, fakeTaskQueue, fakeClock);
+    fakeStore = new FakeStore(configuration, fakeThreadUtils, fakeTaskQueue, fakeClock);
     fakeThreadUtils.enforceMainThread(false);
     fakeTaskQueue.initialize(() -> {});
     sessionCache = getSessionCache();
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 7084545..7a98080 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
@@ -96,7 +96,7 @@
     fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeThreadUtils);
     fakeTaskQueue.initialize(() -> {});
     fakeThreadUtils.enforceMainThread(false);
-    fakeStore = new FakeStore(fakeThreadUtils, fakeTaskQueue, fakeClock);
+    fakeStore = new FakeStore(configuration, fakeThreadUtils, fakeTaskQueue, fakeClock);
     SessionFactory sessionFactory =
         new SessionFactory(fakeStore, fakeTaskQueue, timingUtils, fakeThreadUtils, configuration);
     sessionCache =
diff --git a/src/test/java/com/google/android/libraries/feed/feedstore/FeedStoreEphemeralModeTest.java b/src/test/java/com/google/android/libraries/feed/feedstore/FeedStoreEphemeralModeTest.java
index d82ffb5..822590c 100644
--- a/src/test/java/com/google/android/libraries/feed/feedstore/FeedStoreEphemeralModeTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedstore/FeedStoreEphemeralModeTest.java
@@ -58,6 +58,7 @@
     fakeTaskQueue.initialize(() -> {});
     FeedStore feedStore =
         new FeedStore(
+            configuration,
             timingUtils,
             extensionRegistry,
             contentStorage,
diff --git a/src/test/java/com/google/android/libraries/feed/feedstore/FeedStoreTest.java b/src/test/java/com/google/android/libraries/feed/feedstore/FeedStoreTest.java
index 8237416..ede0bab 100644
--- a/src/test/java/com/google/android/libraries/feed/feedstore/FeedStoreTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedstore/FeedStoreTest.java
@@ -104,6 +104,7 @@
   @Override
   protected Store getStore(MainThreadRunner mainThreadRunner) {
     return new FeedStore(
+        configuration,
         timingUtils,
         extensionRegistry,
         contentStorage,
@@ -143,6 +144,7 @@
     ContentStorageDirect contentStorageSpy = spy(new DelegatingContentStorage(this.contentStorage));
     FeedStore store =
         new FeedStore(
+            configuration,
             timingUtils,
             extensionRegistry,
             contentStorageSpy,
@@ -163,6 +165,7 @@
     ContentStorageDirect contentStorageSpy = spy(new DelegatingContentStorage(this.contentStorage));
     FeedStore store =
         new FeedStore(
+            configuration,
             timingUtils,
             extensionRegistry,
             contentStorageSpy,
diff --git a/src/test/java/com/google/android/libraries/feed/feedstore/internal/BUILD b/src/test/java/com/google/android/libraries/feed/feedstore/internal/BUILD
index 9b71a53..64ebead 100644
--- a/src/test/java/com/google/android/libraries/feed/feedstore/internal/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/feedstore/internal/BUILD
@@ -29,10 +29,15 @@
     srcs = ["ContentGcTest.java"],
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
+        "//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/storage",
         "//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",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
         "//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/feedstore/internal",
         "//src/main/proto/com/google/android/libraries/feed/api/internal/proto:client_feed_java_proto_lite",
         "//third_party:robolectric",
@@ -69,6 +74,7 @@
     srcs = ["PersistentFeedStoreTest.java"],
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
+        "//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/storage",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common",
diff --git a/src/test/java/com/google/android/libraries/feed/feedstore/internal/ContentGcTest.java b/src/test/java/com/google/android/libraries/feed/feedstore/internal/ContentGcTest.java
index 5f76786..f36d054 100644
--- a/src/test/java/com/google/android/libraries/feed/feedstore/internal/ContentGcTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedstore/internal/ContentGcTest.java
@@ -18,10 +18,14 @@
 import static com.google.android.libraries.feed.feedstore.internal.FeedStoreConstants.UPLOADABLE_ACTION_PREFIX;
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
 
+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.Task;
 import com.google.android.libraries.feed.api.host.storage.CommitResult;
 import com.google.android.libraries.feed.api.host.storage.ContentMutation;
 import com.google.android.libraries.feed.api.host.storage.ContentMutation.Builder;
@@ -29,7 +33,13 @@
 import com.google.android.libraries.feed.api.host.storage.ContentStorageDirect;
 import com.google.android.libraries.feed.api.internal.store.LocalActionMutation.ActionType;
 import com.google.android.libraries.feed.common.Result;
+import com.google.android.libraries.feed.common.concurrent.TaskQueue.TaskType;
+import com.google.android.libraries.feed.common.concurrent.testing.FakeDirectExecutor;
+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.time.TimingUtils;
+import com.google.android.libraries.feed.common.time.testing.FakeClock;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.search.now.feed.client.StreamDataProto.StreamLocalAction;
 import java.util.ArrayList;
@@ -38,6 +48,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.robolectric.RobolectricTestRunner;
 
@@ -45,6 +56,7 @@
 @RunWith(RobolectricTestRunner.class)
 public class ContentGcTest {
 
+  @Captor private ArgumentCaptor<ContentMutation> contentMutationCaptor;
   @Mock private ContentStorageDirect contentStorage;
 
   private static final String CONTENT_ID_1 = "contentId1";
@@ -52,12 +64,23 @@
   private static final String SEMANTIC_PROPERTIES_1 = SEMANTIC_PROPERTIES_PREFIX + CONTENT_ID_1;
   private static final String SEMANTIC_PROPERTIES_2 = SEMANTIC_PROPERTIES_PREFIX + CONTENT_ID_2;
   private static final String ACTION_1 = UPLOADABLE_ACTION_PREFIX + CONTENT_ID_1;
+  private static final long MAXIMUM_GC_ATTEMPTS = 3L;
 
+  private final Configuration configuration =
+      new Configuration.Builder().put(ConfigKey.MAXIMUM_GC_ATTEMPTS, MAXIMUM_GC_ATTEMPTS).build();
+  private final FakeClock fakeClock = new FakeClock();
+  private final FakeThreadUtils fakeThreadUtils = FakeThreadUtils.withThreadChecks();
+  private final FakeDirectExecutor fakeDirectExecutor =
+      FakeDirectExecutor.queueAllTasks(fakeThreadUtils);
   private final TimingUtils timingUtils = new TimingUtils();
 
+  private FakeTaskQueue fakeTaskQueue;
+
   @Before
   public void setUp() throws Exception {
     initMocks(this);
+    fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeDirectExecutor);
+    fakeTaskQueue.initialize(() -> {});
   }
 
   @Test
@@ -68,15 +91,15 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             ImmutableSet::of,
             ImmutableSet.of(),
             ImmutableSet::of,
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ true);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations =
         new Builder().delete(CONTENT_ID_1).delete(CONTENT_ID_2).build().getOperations();
@@ -92,15 +115,15 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             () -> ImmutableSet.of(CONTENT_ID_1),
             ImmutableSet.of(),
             ImmutableSet::of,
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ true);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations =
         new Builder().delete(CONTENT_ID_2).build().getOperations();
@@ -116,15 +139,15 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             ImmutableSet::of,
             ImmutableSet.of(CONTENT_ID_1),
             ImmutableSet::of,
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ true);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations =
         new Builder().delete(CONTENT_ID_2).build().getOperations();
@@ -139,15 +162,15 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             ImmutableSet::of,
             ImmutableSet.of(),
             ImmutableSet::of,
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ true);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations = new Builder().build().getOperations();
     List<ContentOperation> resultOperations = contentMutationCaptor.getValue().getOperations();
@@ -164,15 +187,15 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             ImmutableSet::of,
             ImmutableSet.of(),
             ImmutableSet::of,
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ true);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations =
         new Builder()
@@ -192,6 +215,7 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             ImmutableSet::of,
             ImmutableSet.of(),
             () ->
@@ -201,11 +225,10 @@
                         .setFeatureContentId(CONTENT_ID_1)
                         .build()),
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ true);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations =
         new Builder().delete(SEMANTIC_PROPERTIES_2).build().getOperations();
@@ -222,15 +245,15 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             () -> ImmutableSet.of(CONTENT_ID_1),
             ImmutableSet.of(),
             ImmutableSet::of,
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ true);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations =
         new Builder().delete(SEMANTIC_PROPERTIES_2).build().getOperations();
@@ -246,15 +269,15 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             ImmutableSet::of,
             ImmutableSet.of(),
             ImmutableSet::of,
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ true);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations =
         new Builder().delete(CONTENT_ID_2).build().getOperations();
@@ -270,15 +293,15 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             ImmutableSet::of,
             ImmutableSet.of(),
             ImmutableSet::of,
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ false);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations =
         new Builder()
@@ -298,15 +321,15 @@
     mockContentStorageWithContents(contentKeys);
     ContentGc contentGc =
         new ContentGc(
+            configuration,
             () -> ImmutableSet.of(CONTENT_ID_1),
             ImmutableSet.of(),
             ImmutableSet::of,
             contentStorage,
+            fakeTaskQueue,
             timingUtils,
             /* keepSharedStates= */ false);
     contentGc.gc();
-    ArgumentCaptor<ContentMutation> contentMutationCaptor =
-        ArgumentCaptor.forClass(ContentMutation.class);
     verify(contentStorage).commit(contentMutationCaptor.capture());
     List<ContentOperation> expectedOperations =
         new Builder().delete(CONTENT_ID_2).build().getOperations();
@@ -314,6 +337,72 @@
     assertListsContainSameElements(expectedOperations, resultOperations);
   }
 
+  @Test
+  public void gc_delayWhileTaskEnqueued() {
+    fakeTaskQueue.execute(Task.UNKNOWN, TaskType.BACKGROUND, () -> {});
+    List<String> contentKeys = ImmutableList.of(CONTENT_ID_1, CONTENT_ID_2);
+    mockContentStorageWithContents(contentKeys);
+    ContentGc contentGc =
+        new ContentGc(
+            configuration,
+            ImmutableSet::of,
+            ImmutableSet.of(),
+            ImmutableSet::of,
+            contentStorage,
+            fakeTaskQueue,
+            timingUtils,
+            /* keepSharedStates= */ false);
+    contentGc.gc();
+
+    assertThat(fakeTaskQueue.getBackgroundTaskCount()).isEqualTo(2);
+    verify(contentStorage, never()).commit(contentMutationCaptor.capture());
+  }
+
+  @Test
+  public void gc_runWhenMaximumAttemptsReached() {
+    fakeTaskQueue.execute(Task.UNKNOWN, TaskType.BACKGROUND, () -> {});
+    List<String> contentKeys = ImmutableList.of(CONTENT_ID_1, CONTENT_ID_2);
+    mockContentStorageWithContents(contentKeys);
+    ContentGc contentGc =
+        new ContentGc(
+            configuration,
+            ImmutableSet::of,
+            ImmutableSet.of(),
+            ImmutableSet::of,
+            contentStorage,
+            fakeTaskQueue,
+            timingUtils,
+            /* keepSharedStates= */ false);
+    for (int i = 0; i < MAXIMUM_GC_ATTEMPTS; i++) {
+      contentGc.gc();
+      assertThat(fakeTaskQueue.getBackgroundTaskCount()).isEqualTo(2 + i);
+      verify(contentStorage, never()).commit(contentMutationCaptor.capture());
+    }
+
+    contentGc.gc();
+    verify(contentStorage).commit(contentMutationCaptor.capture());
+  }
+
+  @Test
+  public void gc_runWhenMaximumIsZero() {
+    fakeTaskQueue.execute(Task.UNKNOWN, TaskType.BACKGROUND, () -> {});
+    List<String> contentKeys = ImmutableList.of(CONTENT_ID_1, CONTENT_ID_2);
+    mockContentStorageWithContents(contentKeys);
+    ContentGc contentGc =
+        new ContentGc(
+            new Configuration.Builder().put(ConfigKey.MAXIMUM_GC_ATTEMPTS, 0L).build(),
+            ImmutableSet::of,
+            ImmutableSet.of(),
+            ImmutableSet::of,
+            contentStorage,
+            fakeTaskQueue,
+            timingUtils,
+            /* keepSharedStates= */ false);
+    contentGc.gc();
+
+    verify(contentStorage).commit(contentMutationCaptor.capture());
+  }
+
   private void mockContentStorageWithContents(List<String> contentKeys) {
     when(contentStorage.getAllKeys()).thenReturn(Result.success(contentKeys));
     when(contentStorage.commit(any(ContentMutation.class))).thenReturn(CommitResult.SUCCESS);
diff --git a/src/test/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStoreTest.java b/src/test/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStoreTest.java
index c6d4d14..9d59238 100644
--- a/src/test/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStoreTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStoreTest.java
@@ -18,10 +18,10 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.MockitoAnnotations.initMocks;
 
+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.storage.CommitResult;
 import com.google.android.libraries.feed.api.host.storage.ContentMutation;
@@ -30,12 +30,13 @@
 import com.google.android.libraries.feed.api.host.storage.JournalStorageDirect;
 import com.google.android.libraries.feed.api.internal.common.PayloadWithId;
 import com.google.android.libraries.feed.api.internal.common.SemanticPropertiesWithId;
-import com.google.android.libraries.feed.api.internal.common.ThreadUtils;
 import com.google.android.libraries.feed.api.internal.store.LocalActionMutation.ActionType;
 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.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.protoextensions.FeedExtensionRegistry;
 import com.google.android.libraries.feed.feedstore.testing.AbstractClearableFeedStoreTest;
 import com.google.android.libraries.feed.feedstore.testing.DelegatingContentStorage;
@@ -68,26 +69,33 @@
 @RunWith(RobolectricTestRunner.class)
 public class PersistentFeedStoreTest extends AbstractClearableFeedStoreTest {
 
-  private ThreadUtils threadUtils = mock(ThreadUtils.class);
+  private final FakeThreadUtils fakeThreadUtils = FakeThreadUtils.withThreadChecks();
   private final FeedExtensionRegistry extensionRegistry = new FeedExtensionRegistry(ArrayList::new);
   private final ContentStorageDirect contentStorage = new InMemoryContentStorage();
   private final JournalStorageDirect journalStorage = new InMemoryJournalStorage();
   private final FakeBasicLoggingApi basicLoggingApi = new FakeBasicLoggingApi();
   private final FakeMainThreadRunner mainThreadRunner = FakeMainThreadRunner.runTasksImmediately();
 
+  private FakeTaskQueue fakeTaskQueue;
+
   @Before
   public void setUp() throws Exception {
     initMocks(this);
+    fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeThreadUtils);
+    fakeTaskQueue.initialize(() -> {});
+    fakeThreadUtils.enforceMainThread(false);
   }
 
   @Override
   protected Store getStore(MainThreadRunner mainThreadRunner) {
     return new PersistentFeedStore(
+        Configuration.getDefaultInstance(),
         timingUtils,
         extensionRegistry,
         contentStorage,
         journalStorage,
-        threadUtils,
+        fakeTaskQueue,
+        fakeThreadUtils,
         fakeClock,
         new FeedStoreHelper(),
         basicLoggingApi,
@@ -99,11 +107,13 @@
     ContentStorageDirect contentStorageSpy = spy(new DelegatingContentStorage(contentStorage));
     PersistentFeedStore store =
         new PersistentFeedStore(
+            Configuration.getDefaultInstance(),
             timingUtils,
             extensionRegistry,
             contentStorageSpy,
             journalStorage,
-            threadUtils,
+            fakeTaskQueue,
+            fakeThreadUtils,
             fakeClock,
             new FeedStoreHelper(),
             basicLoggingApi,
@@ -119,11 +129,13 @@
     ContentStorageDirect contentStorageSpy = spy(new DelegatingContentStorage(contentStorage));
     PersistentFeedStore store =
         new PersistentFeedStore(
+            Configuration.getDefaultInstance(),
             timingUtils,
             extensionRegistry,
             contentStorageSpy,
             journalStorage,
-            threadUtils,
+            fakeTaskQueue,
+            fakeThreadUtils,
             fakeClock,
             new FeedStoreHelper(),
             basicLoggingApi,
@@ -151,11 +163,13 @@
     JournalStorageDirect journalStorageSpy = spy(new DelegatingJournalStorage(journalStorage));
     PersistentFeedStore store =
         new PersistentFeedStore(
+            Configuration.getDefaultInstance(),
             timingUtils,
             extensionRegistry,
             contentStorage,
             journalStorageSpy,
-            threadUtils,
+            fakeTaskQueue,
+            fakeThreadUtils,
             fakeClock,
             new FeedStoreHelper(),
             basicLoggingApi,
@@ -186,11 +200,13 @@
     JournalStorageDirect journalStorageSpy = spy(new DelegatingJournalStorage(journalStorage));
     PersistentFeedStore store =
         new PersistentFeedStore(
+            Configuration.getDefaultInstance(),
             timingUtils,
             extensionRegistry,
             contentStorage,
             journalStorageSpy,
-            threadUtils,
+            fakeTaskQueue,
+            fakeThreadUtils,
             fakeClock,
             new FeedStoreHelper(),
             basicLoggingApi,