Skip to content

Commit ecda2ba

Browse files
committed
Merge remote-tracking branch 'origin/master' into mila/BloomFilter
2 parents 36a1643 + 3d4cfed commit ecda2ba

File tree

5 files changed

+269
-271
lines changed

5 files changed

+269
-271
lines changed

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import com.google.android.gms.tasks.Task;
3838
import com.google.common.collect.Lists;
3939
import com.google.firebase.firestore.Query.Direction;
40-
import com.google.firebase.firestore.remote.WatchChangeAggregatorTestingHooksAccessor;
40+
import com.google.firebase.firestore.remote.ExistenceFilterMismatchListener;
4141
import com.google.firebase.firestore.testutil.EventAccumulator;
4242
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
4343
import java.util.ArrayList;
@@ -1060,6 +1060,7 @@ public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Except
10601060
createdDocuments.add(documentSnapshot.getReference());
10611061
}
10621062
}
1063+
assertWithMessage("createdDocuments").that(createdDocuments).hasSize(100);
10631064

10641065
// Delete 50 of the 100 documents. Do this in a transaction, rather than
10651066
// DocumentReference.delete(), to avoid affecting the local cache.
@@ -1076,6 +1077,7 @@ public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Except
10761077
}
10771078
return null;
10781079
}));
1080+
assertWithMessage("deletedDocumentIds").that(deletedDocumentIds).hasSize(50);
10791081

10801082
// Wait for 10 seconds, during which Watch will stop tracking the query and will send an
10811083
// existence filter rather than "delete" events when the query is resumed.
@@ -1084,26 +1086,24 @@ public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Except
10841086
// Resume the query and save the resulting snapshot for verification. Use some internal
10851087
// testing hooks to "capture" the existence filter mismatches to verify that Watch sent a
10861088
// bloom filter, and it was used to avert a full requery.
1089+
ExistenceFilterMismatchListener existenceFilterMismatchListener =
1090+
new ExistenceFilterMismatchListener();
10871091
QuerySnapshot snapshot2;
1088-
WatchChangeAggregatorTestingHooksAccessor.ExistenceFilterMismatchInfo
1089-
existenceFilterMismatchInfo;
1090-
WatchChangeAggregatorTestingHooksAccessor.ExistenceFilterMismatchAccumulator
1091-
existenceFilterMismatchAccumulator =
1092-
new WatchChangeAggregatorTestingHooksAccessor.ExistenceFilterMismatchAccumulator();
1093-
existenceFilterMismatchAccumulator.register();
1092+
ExistenceFilterMismatchListener.ExistenceFilterMismatchInfo existenceFilterMismatchInfo;
10941093
try {
1094+
existenceFilterMismatchListener.startListening();
10951095
snapshot2 = waitFor(collection.get());
10961096
// TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed
10971097
// to send an existence filter.
10981098
if (isRunningAgainstEmulator()) {
10991099
existenceFilterMismatchInfo = null;
11001100
} else {
11011101
existenceFilterMismatchInfo =
1102-
existenceFilterMismatchAccumulator.waitForExistenceFilterMismatch(
1102+
existenceFilterMismatchListener.getOrWaitForExistenceFilterMismatch(
11031103
/*timeoutMillis=*/ 5000);
11041104
}
11051105
} finally {
1106-
existenceFilterMismatchAccumulator.unregister();
1106+
existenceFilterMismatchListener.stopListening();
11071107
}
11081108

11091109
// Verify that the snapshot from the resumed query contains the expected documents; that is,
@@ -1157,7 +1157,7 @@ public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Except
11571157
}
11581158

11591159
// Verify that Watch sent a valid bloom filter.
1160-
WatchChangeAggregatorTestingHooksAccessor.ExistenceFilterBloomFilterInfo bloomFilter =
1160+
ExistenceFilterMismatchListener.ExistenceFilterBloomFilterInfo bloomFilter =
11611161
existenceFilterMismatchInfo.bloomFilter();
11621162
assertWithMessage("The bloom filter specified in the existence filter")
11631163
.that(bloomFilter)
@@ -1179,6 +1179,9 @@ public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Except
11791179
assertWithMessage("bloom filter successfully applied with attemptNumber=" + attemptNumber)
11801180
.that(bloomFilter.applied())
11811181
.isTrue();
1182+
1183+
// Break out of the test loop now that the test passes.
1184+
break;
11821185
}
11831186
}
11841187

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.remote;
16+
17+
import android.os.SystemClock;
18+
import androidx.annotation.NonNull;
19+
import androidx.annotation.Nullable;
20+
import com.google.firebase.firestore.ListenerRegistration;
21+
import java.util.ArrayList;
22+
23+
/**
24+
* Provides a mechanism for tests to listen for existence filter mismatches in the Watch "listen"
25+
* stream.
26+
*/
27+
public final class ExistenceFilterMismatchListener {
28+
29+
private TestingHooksExistenceFilterMismatchListenerImpl listener;
30+
private ListenerRegistration listenerRegistration;
31+
32+
/**
33+
* Starts listening for existence filter mismatches.
34+
*
35+
* @throws IllegalStateException if this object is already started.
36+
* @see #stopListening
37+
*/
38+
public synchronized void startListening() {
39+
if (listener != null) {
40+
throw new IllegalStateException("already registered");
41+
}
42+
listener = new TestingHooksExistenceFilterMismatchListenerImpl();
43+
listenerRegistration = TestingHooks.getInstance().addExistenceFilterMismatchListener(listener);
44+
}
45+
46+
/**
47+
* Stops listening for existence filter mismatches.
48+
*
49+
* <p>If listening has not been started then this method does nothing.
50+
*
51+
* @see #startListening
52+
*/
53+
public synchronized void stopListening() {
54+
if (listenerRegistration != null) {
55+
listenerRegistration.remove();
56+
}
57+
listenerRegistration = null;
58+
listener = null;
59+
}
60+
61+
/**
62+
* Returns the oldest existence filter mismatch observed, waiting if none has yet been observed.
63+
*
64+
* <p>The oldest existence filter mismatch observed since the most recent successful invocation of
65+
* {@link #startListening} will be returned. A subsequent invocation of this method will return
66+
* the second-oldest existence filter mismatch observed, and so on. An invocation of {@link
67+
* #stopListening} followed by another invocation of {@link #startListening} will discard any
68+
* existence filter mismatches that occurred while previously started and will start observing
69+
* afresh.
70+
*
71+
* @param timeoutMillis the maximum amount of time, in milliseconds, to wait for an existence
72+
* filter mismatch to occur.
73+
* @return information about the existence filter mismatch that occurred.
74+
* @throws InterruptedException if waiting is interrupted.
75+
* @throws IllegalStateException if this object has not been started by {@link #startListening}.
76+
* @throws IllegalArgumentException if the given timeout is less than or equal to zero.
77+
*/
78+
@Nullable
79+
public ExistenceFilterMismatchInfo getOrWaitForExistenceFilterMismatch(long timeoutMillis)
80+
throws InterruptedException {
81+
if (timeoutMillis <= 0) {
82+
throw new IllegalArgumentException("invalid timeout: " + timeoutMillis);
83+
}
84+
85+
TestingHooksExistenceFilterMismatchListenerImpl registeredListener;
86+
synchronized (this) {
87+
registeredListener = listener;
88+
}
89+
90+
if (registeredListener == null) {
91+
throw new IllegalStateException(
92+
"must be registered before waiting for an existence filter mismatch");
93+
}
94+
95+
return registeredListener.getOrWaitForExistenceFilterMismatch(timeoutMillis);
96+
}
97+
98+
private static final class TestingHooksExistenceFilterMismatchListenerImpl
99+
implements TestingHooks.ExistenceFilterMismatchListener {
100+
101+
private final ArrayList<ExistenceFilterMismatchInfo> existenceFilterMismatches =
102+
new ArrayList<>();
103+
104+
@Override
105+
public synchronized void onExistenceFilterMismatch(
106+
@NonNull TestingHooks.ExistenceFilterMismatchInfo info) {
107+
existenceFilterMismatches.add(new ExistenceFilterMismatchInfo(info));
108+
notifyAll();
109+
}
110+
111+
@Nullable
112+
synchronized ExistenceFilterMismatchInfo getOrWaitForExistenceFilterMismatch(long timeoutMillis)
113+
throws InterruptedException {
114+
if (timeoutMillis <= 0) {
115+
throw new IllegalArgumentException("invalid timeout: " + timeoutMillis);
116+
}
117+
118+
long endTimeMillis = SystemClock.uptimeMillis() + timeoutMillis;
119+
while (true) {
120+
if (existenceFilterMismatches.size() > 0) {
121+
return existenceFilterMismatches.remove(0);
122+
}
123+
long currentWaitMillis = endTimeMillis - SystemClock.uptimeMillis();
124+
if (currentWaitMillis <= 0) {
125+
return null;
126+
}
127+
128+
wait(currentWaitMillis);
129+
}
130+
}
131+
}
132+
133+
/** @see TestingHooks.ExistenceFilterMismatchInfo */
134+
public static final class ExistenceFilterMismatchInfo {
135+
136+
private final TestingHooks.ExistenceFilterMismatchInfo info;
137+
138+
ExistenceFilterMismatchInfo(@NonNull TestingHooks.ExistenceFilterMismatchInfo info) {
139+
this.info = info;
140+
}
141+
142+
public int localCacheCount() {
143+
return info.localCacheCount();
144+
}
145+
146+
public int existenceFilterCount() {
147+
return info.existenceFilterCount();
148+
}
149+
150+
@Nullable
151+
public ExistenceFilterBloomFilterInfo bloomFilter() {
152+
TestingHooks.ExistenceFilterBloomFilterInfo bloomFilterInfo = info.bloomFilter();
153+
return bloomFilterInfo == null ? null : new ExistenceFilterBloomFilterInfo(bloomFilterInfo);
154+
}
155+
}
156+
157+
/** @see TestingHooks.ExistenceFilterBloomFilterInfo */
158+
public static final class ExistenceFilterBloomFilterInfo {
159+
160+
private final TestingHooks.ExistenceFilterBloomFilterInfo info;
161+
162+
ExistenceFilterBloomFilterInfo(@NonNull TestingHooks.ExistenceFilterBloomFilterInfo info) {
163+
this.info = info;
164+
}
165+
166+
public boolean applied() {
167+
return info.applied();
168+
}
169+
170+
public int hashCount() {
171+
return info.hashCount();
172+
}
173+
174+
public int bitmapLength() {
175+
return info.bitmapLength();
176+
}
177+
178+
public int padding() {
179+
return info.padding();
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)