@@ -2065,115 +2065,137 @@ apiDescribe('Queries', (persistence: boolean) => {
20652065 } ) ;
20662066
20672067 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 .
20692069 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 } ;
20722072 }
20732073
20742074 // 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.
20772077 const runTestIteration = async (
20782078 coll : CollectionReference ,
20792079 db : Firestore
20802080 ) : 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.
20832083 const snapshot1 = await getDocs ( coll ) ;
20842084 expect ( snapshot1 . size , 'snapshot1.size' ) . to . equal ( 100 ) ;
2085+ const createdDocuments = snapshot1 . docs . map ( snapshot => snapshot . ref ) ;
20852086
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 > ( ) ;
20882090 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 ) ;
20912095 }
20922096 } ) ;
20932097
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.
20972101 await new Promise ( resolve => setTimeout ( resolve , 10000 ) ) ;
20982102
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+ }
21162130
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.
21212137 if ( ! persistence ) {
21222138 return 'passed' ;
21232139 }
21242140
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.
21312146 if ( USE_EMULATOR ) {
21322147 return 'passed' ;
21332148 }
21342149
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+ }
21572168
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' ;
21762194 }
2195+ expect (
2196+ bloomFilter . applied ,
2197+ `bloomFilter.applied with attemptNumber=${ attemptNumber } `
2198+ ) . to . be . true ;
21772199
21782200 return 'passed' ;
21792201 } ;
0 commit comments