@@ -2067,73 +2067,98 @@ apiDescribe('Queries', (persistence: boolean) => {
2067
2067
// query is resumed is fixed.
2068
2068
( USE_EMULATOR || ! persistence ? it . skip : it . only ) (
2069
2069
'resuming a query should use bloom filter to avoid full requery' ,
2070
- ( ) => {
2071
- // TODO(dconeybe) Add a loop to retry 2 or 3 times if the bloom filter
2072
- // experienced a false positive. Since there is a non-zero chance of a
2073
- // false positive, and the chance of experiencing two in a row is
2074
- // negligible, retrying 2 or 3 times should result in basically zero
2075
- // false positives.
2076
-
2070
+ async ( ) => {
2077
2071
// Create 100 documents in a new collection.
2078
2072
const testDocs : { [ key : string ] : object } = { } ;
2079
2073
for ( let i = 1 ; i <= 100 ; i ++ ) {
2080
2074
testDocs [ 'doc' + i ] = { key : i } ;
2081
2075
}
2082
2076
2083
- return withTestCollection ( persistence , testDocs , async ( coll , db ) => {
2084
- // Run a query to populate the local cache with the 100 documents and a
2085
- // resume token.
2086
- const snapshot1 = await getDocs ( coll ) ;
2087
- expect ( snapshot1 . size ) . to . equal ( 100 ) ;
2088
-
2089
- // Delete 50 of the 100 documents. Do this in a transaction, rather than
2090
- // deleteDoc(), to avoid affecting the local cache.
2091
- await runTransaction ( db , async txn => {
2092
- for ( let i = 1 ; i <= 50 ; i ++ ) {
2093
- txn . delete ( doc ( coll , 'doc' + i ) ) ;
2077
+ let attemptNumber = 0 ;
2078
+ while ( true ) {
2079
+ attemptNumber ++ ;
2080
+ const bloomFilterApplied = await withTestCollection (
2081
+ persistence ,
2082
+ testDocs ,
2083
+ async ( coll , db ) => {
2084
+ // Run a query to populate the local cache with the 100 documents and
2085
+ // a resume token.
2086
+ const snapshot1 = await getDocs ( coll ) ;
2087
+ expect ( snapshot1 . size ) . to . equal ( 100 ) ;
2088
+
2089
+ // Delete 50 of the 100 documents. Do this in a transaction, rather
2090
+ // than deleteDoc(), to avoid affecting the local cache.
2091
+ await runTransaction ( db , async txn => {
2092
+ for ( let i = 1 ; i <= 50 ; i ++ ) {
2093
+ txn . delete ( doc ( coll , 'doc' + i ) ) ;
2094
+ }
2095
+ } ) ;
2096
+
2097
+ // Wait for 10 seconds, during which Watch will stop tracking the
2098
+ // query and will send an existence filter rather than "delete" events
2099
+ // when the query is resumed.
2100
+ await new Promise ( resolve => setTimeout ( resolve , 10000 ) ) ;
2101
+
2102
+ // Resume the query and expect to get a snapshot with the 50 remaining
2103
+ // documents. Use some internal testing hooks to "capture" the
2104
+ // existence filter mismatches to later verify that Watch sent a bloom
2105
+ // filter, and it was used to void the full requery.
2106
+ const existenceFilterMismatches =
2107
+ await captureExistenceFilterMismatches ( async ( ) => {
2108
+ const snapshot2 = await getDocs ( coll ) ;
2109
+ expect ( snapshot2 . size ) . to . equal ( 50 ) ;
2110
+ } ) ;
2111
+
2112
+ // Verify that upon resuming the query that Watch sent an existence
2113
+ // filter that included a bloom filter, and that that bloom filter was
2114
+ // successfully used to avoid a full requery.
2115
+ // TODO(b/NNNNNNNN) Replace this "if" condition with !USE_EMULATOR
2116
+ // once the feature has been deployed to production. Note that there
2117
+ // are no plans to implement the bloom filter in the existence filter
2118
+ // responses sent from the Firestore emulator.
2119
+ if ( TARGET_BACKEND === 'nightly' ) {
2120
+ expect ( existenceFilterMismatches ) . to . have . length ( 1 ) ;
2121
+ const existenceFilterMismatch = existenceFilterMismatches [ 0 ] ;
2122
+ expect ( existenceFilterMismatch . actualCount ) . to . equal ( 100 ) ;
2123
+ expect ( existenceFilterMismatch . expectedCount ) . to . equal ( 50 ) ;
2124
+ const bloomFilter = existenceFilterMismatch . bloomFilter ;
2125
+ if ( ! bloomFilter ) {
2126
+ expect . fail ( 'existence filter should have had a bloom filter' ) ;
2127
+ throw new Error ( 'should never get here' ) ;
2128
+ }
2129
+
2130
+ // Although statistically rare, it _is_ possible to get a legitimate
2131
+ // false positive when checking the bloom filter. If this happens,
2132
+ // then retry the test with a different set of documents, and only
2133
+ // fail if the retry _also_ experiences a false positive.
2134
+ if ( ! bloomFilter . applied ) {
2135
+ if ( attemptNumber < 2 ) {
2136
+ return false ;
2137
+ } else {
2138
+ expect . fail (
2139
+ 'bloom filter false positive occurred ' +
2140
+ 'multiple times; this is statistically ' +
2141
+ 'improbable and should be investigated.'
2142
+ ) ;
2143
+ }
2144
+ }
2145
+
2146
+ expect ( bloomFilter . bitmapLength ) . to . be . above ( 0 ) ;
2147
+ expect ( bloomFilter . hashCount ) . to . be . above ( 0 ) ;
2148
+ expect ( bloomFilter . padding ) . to . be . above ( 0 ) ;
2149
+ expect ( bloomFilter . padding ) . to . be . below ( 8 ) ;
2150
+ }
2151
+
2152
+ return true ;
2094
2153
}
2095
- } ) ;
2154
+ ) ;
2096
2155
2097
- // Wait for 10 seconds, during which Watch will stop tracking the query
2098
- // and will send an existence filter rather than "delete" events when
2099
- // the query is resumed.
2100
- await new Promise ( resolve => setTimeout ( resolve , 10000 ) ) ;
2101
-
2102
- // Resume the query and expect to get a snapshot with the 50 remaining
2103
- // documents. Use some internal testing hooks to "capture" the existence
2104
- // filter mismatches to later verify that Watch sent a bloom filter and
2105
- // it was used to void the full requery.
2106
- const existenceFilterMismatches =
2107
- await captureExistenceFilterMismatches ( async ( ) => {
2108
- const snapshot2 = await getDocs ( coll ) ;
2109
- expect ( snapshot2 . size ) . to . equal ( 50 ) ;
2110
- } ) ;
2111
-
2112
- // Verify that upon resuming the query that Watch sent an existence
2113
- // filter that included a bloom filter, and that that bloom filter was
2114
- // successfully used to avoid a full requery.
2115
- // TODO(b/NNNNNNNN) Replace this "if" condition with !USE_EMULATOR once
2116
- // the feature has been deployed to production. Note that there are no
2117
- // plans to add bloom filter support to the Firestore emulator.
2118
- if ( TARGET_BACKEND === 'nightly' ) {
2119
- expect ( existenceFilterMismatches ) . to . have . length ( 1 ) ;
2120
- const existenceFilterMismatch = existenceFilterMismatches [ 0 ] ;
2121
- expect ( existenceFilterMismatch . actualCount ) . to . equal ( 100 ) ;
2122
- expect ( existenceFilterMismatch . expectedCount ) . to . equal ( 50 ) ;
2123
- const bloomFilter = existenceFilterMismatch . bloomFilter ;
2124
- if ( ! bloomFilter ) {
2125
- expect . fail ( 'existence filter should have had a bloom filter' ) ;
2126
- throw new Error ( 'should never get here' ) ;
2127
- }
2128
- expect ( bloomFilter . applied ) . to . equal ( true ) ;
2129
- expect ( bloomFilter . bitmapLength ) . to . be . above ( 0 ) ;
2130
- expect ( bloomFilter . hashCount ) . to . be . above ( 0 ) ;
2131
- expect ( bloomFilter . padding ) . to . be . above ( 0 ) ;
2132
- expect ( bloomFilter . padding ) . to . be . below ( 8 ) ;
2156
+ if ( bloomFilterApplied ) {
2157
+ break ;
2133
2158
}
2134
- } ) ;
2159
+ }
2135
2160
}
2136
- ) . timeout ( '30s ' ) ;
2161
+ ) . timeout ( '90s ' ) ;
2137
2162
} ) ;
2138
2163
2139
2164
function verifyDocumentChange < T > (
0 commit comments