-
Notifications
You must be signed in to change notification settings - Fork 615
Enable LRU GC #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable LRU GC #68
Changes from 8 commits
4aa7357
6163a63
b392c4f
096a9d2
2528b78
b406fd4
cdbfa86
a67b447
358ded8
0b5fb37
8cb050a
9244537
b87a3cd
ca8e8a9
38a35ac
1a53dfd
83f7df5
1e97139
0979892
793b0b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,11 +27,14 @@ | |
import com.google.firebase.firestore.EventListener; | ||
import com.google.firebase.firestore.FirebaseFirestoreException; | ||
import com.google.firebase.firestore.FirebaseFirestoreException.Code; | ||
import com.google.firebase.firestore.FirebaseFirestoreSettings; | ||
import com.google.firebase.firestore.auth.CredentialsProvider; | ||
import com.google.firebase.firestore.auth.User; | ||
import com.google.firebase.firestore.core.EventManager.ListenOptions; | ||
import com.google.firebase.firestore.local.LocalSerializer; | ||
import com.google.firebase.firestore.local.LocalStore; | ||
import com.google.firebase.firestore.local.LruDelegate; | ||
import com.google.firebase.firestore.local.LruGarbageCollector; | ||
import com.google.firebase.firestore.local.MemoryPersistence; | ||
import com.google.firebase.firestore.local.Persistence; | ||
import com.google.firebase.firestore.local.SQLitePersistence; | ||
|
@@ -51,6 +54,7 @@ | |
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
|
||
/** | ||
|
@@ -61,6 +65,11 @@ public final class FirestoreClient implements RemoteStore.RemoteStoreCallback { | |
|
||
private static final String LOG_TAG = "FirestoreClient"; | ||
|
||
/** How long we wait to try running LRU GC after SDK initialization. */ | ||
private static final long INITIAL_GC_DELAY_MS = TimeUnit.MINUTES.toMillis(1); | ||
/** Minimum amount of time between GC checks, after the first one. */ | ||
private static final long REGULAR_GC_DELAY_MS = TimeUnit.MINUTES.toMillis(5); | ||
|
||
private final DatabaseInfo databaseInfo; | ||
private final CredentialsProvider credentialsProvider; | ||
private final AsyncQueue asyncQueue; | ||
|
@@ -71,10 +80,15 @@ public final class FirestoreClient implements RemoteStore.RemoteStoreCallback { | |
private SyncEngine syncEngine; | ||
private EventManager eventManager; | ||
|
||
// LRU-related | ||
private boolean gcHasRun = false; | ||
@Nullable private LruDelegate lruDelegate; | ||
@Nullable private AsyncQueue.DelayedTask gcTask; | ||
|
||
public FirestoreClient( | ||
final Context context, | ||
DatabaseInfo databaseInfo, | ||
final boolean usePersistence, | ||
FirebaseFirestoreSettings settings, | ||
CredentialsProvider credentialsProvider, | ||
final AsyncQueue asyncQueue) { | ||
this.databaseInfo = databaseInfo; | ||
|
@@ -105,7 +119,11 @@ public FirestoreClient( | |
try { | ||
// Block on initial user being available | ||
User initialUser = Tasks.await(firstUser.getTask()); | ||
initialize(context, initialUser, usePersistence); | ||
initialize( | ||
context, | ||
initialUser, | ||
settings.isPersistenceEnabled(), | ||
settings.getCacheSizeBytes()); | ||
} catch (InterruptedException | ExecutionException e) { | ||
throw new RuntimeException(e); | ||
} | ||
|
@@ -127,6 +145,9 @@ public Task<Void> shutdown() { | |
() -> { | ||
remoteStore.shutdown(); | ||
persistence.shutdown(); | ||
if (gcTask != null) { | ||
gcTask.cancel(); | ||
} | ||
}); | ||
} | ||
|
||
|
@@ -194,7 +215,7 @@ public <TResult> Task<TResult> transaction( | |
() -> syncEngine.transaction(asyncQueue, updateFunction, retries)); | ||
} | ||
|
||
private void initialize(Context context, User user, boolean usePersistence) { | ||
private void initialize(Context context, User user, boolean usePersistence, long cacheSizeBytes) { | ||
// Note: The initialization work must all be synchronous (we can't dispatch more work) since | ||
// external write/listen operations could get queued to run before that subsequent work | ||
// completes. | ||
|
@@ -203,9 +224,18 @@ private void initialize(Context context, User user, boolean usePersistence) { | |
if (usePersistence) { | ||
LocalSerializer serializer = | ||
new LocalSerializer(new RemoteSerializer(databaseInfo.getDatabaseId())); | ||
persistence = | ||
LruGarbageCollector.Params params = | ||
LruGarbageCollector.Params.WithCacheSizeBytes(cacheSizeBytes); | ||
SQLitePersistence sqlitePersistence = | ||
new SQLitePersistence( | ||
context, databaseInfo.getPersistenceKey(), databaseInfo.getDatabaseId(), serializer); | ||
context, | ||
databaseInfo.getPersistenceKey(), | ||
databaseInfo.getDatabaseId(), | ||
serializer, | ||
params); | ||
lruDelegate = sqlitePersistence.getReferenceDelegate(); | ||
persistence = sqlitePersistence; | ||
scheduleLruGarbageCollection(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to check if the cache size is unlimited, so that we can skip the GC run? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good question, but I think I'd prefer to leave it the way it is. The reasoning is this: currently the check for actually running the garbage collection is confined to one place: the garbage collector. If in the future we decide to customize the GC runs more than we have, I think it will be helpful to have that logic encapsulated. In addition, we would still need to check against There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can look at this more in the future, but it might actually make sense to split these up, using the same argument that you provided: In FirebaseFirestoreSettings/FirestoreClient we can validate whether the settings for the GC algorithm make sense. In the GC code, we can then always assume that all values make sense and we don't have to special case anything. That's just my two cents and your mileage may vary. I believe that for now the PR could be simplified if we didn't have to differentiate between -1 and >0 in the LRU GC. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not wild about What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, I think that would indeed be a compromise, and it's one that I am less happy with than what we currently have. I prefer the current solution over asking GC whether it is enabled and then acting upon this by asking GC to do its job. I thought of another approach though - can we passed the AsyncQueue to the GC algorithm and let it schedule itself if it needs to? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @schmidt-sebastian I started implementing this, but in the process of doing the plumbing to get the Additionally, I believe the only component that is initialized currently with an async queue is the remote store. I don't think we want to extend that list to include persistence, which is also sort of an end run around sync engine and local store having an async queue. My next proposal is this instead: what about an object that is solely responsible for the GC scheduling, owned by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey :) I like the idea of following the pattern of FWIW, the Web JS SDK does have a dependency on the AsyncQueue from the persistence layer (but that doesn't mean that we have to follow this example). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I took a quick stab at it: #111 I'm not sure it's better, maybe just different? Let me know what you think. If you think it's better, then I'll clean it up and we can merge it, otherwise let's keep what's here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do like this quite a bit, since it moves the LRU state out of Firestore Client and nicely encapsulates the different timeouts and its scheduling. If you have some time to clean it up (not sure how much you were thinking?) and merge it into here, then I'd be very happy :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. I'll merge it here shortly. |
||
} else { | ||
persistence = MemoryPersistence.createEagerGcMemoryPersistence(); | ||
} | ||
|
@@ -225,6 +255,19 @@ private void initialize(Context context, User user, boolean usePersistence) { | |
remoteStore.start(); | ||
} | ||
|
||
private void scheduleLruGarbageCollection() { | ||
long delay = gcHasRun ? REGULAR_GC_DELAY_MS : INITIAL_GC_DELAY_MS; | ||
gcTask = | ||
asyncQueue.enqueueAfterDelay( | ||
AsyncQueue.TimerId.GARBAGE_COLLECTION, | ||
delay, | ||
() -> { | ||
localStore.collectGarbage(lruDelegate.getGarbageCollector()); | ||
gcHasRun = true; | ||
scheduleLruGarbageCollection(); | ||
}); | ||
} | ||
|
||
@Override | ||
public void handleRemoteEvent(RemoteEvent remoteEvent) { | ||
syncEngine.handleRemoteEvent(remoteEvent); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Maybe
Constant to use with
, since we want to describe what the value is, rather than what someone can do with it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure