@@ -2065,115 +2065,137 @@ apiDescribe('Queries', (persistence: boolean) => {
2065
2065
} ) ;
2066
2066
2067
2067
it ( 'resuming a query should use bloom filter to avoid full requery' , async ( ) => {
2068
- // Create 100 documents in a new collection .
2068
+ // Prepare the names and contents of the 100 documents to create .
2069
2069
const testDocs : { [ key : string ] : object } = { } ;
2070
- for ( let i = 1 ; i <= 100 ; i ++ ) {
2071
- testDocs [ 'doc' + i ] = { key : i } ;
2070
+ for ( let i = 0 ; i < 100 ; i ++ ) {
2071
+ testDocs [ 'doc' + ( 1000 + i ) ] = { key : 42 } ;
2072
2072
}
2073
2073
2074
2074
// The function that runs a single iteration of the test.
2075
- // Below this definition, there is a "while" loop that calls this
2076
- // function potentially multiple times.
2075
+ // Below this definition, there is a "while" loop that calls this function
2076
+ // potentially multiple times.
2077
2077
const runTestIteration = async (
2078
2078
coll : CollectionReference ,
2079
2079
db : Firestore
2080
2080
) : Promise < 'retry' | 'passed' > => {
2081
- // Run a query to populate the local cache with the 100 documents
2082
- // and a resume token.
2081
+ // Run a query to populate the local cache with the 100 documents and a
2082
+ // resume token.
2083
2083
const snapshot1 = await getDocs ( coll ) ;
2084
2084
expect ( snapshot1 . size , 'snapshot1.size' ) . to . equal ( 100 ) ;
2085
+ const createdDocuments = snapshot1 . docs . map ( snapshot => snapshot . ref ) ;
2085
2086
2086
- // Delete 50 of the 100 documents. Do this in a transaction, rather
2087
- // than deleteDoc(), to avoid affecting the local cache.
2087
+ // Delete 50 of the 100 documents. Do this in a transaction, rather than
2088
+ // deleteDoc(), to avoid affecting the local cache.
2089
+ const deletedDocumentIds = new Set < string > ( ) ;
2088
2090
await runTransaction ( db , async txn => {
2089
- for ( let i = 1 ; i <= 50 ; i ++ ) {
2090
- txn . delete ( doc ( coll , 'doc' + i ) ) ;
2091
+ for ( let i = 0 ; i < createdDocuments . length ; i += 2 ) {
2092
+ const documentToDelete = createdDocuments [ i ] ;
2093
+ txn . delete ( documentToDelete ) ;
2094
+ deletedDocumentIds . add ( documentToDelete . id ) ;
2091
2095
}
2092
2096
} ) ;
2093
2097
2094
- // Wait for 10 seconds, during which Watch will stop tracking the
2095
- // query and will send an existence filter rather than "delete"
2096
- // events when the query is resumed.
2098
+ // Wait for 10 seconds, during which Watch will stop tracking the query
2099
+ // and will send an existence filter rather than "delete" events when the
2100
+ // query is resumed.
2097
2101
await new Promise ( resolve => setTimeout ( resolve , 10000 ) ) ;
2098
2102
2099
- // Resume the query and expect to get a snapshot with the 50
2100
- // remaining documents. Use some internal testing hooks to "capture"
2101
- // the existence filter mismatches to later verify that Watch sent a
2102
- // bloom filter, and it was used to avert a full requery.
2103
- const existenceFilterMismatches = await captureExistenceFilterMismatches (
2104
- async ( ) => {
2105
- const snapshot2 = await getDocs ( coll ) ;
2106
- // TODO(b/270731363): Remove the "if" condition below once the
2107
- // Firestore Emulator is fixed to send an existence filter. At the
2108
- // time of writing, the Firestore emulator fails to send an
2109
- // existence filter, resulting in the client including the deleted
2110
- // documents in the snapshot of the resumed query.
2111
- if ( ! ( USE_EMULATOR && snapshot2 . size === 100 ) ) {
2112
- expect ( snapshot2 . size , 'snapshot2.size' ) . to . equal ( 50 ) ;
2113
- }
2114
- }
2115
- ) ;
2103
+ // Resume the query and save the resulting snapshot for verification.
2104
+ // Use some internal testing hooks to "capture" the existence filter
2105
+ // mismatches to verify that Watch sent a bloom filter, and it was used to
2106
+ // avert a full requery.
2107
+ const [ existenceFilterMismatches , snapshot2 ] =
2108
+ await captureExistenceFilterMismatches ( ( ) => getDocs ( coll ) ) ;
2109
+
2110
+ // Verify that the snapshot from the resumed query contains the expected
2111
+ // documents; that is, that it contains the 50 documents that were _not_
2112
+ // deleted.
2113
+ // TODO(b/270731363): Remove the "if" condition below once the
2114
+ // Firestore Emulator is fixed to send an existence filter. At the time of
2115
+ // writing, the Firestore emulator fails to send an existence filter,
2116
+ // resulting in the client including the deleted documents in the snapshot
2117
+ // of the resumed query.
2118
+ if ( ! ( USE_EMULATOR && snapshot2 . size === 100 ) ) {
2119
+ const actualDocumentIds = snapshot2 . docs
2120
+ . map ( documentSnapshot => documentSnapshot . ref . id )
2121
+ . sort ( ) ;
2122
+ const expectedDocumentIds = createdDocuments
2123
+ . filter ( documentRef => ! deletedDocumentIds . has ( documentRef . id ) )
2124
+ . map ( documentRef => documentRef . id )
2125
+ . sort ( ) ;
2126
+ expect ( actualDocumentIds , 'snapshot2.docs' ) . to . deep . equal (
2127
+ expectedDocumentIds
2128
+ ) ;
2129
+ }
2116
2130
2117
- // Skip the verification of the existence filter mismatch when
2118
- // persistence is disabled because without persistence there is no
2119
- // resume token specified in the subsequent call to getDocs(), and,
2120
- // therefore, Watch will _not_ send an existence filter.
2131
+ // Skip the verification of the existence filter mismatch when persistence
2132
+ // is disabled because without persistence there is no resume token
2133
+ // specified in the subsequent call to getDocs(), and, therefore, Watch
2134
+ // will _not_ send an existence filter.
2135
+ // TODO(b/272754156) Re-write this test using a snapshot listener instead
2136
+ // of calls to getDocs() and remove this check for disabled persistence.
2121
2137
if ( ! persistence ) {
2122
2138
return 'passed' ;
2123
2139
}
2124
2140
2125
- // Skip the verification of the existence filter mismatch when
2126
- // testing against the Firestore emulator because the Firestore
2127
- // emulator does not include the `unchanged_names` bloom filter when
2128
- // it sends ExistenceFilter messages. Some day the emulator _may_
2129
- // implement this logic, at which time this short-circuit can be
2130
- // removed.
2141
+ // Skip the verification of the existence filter mismatch when testing
2142
+ // against the Firestore emulator because the Firestore emulator does not
2143
+ // include the `unchanged_names` bloom filter when it sends
2144
+ // ExistenceFilter messages. Some day the emulator _may_ implement this
2145
+ // logic, at which time this short-circuit can be removed.
2131
2146
if ( USE_EMULATOR ) {
2132
2147
return 'passed' ;
2133
2148
}
2134
2149
2135
- // Verify that upon resuming the query that Watch sent an existence
2136
- // filter that included a bloom filter, and that the bloom filter
2137
- // was successfully used to avoid a full requery.
2138
- // TODO(b/271949433) Remove this check for "nightly" once the bloom
2139
- // filter logic is deployed to production, circa May 2023.
2140
- if ( TARGET_BACKEND === 'nightly' ) {
2141
- expect (
2142
- existenceFilterMismatches ,
2143
- 'existenceFilterMismatches'
2144
- ) . to . have . length ( 1 ) ;
2145
- const { localCacheCount, existenceFilterCount, bloomFilter } =
2146
- existenceFilterMismatches [ 0 ] ;
2147
-
2148
- expect ( localCacheCount , 'localCacheCount' ) . to . equal ( 100 ) ;
2149
- expect ( existenceFilterCount , 'existenceFilterCount' ) . to . equal ( 50 ) ;
2150
- if ( ! bloomFilter ) {
2151
- expect . fail (
2152
- 'The existence filter should have specified ' +
2153
- 'a bloom filter in its `unchanged_names` field.'
2154
- ) ;
2155
- throw new Error ( 'should never get here' ) ;
2156
- }
2150
+ // Verify that Watch sent an existence filter with the correct counts when
2151
+ // the query was resumed.
2152
+ expect (
2153
+ existenceFilterMismatches ,
2154
+ 'existenceFilterMismatches'
2155
+ ) . to . have . length ( 1 ) ;
2156
+ const { localCacheCount, existenceFilterCount, bloomFilter } =
2157
+ existenceFilterMismatches [ 0 ] ;
2158
+ expect ( localCacheCount , 'localCacheCount' ) . to . equal ( 100 ) ;
2159
+ expect ( existenceFilterCount , 'existenceFilterCount' ) . to . equal ( 50 ) ;
2160
+
2161
+ // Skip the verification of the bloom filter when testing against
2162
+ // production because the bloom filter is only implemented in nightly.
2163
+ // TODO(b/271949433) Remove this "if" block once the bloom filter logic is
2164
+ // deployed to production.
2165
+ if ( TARGET_BACKEND !== 'nightly' ) {
2166
+ return 'passed' ;
2167
+ }
2157
2168
2158
- expect ( bloomFilter . hashCount , 'bloomFilter.hashCount' ) . to . be . above ( 0 ) ;
2159
- expect (
2160
- bloomFilter . bitmapLength ,
2161
- 'bloomFilter.bitmapLength'
2162
- ) . to . be . above ( 0 ) ;
2163
- expect ( bloomFilter . padding , 'bloomFilterPadding' ) . to . be . above ( 0 ) ;
2164
- expect ( bloomFilter . padding , 'bloomFilterPadding' ) . to . be . below ( 8 ) ;
2165
-
2166
- // Retry the entire test if a bloom filter false positive occurs.
2167
- // Although statistically rare, false positives are expected to
2168
- // happen occasionally. When a false positive _does_ happen, just
2169
- // retry the test with a different set of documents. If that retry
2170
- // _also_ experiences a false positive, then fail the test because
2171
- // that is so improbable that something must have gone wrong.
2172
- if ( attemptNumber > 1 && ! bloomFilter . applied ) {
2173
- return 'retry' ;
2174
- }
2175
- expect ( bloomFilter . applied , 'bloomFilter.applied' ) . to . be . true ;
2169
+ // Verify that Watch sent a valid bloom filter.
2170
+ if ( ! bloomFilter ) {
2171
+ expect . fail (
2172
+ 'The existence filter should have specified a bloom filter in its ' +
2173
+ '`unchanged_names` field.'
2174
+ ) ;
2175
+ throw new Error ( 'should never get here' ) ;
2176
+ }
2177
+
2178
+ expect ( bloomFilter . hashCount , 'bloomFilter.hashCount' ) . to . be . above ( 0 ) ;
2179
+ expect ( bloomFilter . bitmapLength , 'bloomFilter.bitmapLength' ) . to . be . above (
2180
+ 0
2181
+ ) ;
2182
+ expect ( bloomFilter . padding , 'bloomFilterPadding' ) . to . be . above ( 0 ) ;
2183
+ expect ( bloomFilter . padding , 'bloomFilterPadding' ) . to . be . below ( 8 ) ;
2184
+
2185
+ // Verify that the bloom filter was successfully used to avert a full
2186
+ // requery. If a false positive occurred then retry the entire test.
2187
+ // Although statistically rare, false positives are expected to happen
2188
+ // occasionally. When a false positive _does_ happen, just retry the test
2189
+ // with a different set of documents. If that retry _also_ experiences a
2190
+ // false positive, then fail the test because that is so improbable that
2191
+ // something must have gone wrong.
2192
+ if ( attemptNumber === 1 && ! bloomFilter . applied ) {
2193
+ return 'retry' ;
2176
2194
}
2195
+ expect (
2196
+ bloomFilter . applied ,
2197
+ `bloomFilter.applied with attemptNumber=${ attemptNumber } `
2198
+ ) . to . be . true ;
2177
2199
2178
2200
return 'passed' ;
2179
2201
} ;
0 commit comments