Skip to content

Commit 884e537

Browse files
maneeshtjmwski
andauthored
Fixed get() to make sure it updates the correct paths (#3887)
Fixed `get()` issue where wrong cache paths were being updated in the SyncTree. Co-authored-by: Jan Wyszynski <[email protected]>
1 parent d4fd4a8 commit 884e537

File tree

10 files changed

+413
-43
lines changed

10 files changed

+413
-43
lines changed

firebase-database/src/androidTest/java/com/google/firebase/database/IntegrationTestHelpers.java

+8
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,14 @@ public static void waitFor(Semaphore semaphore, int count, long timeout, TimeUni
334334
assertTrue("Operation timed out", success);
335335
}
336336

337+
public static DatabaseReference translateReference(DatabaseReference node, FirebaseDatabase db) {
338+
return db.getReference().child(node.getPath().toString());
339+
}
340+
341+
public static DataSnapshot referenceAtPath(DatabaseReference node, EventRecord record) {
342+
return record.getSnapshot().child(node.getPath().toString());
343+
}
344+
337345
public static DataSnapshot getSnap(Query ref) throws InterruptedException {
338346

339347
final Semaphore semaphore = new Semaphore(0);

firebase-database/src/androidTest/java/com/google/firebase/database/QueryTest.java

+320-27
Large diffs are not rendered by default.

firebase-database/src/androidTest/java/com/google/firebase/database/future/ReadFuture.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
public class ReadFuture implements Future<List<EventRecord>> {
3535

3636
public interface CompletionCondition {
37-
public boolean isComplete(List<EventRecord> events);
37+
public boolean isComplete(List<EventRecord> events)
38+
throws ExecutionException, InterruptedException;
3839
}
3940

4041
private List<EventRecord> events = new ArrayList<EventRecord>();
@@ -66,13 +67,15 @@ public void onDataChange(DataSnapshot snapshot) {
6667
}
6768
} catch (Exception e) {
6869
exception = e;
70+
ref.removeEventListener(valueEventListener);
6971
finish();
7072
}
7173
}
7274

7375
@Override
7476
public void onCancelled(DatabaseError error) {
7577
wasCancelled = true;
78+
ref.removeEventListener(valueEventListener);
7679
finish();
7780
}
7881
};

firebase-database/src/main/java/com/google/firebase/database/android/SqlPersistenceStorageEngine.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
/**
5959
* This class is an Android/SQL-backed implementation of PersistenceStorageEngine.
6060
*
61-
* <p>The implementation uses 3 tables for persistence: - writes: A list of all currently
61+
* <p>The implementation uses 4 tables for persistence: - writes: A list of all currently
6262
* outstanding (visible) writes. A row represents a single write containing a unique-across-restarts
6363
* write id, the path string, the type which can be 'o' for an overwrite or 'm' for a merge, and the
6464
* serialized node wrapped. Part number is NULL for normal writes. Large writes (>256K) are split
@@ -1058,6 +1058,7 @@ private Cursor loadNestedQuery(Path path, String[] columns) {
10581058
arguments[path.size() + 1] = pathPrefixStart;
10591059
arguments[path.size() + 2] = pathPrefixEnd;
10601060
String orderBy = PATH_COLUMN_NAME;
1061+
// TODO: We need to modify this to make sure that we limit based on trackedquerykeys
10611062

10621063
return database.query(SERVER_CACHE_TABLE, columns, whereClause, arguments, null, null, orderBy);
10631064
}

firebase-database/src/main/java/com/google/firebase/database/connection/PersistentConnectionImpl.java

-1
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,6 @@ public Task<Object> get(List<String> path, Map<String, Object> queryParams) {
419419
String status = (String) response.get(REQUEST_STATUS);
420420
if (status.equals("ok")) {
421421
Object body = response.get(SERVER_DATA_UPDATE_BODY);
422-
delegate.onDataUpdate(query.path, body, /*isMerge=*/ false, /*tagNumber=*/ null);
423422
source.setResult(body);
424423
} else {
425424
source.setException(new Exception((String) response.get(SERVER_DATA_UPDATE_BODY)));

firebase-database/src/main/java/com/google/firebase/database/core/Path.java

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ public Path(String pathString) {
7575
count++;
7676
}
7777
}
78+
// We are essentially doing the same work twice; once here, and once again in the constructor on
79+
// line 60
7880
pieces = new ChildKey[count];
7981
int j = 0;
8082
for (String segment : segments) {

firebase-database/src/main/java/com/google/firebase/database/core/Repo.java

+34-4
Original file line numberDiff line numberDiff line change
@@ -514,12 +514,13 @@ public void onRequestResult(String optErrorCode, String optErrorMessage) {
514514
*/
515515
public Task<DataSnapshot> getValue(Query query) {
516516
TaskCompletionSource<DataSnapshot> source = new TaskCompletionSource<>();
517+
final Repo repo = this;
517518
this.scheduleNow(
518519
new Runnable() {
519520
@Override
520521
public void run() {
521522
// Always check active-listener in-memory caches first. These are always at least as
522-
// up to date as the persistence cache.
523+
// up to date as the persistence cache
523524
Node serverValue = serverSyncTree.getServerValue(query.getSpec());
524525
if (serverValue != null) {
525526
source.setResult(
@@ -548,15 +549,44 @@ public void run() {
548549
source.setException(Objects.requireNonNull(task.getException()));
549550
}
550551
} else {
552+
/*
553+
* We need to replicate the behavior that occurs when running `once()`. In other words,
554+
* we need to create a new eventRegistration, register it with a view and then
555+
* overwrite the data at that location, and then remove the view.
556+
*/
551557
Node serverNode = NodeUtilities.NodeFromJSON(task.getResult());
552-
postEvents(
553-
serverSyncTree.applyServerOverwrite(query.getPath(), serverNode));
558+
QuerySpec spec = query.getSpec();
559+
// EventRegistrations require a listener to be attached, so a dummy
560+
// ValueEventListener was created.
561+
ValueEventListener listener =
562+
new ValueEventListener() {
563+
@Override
564+
public void onDataChange(@NonNull DataSnapshot snapshot) {
565+
// noOp
566+
}
567+
568+
@Override
569+
public void onCancelled(@NonNull DatabaseError error) {
570+
// noOp
571+
}
572+
};
573+
ValueEventRegistration eventRegistration =
574+
new ValueEventRegistration(repo, listener, spec);
575+
serverSyncTree.addEventRegistration(
576+
eventRegistration, /*skipListenerSetup=*/ true);
577+
if (spec.loadsAllData()) {
578+
serverSyncTree.applyServerOverwrite(spec.getPath(), serverNode);
579+
} else {
580+
serverSyncTree.applyTaggedQueryOverwrite(
581+
spec.getPath(), serverNode, getServerSyncTree().tagForQuery(spec));
582+
}
554583
source.setResult(
555584
InternalHelpers.createDataSnapshot(
556585
query.getRef(),
557586
IndexedNode.from(serverNode, query.getSpec().getIndex())));
587+
serverSyncTree.removeEventRegistration(
588+
eventRegistration, /*skipDedup=*/ true);
558589
}
559-
serverSyncTree.setQueryInactive(query.getSpec());
560590
});
561591
}
562592
});

firebase-database/src/main/java/com/google/firebase/database/core/SyncPoint.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,9 @@ public List<View> getQueryViews() {
235235

236236
public Node getCompleteServerCache(Path path) {
237237
for (View view : this.views.values()) {
238-
if (view.getCompleteServerCache(path) != null) {
239-
return view.getCompleteServerCache(path);
238+
Node serverCache = view.getCompleteServerCache(path);
239+
if (serverCache != null) {
240+
return serverCache;
240241
}
241242
}
242243
return null;

firebase-database/src/main/java/com/google/firebase/database/core/SyncTree.java

+39-6
Original file line numberDiff line numberDiff line change
@@ -538,9 +538,14 @@ public Node getServerValue(QuerySpec query) {
538538
});
539539
}
540540

541-
/** Add an event callback for the specified query. */
542541
public List<? extends Event> addEventRegistration(
543542
@NotNull final EventRegistration eventRegistration) {
543+
return addEventRegistration(eventRegistration, false);
544+
}
545+
546+
/** Add an event callback for the specified query. */
547+
public List<? extends Event> addEventRegistration(
548+
@NotNull final EventRegistration eventRegistration, final boolean skipListenerSetup) {
544549
return persistenceManager.runInTransaction(
545550
new Callable<List<? extends Event>>() {
546551
@Override
@@ -635,7 +640,7 @@ public List<? extends Event> call() {
635640
WriteTreeRef writesCache = pendingWriteTree.childWrites(path);
636641
List<? extends Event> events =
637642
syncPoint.addEventRegistration(eventRegistration, writesCache, serverCache);
638-
if (!viewAlreadyExists && !foundAncestorDefaultView) {
643+
if (!viewAlreadyExists && !foundAncestorDefaultView && !skipListenerSetup) {
639644
View view = syncPoint.viewForQuery(query);
640645
setupListener(query, view);
641646
}
@@ -650,7 +655,14 @@ public List<? extends Event> call() {
650655
* <p>If query is the default query, we'll check all queries for the specified eventRegistration.
651656
*/
652657
public List<Event> removeEventRegistration(@NotNull EventRegistration eventRegistration) {
653-
return this.removeEventRegistration(eventRegistration.getQuerySpec(), eventRegistration, null);
658+
return this.removeEventRegistration(
659+
eventRegistration.getQuerySpec(), eventRegistration, null, false);
660+
}
661+
662+
public List<Event> removeEventRegistration(
663+
@NotNull EventRegistration eventRegistration, boolean skipDedup) {
664+
return this.removeEventRegistration(
665+
eventRegistration.getQuerySpec(), eventRegistration, null, skipDedup);
654666
}
655667

656668
/**
@@ -660,13 +672,14 @@ public List<Event> removeEventRegistration(@NotNull EventRegistration eventRegis
660672
*/
661673
public List<Event> removeAllEventRegistrations(
662674
@NotNull QuerySpec query, @NotNull DatabaseError error) {
663-
return this.removeEventRegistration(query, null, error);
675+
return this.removeEventRegistration(query, null, error, false);
664676
}
665677

666678
private List<Event> removeEventRegistration(
667679
final @NotNull QuerySpec query,
668680
final @Nullable EventRegistration eventRegistration,
669-
final @Nullable DatabaseError cancelError) {
681+
final @Nullable DatabaseError cancelError,
682+
final boolean skipDedup) {
670683
return persistenceManager.runInTransaction(
671684
new Callable<List<Event>>() {
672685
@Override
@@ -688,6 +701,7 @@ public List<Event> call() {
688701
if (maybeSyncPoint.isEmpty()) {
689702
syncPointTree = syncPointTree.remove(path);
690703
}
704+
691705
List<QuerySpec> removed = removedAndEvents.getFirst();
692706
cancelEvents = removedAndEvents.getSecond();
693707
// We may have just removed one of many listeners and can short-circuit this whole
@@ -701,6 +715,25 @@ public List<Event> call() {
701715
persistenceManager.setQueryInactive(query);
702716
removingDefault = removingDefault || queryRemoved.loadsAllData();
703717
}
718+
719+
/**
720+
* This is to handle removeRegistration by {@link Repo#getValue(Query)}. Specifically
721+
* to avoid the scenario where: A listener is attached at a child path, and {@link
722+
* Repo#getValue(Query)} is called on the parent path. Normally, when a listener is
723+
* attached on a child path and then a parent path has a listener attached to it, to
724+
* reduce the number of listeners, the listen() function will unlisten to the child
725+
* path and listen instead on the parent path. And then, when removeRegistration is
726+
* called on the parent path, the child path will get listened to, since it doesn't
727+
* have anything covering its path. However, for {@link Repo#getValue(Query)}, we do
728+
* not call listen on the parent path, and the child path is still listened to and so
729+
* when the deduping happens below, the SyncTree assumes that the child listener has
730+
* been removed and attempts to call listen again, but since we are still listening on
731+
* that location, listen would be called twice on the same query. skipDedup allows us
732+
* to skip this deduping process altogether.
733+
*/
734+
if (skipDedup) {
735+
return null;
736+
}
704737
ImmutableTree<SyncPoint> currentTree = syncPointTree;
705738
boolean covered =
706739
currentTree.getValue() != null && currentTree.getValue().hasCompleteView();
@@ -915,7 +948,7 @@ private QuerySpec queryForTag(Tag tag) {
915948
}
916949

917950
/** Return the tag associated with the given query. */
918-
private Tag tagForQuery(QuerySpec query) {
951+
public Tag tagForQuery(QuerySpec query) {
919952
return this.queryToTagMap.get(query);
920953
}
921954

firebase-database/src/main/java/com/google/firebase/database/core/ZombieEventManager.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public void zombifyForRemove(EventRegistration registration) {
122122
if (registrationList != null && !registrationList.isEmpty()) {
123123
if (registration.getQuerySpec().isDefault()) {
124124
// The behavior here has to match the behavior of SyncPoint.removeEventRegistration.
125-
// If the query is default, it remove a single instance of the registration
125+
// If the query is default, it removes a single instance of the registration
126126
// from each unique query. So for example, if you had 3 copies registered under default,
127127
// you would end up with 2 still registered.
128128
// If you had 1 registration in default and 2 in query a', you'd end up with just

0 commit comments

Comments
 (0)