Skip to content

Commit ded3ee1

Browse files
authored
Merge 1f34778 into 2139f5e
2 parents 2139f5e + 1f34778 commit ded3ee1

File tree

13 files changed

+6798
-784
lines changed

13 files changed

+6798
-784
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/BloomFilter.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,19 @@ public BloomFilter(@NonNull byte[] bitmap, int padding, int hashCount) {
3232
throw new NullPointerException("Bitmap cannot be null.");
3333
}
3434
if (padding < 0 || padding >= 8) {
35-
throw new IllegalArgumentException("Invalid padding: " + padding);
35+
throw new BloomFilterException("Invalid padding: " + padding);
3636
}
3737
if (hashCount < 0) {
38-
throw new IllegalArgumentException("Invalid hash count: " + hashCount);
38+
throw new BloomFilterException("Invalid hash count: " + hashCount);
3939
}
4040
if (bitmap.length > 0 && hashCount == 0) {
4141
// Only empty bloom filter can have 0 hash count.
42-
throw new IllegalArgumentException("Invalid hash count: " + hashCount);
42+
throw new BloomFilterException("Invalid hash count: " + hashCount);
4343
}
44-
if (bitmap.length == 0) {
44+
if (bitmap.length == 0 && padding != 0) {
4545
// Empty bloom filter should have 0 padding.
46-
if (padding != 0) {
47-
throw new IllegalArgumentException(
48-
"Expected padding of 0 when bitmap length is 0, but got " + padding);
49-
}
46+
throw new BloomFilterException(
47+
"Expected padding of 0 when bitmap length is 0, but got " + padding);
5048
}
5149

5250
this.bitmap = bitmap;
@@ -133,8 +131,8 @@ private int getBitIndex(long hash1, long hash2, int hashIndex) {
133131
/**
134132
* Calculate modulo, where the dividend and divisor are treated as unsigned 64-bit longs.
135133
*
136-
* <p>The implementation is taken from <a
137-
* href="https://github.com/google/guava/blob/553037486901cc60820ab7dcb38a25b6f34eba43/android/guava/src/com/google/common/primitives/UnsignedLongs.java">Guava</a>,
134+
* <p>The implementation is taken from <a href=
135+
* "https://github.com/google/guava/blob/553037486901cc60820ab7dcb38a25b6f34eba43/android/guava/src/com/google/common/primitives/UnsignedLongs.java">Guava</a>,
138136
* simplified to our needs.
139137
*
140138
* <p>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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 androidx.annotation.NonNull;
18+
19+
public class BloomFilterException extends RuntimeException {
20+
public BloomFilterException(@NonNull String detailMessage) {
21+
super(detailMessage);
22+
}
23+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/ExistenceFilter.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,34 @@
1414

1515
package com.google.firebase.firestore.remote;
1616

17+
import androidx.annotation.Nullable;
18+
import com.google.firestore.v1.BloomFilter;
19+
1720
/** Simplest form of existence filter */
1821
public final class ExistenceFilter {
1922
private final int count;
23+
private BloomFilter unchangedNames;
2024

2125
public ExistenceFilter(int count) {
2226
this.count = count;
2327
}
2428

29+
public ExistenceFilter(int count, @Nullable BloomFilter unchangedNames) {
30+
this.count = count;
31+
this.unchangedNames = unchangedNames;
32+
}
33+
2534
public int getCount() {
2635
return count;
2736
}
2837

38+
@Nullable
39+
public BloomFilter getUnchangedNames() {
40+
return unchangedNames;
41+
}
42+
2943
@Override
3044
public String toString() {
31-
return "ExistenceFilter{count=" + count + '}';
45+
return "ExistenceFilter{count=" + count + ", unchangedNames=" + unchangedNames + '}';
3246
}
3347
}

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -945,8 +945,8 @@ public WatchChange decodeWatchChange(ListenResponse protoChange) {
945945
break;
946946
case FILTER:
947947
com.google.firestore.v1.ExistenceFilter protoFilter = protoChange.getFilter();
948-
// TODO: implement existence filter parsing (see b/33076578)
949-
ExistenceFilter filter = new ExistenceFilter(protoFilter.getCount());
948+
ExistenceFilter filter =
949+
new ExistenceFilter(protoFilter.getCount(), protoFilter.getUnchangedNames());
950950
int targetId = protoFilter.getTargetId();
951951
watchChange = new ExistenceFilterWatchChange(targetId, filter);
952952
break;

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.firebase.firestore.local.LocalStore;
2929
import com.google.firebase.firestore.local.QueryPurpose;
3030
import com.google.firebase.firestore.local.TargetData;
31+
import com.google.firebase.firestore.model.DatabaseId;
3132
import com.google.firebase.firestore.model.DocumentKey;
3233
import com.google.firebase.firestore.model.SnapshotVersion;
3334
import com.google.firebase.firestore.model.mutation.MutationBatch;
@@ -758,6 +759,11 @@ public TargetData getTargetDataForTarget(int targetId) {
758759
return this.listenTargets.get(targetId);
759760
}
760761

762+
@Override
763+
public DatabaseId getDatabaseId() {
764+
return this.datastore.getDatabaseInfo().getDatabaseId();
765+
}
766+
761767
public Task<Long> runCountQuery(Query query) {
762768
if (canUseNetwork()) {
763769
return datastore.runCountQuery(query);

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
import com.google.firebase.firestore.core.Target;
2424
import com.google.firebase.firestore.local.QueryPurpose;
2525
import com.google.firebase.firestore.local.TargetData;
26+
import com.google.firebase.firestore.model.DatabaseId;
2627
import com.google.firebase.firestore.model.DocumentKey;
2728
import com.google.firebase.firestore.model.MutableDocument;
2829
import com.google.firebase.firestore.model.SnapshotVersion;
2930
import com.google.firebase.firestore.remote.WatchChange.DocumentChange;
3031
import com.google.firebase.firestore.remote.WatchChange.ExistenceFilterWatchChange;
3132
import com.google.firebase.firestore.remote.WatchChange.WatchTargetChange;
33+
import com.google.firebase.firestore.util.Logger;
3234
import java.util.ArrayList;
3335
import java.util.Collection;
3436
import java.util.Collections;
@@ -56,6 +58,9 @@ public interface TargetMetadataProvider {
5658
*/
5759
@Nullable
5860
TargetData getTargetDataForTarget(int targetId);
61+
62+
/** Returns the database ID of the Firestore instance. */
63+
DatabaseId getDatabaseId();
5964
}
6065

6166
private final TargetMetadataProvider targetMetadataProvider;
@@ -75,6 +80,9 @@ public interface TargetMetadataProvider {
7580
*/
7681
private Set<Integer> pendingTargetResets = new HashSet<>();
7782

83+
/** The log tag to use for this class. */
84+
private static final String LOG_TAG = "WatchChangeAggregator";
85+
7886
public WatchChangeAggregator(TargetMetadataProvider targetMetadataProvider) {
7987
this.targetMetadataProvider = targetMetadataProvider;
8088
}
@@ -196,17 +204,78 @@ public void handleExistenceFilter(ExistenceFilterWatchChange watchChange) {
196204
expectedCount == 1, "Single document existence filter with count: %d", expectedCount);
197205
}
198206
} else {
199-
long currentSize = getCurrentDocumentCountForTarget(targetId);
207+
int currentSize = getCurrentDocumentCountForTarget(targetId);
200208
if (currentSize != expectedCount) {
201-
// Existence filter mismatch: We reset the mapping and raise a new snapshot with
202-
// `isFromCache:true`.
203-
resetTarget(targetId);
204-
pendingTargetResets.add(targetId);
209+
210+
// Apply bloom filter to identify and mark removed documents.
211+
boolean bloomFilterApplied = this.applyBloomFilter(watchChange, currentSize);
212+
213+
if (!bloomFilterApplied) {
214+
// If bloom filter application fails, we reset the mapping and
215+
// trigger re-run of the query.
216+
resetTarget(targetId);
217+
pendingTargetResets.add(targetId);
218+
}
205219
}
206220
}
207221
}
208222
}
209223

224+
/** Returns whether a bloom filter removed the deleted documents successfully. */
225+
private boolean applyBloomFilter(ExistenceFilterWatchChange watchChange, int currentCount) {
226+
int expectedCount = watchChange.getExistenceFilter().getCount();
227+
com.google.firestore.v1.BloomFilter unchangedNames =
228+
watchChange.getExistenceFilter().getUnchangedNames();
229+
230+
if (unchangedNames == null || !unchangedNames.hasBits()) {
231+
return false;
232+
}
233+
234+
byte[] bitmap = unchangedNames.getBits().getBitmap().toByteArray();
235+
BloomFilter bloomFilter;
236+
237+
try {
238+
bloomFilter =
239+
new BloomFilter(
240+
bitmap, unchangedNames.getBits().getPadding(), unchangedNames.getHashCount());
241+
} catch (BloomFilterException e) {
242+
Logger.warn(
243+
LOG_TAG,
244+
"Decoding the base64 bloom filter in existence filter failed ("
245+
+ e.getMessage()
246+
+ "); ignoring the bloom filter and falling back to full re-query.");
247+
return false;
248+
}
249+
250+
int removedDocumentCount = this.filterRemovedDocuments(bloomFilter, watchChange.getTargetId());
251+
252+
return expectedCount == (currentCount - removedDocumentCount);
253+
}
254+
255+
/**
256+
* Filter out removed documents based on bloom filter membership result and return number of
257+
* documents removed.
258+
*/
259+
private int filterRemovedDocuments(BloomFilter bloomFilter, int targetId) {
260+
ImmutableSortedSet<DocumentKey> existingKeys =
261+
targetMetadataProvider.getRemoteKeysForTarget(targetId);
262+
int removalCount = 0;
263+
for (DocumentKey key : existingKeys) {
264+
DatabaseId databaseId = targetMetadataProvider.getDatabaseId();
265+
String documentPath =
266+
"projects/"
267+
+ databaseId.getProjectId()
268+
+ "/databases/"
269+
+ databaseId.getDatabaseId()
270+
+ "/documents/"
271+
+ key.getPath().canonicalString();
272+
if (!bloomFilter.mightContain(documentPath)) {
273+
this.removeDocumentFromTarget(targetId, key, /*updatedDocument=*/ null);
274+
removalCount++;
275+
}
276+
}
277+
return removalCount;
278+
}
210279
/**
211280
* Converts the currently accumulated state into a remote event at the provided snapshot version.
212281
* Resets the accumulated changes before returning.

firebase-firestore/src/test/java/com/google/firebase/firestore/remote/BloomFilterTest.java

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -73,55 +73,53 @@ public void constructorShouldThrowNPEOnNullBitmap() {
7373
}
7474

7575
@Test
76-
public void constructorShouldThrowIAEOnEmptyBloomFilterWithNonZeroPadding() {
77-
IllegalArgumentException exception =
78-
assertThrows(IllegalArgumentException.class, () -> new BloomFilter(new byte[0], 1, 0));
76+
public void constructorShouldThrowBFEOnEmptyBloomFilterWithNonZeroPadding() {
77+
BloomFilterException exception =
78+
assertThrows(BloomFilterException.class, () -> new BloomFilter(new byte[0], 1, 0));
7979
assertThat(exception)
8080
.hasMessageThat()
8181
.contains("Expected padding of 0 when bitmap length is 0, but got 1");
8282
}
8383

8484
@Test
85-
public void constructorShouldThrowIAEOnNonEmptyBloomFilterWithZeroHashCount() {
86-
IllegalArgumentException zeroHashCountException =
87-
assertThrows(IllegalArgumentException.class, () -> new BloomFilter(new byte[] {1}, 1, 0));
85+
public void constructorShouldThrowBFEOnNonEmptyBloomFilterWithZeroHashCount() {
86+
BloomFilterException zeroHashCountException =
87+
assertThrows(BloomFilterException.class, () -> new BloomFilter(new byte[] {1}, 1, 0));
8888
assertThat(zeroHashCountException).hasMessageThat().contains("Invalid hash count: 0");
8989
}
9090

9191
@Test
92-
public void constructorShouldThrowIAEOnNegativePadding() {
92+
public void constructorShouldThrowBFEOnNegativePadding() {
9393
{
94-
IllegalArgumentException emptyBloomFilterException =
95-
assertThrows(IllegalArgumentException.class, () -> new BloomFilter(new byte[0], -1, 0));
94+
BloomFilterException emptyBloomFilterException =
95+
assertThrows(BloomFilterException.class, () -> new BloomFilter(new byte[0], -1, 0));
9696
assertThat(emptyBloomFilterException).hasMessageThat().contains("Invalid padding: -1");
9797
}
9898
{
99-
IllegalArgumentException nonEmptyBloomFilterException =
100-
assertThrows(
101-
IllegalArgumentException.class, () -> new BloomFilter(new byte[] {1}, -1, 1));
99+
BloomFilterException nonEmptyBloomFilterException =
100+
assertThrows(BloomFilterException.class, () -> new BloomFilter(new byte[] {1}, -1, 1));
102101
assertThat(nonEmptyBloomFilterException).hasMessageThat().contains("Invalid padding: -1");
103102
}
104103
}
105104

106105
@Test
107-
public void constructorShouldThrowIAEOnNegativeHashCount() {
106+
public void constructorShouldThrowBFEOnNegativeHashCount() {
108107
{
109-
IllegalArgumentException emptyBloomFilterException =
110-
assertThrows(IllegalArgumentException.class, () -> new BloomFilter(new byte[0], 0, -1));
108+
BloomFilterException emptyBloomFilterException =
109+
assertThrows(BloomFilterException.class, () -> new BloomFilter(new byte[0], 0, -1));
111110
assertThat(emptyBloomFilterException).hasMessageThat().contains("Invalid hash count: -1");
112111
}
113112
{
114-
IllegalArgumentException nonEmptyBloomFilterException =
115-
assertThrows(
116-
IllegalArgumentException.class, () -> new BloomFilter(new byte[] {1}, 1, -1));
113+
BloomFilterException nonEmptyBloomFilterException =
114+
assertThrows(BloomFilterException.class, () -> new BloomFilter(new byte[] {1}, 1, -1));
117115
assertThat(nonEmptyBloomFilterException).hasMessageThat().contains("Invalid hash count: -1");
118116
}
119117
}
120118

121119
@Test
122-
public void constructorShouldThrowIAEIfPaddingIsTooLarge() {
123-
IllegalArgumentException exception =
124-
assertThrows(IllegalArgumentException.class, () -> new BloomFilter(new byte[] {1}, 8, 1));
120+
public void constructorShouldThrowBFEIfPaddingIsTooLarge() {
121+
BloomFilterException exception =
122+
assertThrows(BloomFilterException.class, () -> new BloomFilter(new byte[] {1}, 8, 1));
125123
assertThat(exception).hasMessageThat().contains("Invalid padding: 8");
126124
}
127125

@@ -179,7 +177,7 @@ public void bloomFilterToString() {
179177
private static void runGoldenTest(String testFile) throws Exception {
180178
String resultFile = testFile.replace("bloom_filter_proto", "membership_test_result");
181179
if (resultFile.equals(testFile)) {
182-
throw new IllegalArgumentException("Cannot find corresponding result file for " + testFile);
180+
throw new BloomFilterException("Cannot find corresponding result file for " + testFile);
183181
}
184182

185183
JSONObject testJson = readJsonFile(testFile);

0 commit comments

Comments
 (0)