Skip to content

Commit c22a190

Browse files
committed
Add a schema migration that drops the query cache (#1558)
This is a force fix for potential existence filter mismatches caused by #1548 The essential problem is that when resuming a query, the server is allowed to forget deletes. If the number of incorrectly synthesized deletes matches the number of server-forgotten deletes then the existence filter can give a false positive, preventing the cache from self healing. Dropping the query cache clears any client-side resume token which prevents a false positive existence filter mismatch. Note that the remote document cache and mutation queues are unaffected so any cached documents that do exist will still work while offline firebase/firebase-js-sdk#1019
1 parent 9cc378a commit c22a190

File tree

5 files changed

+191
-87
lines changed

5 files changed

+191
-87
lines changed

Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm

Lines changed: 119 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,29 @@
1818

1919
#include <memory>
2020
#include <string>
21+
#include <vector>
2122

2223
#import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h"
2324
#import "Firestore/Source/Local/FSTLevelDB.h"
2425
#import "Firestore/Source/Local/FSTLevelDBKey.h"
2526
#import "Firestore/Source/Local/FSTLevelDBMigrations.h"
27+
#import "Firestore/Source/Local/FSTLevelDBMutationQueue.h"
2628
#import "Firestore/Source/Local/FSTLevelDBQueryCache.h"
2729

2830
#include "Firestore/core/src/firebase/firestore/util/ordered_code.h"
31+
#include "Firestore/core/src/firebase/firestore/util/status.h"
32+
#include "Firestore/core/test/firebase/firestore/testutil/testutil.h"
33+
#include "absl/strings/match.h"
2934
#include "leveldb/db.h"
3035

3136
#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h"
3237

3338
NS_ASSUME_NONNULL_BEGIN
3439

40+
using firebase::firestore::FirestoreErrorCode;
3541
using firebase::firestore::local::LevelDbTransaction;
3642
using firebase::firestore::util::OrderedCode;
43+
using firebase::firestore::testutil::Key;
3744
using leveldb::DB;
3845
using leveldb::Options;
3946
using leveldb::Status;
@@ -64,54 +71,136 @@ - (void)tearDown {
6471
- (void)testAddsTargetGlobal {
6572
FSTPBTargetGlobal *metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()];
6673
XCTAssertNil(metadata, @"Not expecting metadata yet, we should have an empty db");
67-
LevelDbTransaction transaction(_db.get(), "testAddsTargetGlobal");
68-
[FSTLevelDBMigrations runMigrationsWithTransaction:&transaction];
69-
transaction.Commit();
74+
[FSTLevelDBMigrations runMigrationsWithDatabase:_db.get()];
75+
7076
metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()];
7177
XCTAssertNotNil(metadata, @"Migrations should have added the metadata");
7278
}
7379

7480
- (void)testSetsVersionNumber {
75-
LevelDbTransaction transaction(_db.get(), "testSetsVersionNumber");
76-
FSTLevelDBSchemaVersion initial =
77-
[FSTLevelDBMigrations schemaVersionWithTransaction:&transaction];
78-
XCTAssertEqual(0, initial, "No version should be equivalent to 0");
79-
80-
// Pick an arbitrary high migration number and migrate to it.
81-
[FSTLevelDBMigrations runMigrationsWithTransaction:&transaction];
82-
FSTLevelDBSchemaVersion actual = [FSTLevelDBMigrations schemaVersionWithTransaction:&transaction];
83-
XCTAssertGreaterThan(actual, 0, @"Expected to migrate to a schema version > 0");
81+
{
82+
LevelDbTransaction transaction(_db.get(), "testSetsVersionNumber before");
83+
FSTLevelDBSchemaVersion initial =
84+
[FSTLevelDBMigrations schemaVersionWithTransaction:&transaction];
85+
XCTAssertEqual(0, initial, "No version should be equivalent to 0");
86+
}
87+
88+
{
89+
// Pick an arbitrary high migration number and migrate to it.
90+
[FSTLevelDBMigrations runMigrationsWithDatabase:_db.get()];
91+
92+
LevelDbTransaction transaction(_db.get(), "testSetsVersionNumber after");
93+
FSTLevelDBSchemaVersion actual =
94+
[FSTLevelDBMigrations schemaVersionWithTransaction:&transaction];
95+
XCTAssertGreaterThan(actual, 0, @"Expected to migrate to a schema version > 0");
96+
}
8497
}
8598

86-
- (void)testCountsQueries {
87-
NSUInteger expected = 50;
99+
#define ASSERT_NOT_FOUND(transaction, key) \
100+
do { \
101+
std::string unused_result; \
102+
Status status = transaction.Get(key, &unused_result); \
103+
XCTAssertTrue(status.IsNotFound()); \
104+
} while (0)
105+
106+
#define ASSERT_FOUND(transaction, key) \
107+
do { \
108+
std::string unused_result; \
109+
Status status = transaction.Get(key, &unused_result); \
110+
XCTAssertTrue(status.ok()); \
111+
} while (0)
112+
113+
- (void)testDropsTheQueryCache {
114+
NSString *userID = @"user";
115+
FSTBatchID batchID = 1;
116+
FSTTargetID targetID = 2;
117+
118+
FSTDocumentKey *key1 = Key("documents/1");
119+
FSTDocumentKey *key2 = Key("documents/2");
120+
121+
std::string targetKeys[] = {
122+
[FSTLevelDBTargetKey keyWithTargetID:targetID],
123+
[FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key1],
124+
[FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key2],
125+
[FSTLevelDBDocumentTargetKey keyWithDocumentKey:key1 targetID:targetID],
126+
[FSTLevelDBDocumentTargetKey keyWithDocumentKey:key2 targetID:targetID],
127+
[FSTLevelDBQueryTargetKey keyWithCanonicalID:"foo.bar.baz" targetID:targetID],
128+
};
129+
130+
// Keys that should not be modified by the dropping the query cache
131+
std::string preservedKeys[] = {
132+
[self dummyKeyForTable:"targetA"],
133+
[FSTLevelDBMutationQueueKey keyWithUserID:userID],
134+
[FSTLevelDBMutationKey keyWithUserID:userID batchID:batchID],
135+
};
136+
137+
[FSTLevelDBMigrations runMigrationsWithDatabase:_db.get() upToVersion:2];
88138
{
89139
// Setup some targets to be counted in the migration.
90-
LevelDbTransaction transaction(_db.get(), "testCountsQueries setup");
91-
for (int i = 0; i < expected; i++) {
92-
std::string key = [FSTLevelDBTargetKey keyWithTargetID:i];
93-
transaction.Put(key, "dummy");
140+
LevelDbTransaction transaction(_db.get(), "testDropsTheQueryCache setup");
141+
for (const std::string &key : targetKeys) {
142+
transaction.Put(key, "target");
143+
}
144+
for (const std::string &key : preservedKeys) {
145+
transaction.Put(key, "preserved");
94146
}
95-
// Add a dummy entry after the targets to make sure the iteration is correctly bounded.
96-
// Use a table that would sort logically right after that table 'target'.
97-
std::string dummyKey;
98-
// Magic number that indicates a table name follows. Needed to mimic the prefix to the target
99-
// table.
100-
OrderedCode::WriteSignedNumIncreasing(&dummyKey, 5);
101-
OrderedCode::WriteString(&dummyKey, "targetA");
102-
transaction.Put(dummyKey, "dummy");
103147
transaction.Commit();
104148
}
105149

150+
[FSTLevelDBMigrations runMigrationsWithDatabase:_db.get() upToVersion:3];
106151
{
107-
LevelDbTransaction transaction(_db.get(), "testCountsQueries");
108-
[FSTLevelDBMigrations runMigrationsWithTransaction:&transaction];
109-
transaction.Commit();
152+
LevelDbTransaction transaction(_db.get(), "testDropsTheQueryCache");
153+
for (const std::string &key : targetKeys) {
154+
ASSERT_NOT_FOUND(transaction, key);
155+
}
156+
for (const std::string &key : preservedKeys) {
157+
ASSERT_FOUND(transaction, key);
158+
}
159+
110160
FSTPBTargetGlobal *metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()];
111-
XCTAssertEqual(expected, metadata.targetCount, @"Failed to count all of the targets we added");
161+
XCTAssertNotNil(metadata, @"Metadata should have been added");
162+
XCTAssertEqual(metadata.targetCount, 0);
112163
}
113164
}
114165

166+
- (void)testDropsTheQueryCacheWithThousandsOfEntries {
167+
[FSTLevelDBMigrations runMigrationsWithDatabase:_db.get() upToVersion:2];
168+
{
169+
// Setup some targets to be destroyed.
170+
LevelDbTransaction transaction(_db.get(), "testDropsTheQueryCacheWithThousandsOfEntries setup");
171+
for (int i = 0; i < 10000; ++i) {
172+
transaction.Put([FSTLevelDBTargetKey keyWithTargetID:i], "");
173+
}
174+
transaction.Commit();
175+
}
176+
177+
[FSTLevelDBMigrations runMigrationsWithDatabase:_db.get() upToVersion:3];
178+
{
179+
LevelDbTransaction transaction(_db.get(), "Verify");
180+
std::string prefix = [FSTLevelDBTargetKey keyPrefix];
181+
182+
auto it = transaction.NewIterator();
183+
std::vector<std::string> found_keys;
184+
for (it->Seek(prefix); it->Valid() && absl::StartsWith(it->key(), prefix); it->Next()) {
185+
found_keys.push_back(std::string{it->key()});
186+
}
187+
188+
XCTAssertEqual(found_keys, std::vector<std::string>{});
189+
}
190+
}
191+
192+
/**
193+
* Creates the name of a dummy entry to make sure the iteration is correctly bounded.
194+
*/
195+
- (std::string)dummyKeyForTable:(const char *)tableName {
196+
std::string dummyKey;
197+
// Magic number that indicates a table name follows. Needed to mimic the prefix to the target
198+
// table.
199+
OrderedCode::WriteSignedNumIncreasing(&dummyKey, 5);
200+
OrderedCode::WriteString(&dummyKey, tableName);
201+
return dummyKey;
202+
}
203+
115204
@end
116205

117206
NS_ASSUME_NONNULL_END

Firestore/Source/Local/FSTLevelDB.mm

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,7 @@ - (BOOL)start:(NSError **)error {
150150
return NO;
151151
}
152152
_ptr.reset(database);
153-
LevelDbTransaction transaction(_ptr.get(), "Start LevelDB");
154-
[FSTLevelDBMigrations runMigrationsWithTransaction:&transaction];
155-
transaction.Commit();
153+
[FSTLevelDBMigrations runMigrationsWithDatabase:_ptr.get()];
156154
return YES;
157155
}
158156

Firestore/Source/Local/FSTLevelDBMigrations.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ typedef int32_t FSTLevelDBSchemaVersion;
3636
/**
3737
* Runs any migrations needed to bring the given database up to the current schema version
3838
*/
39-
+ (void)runMigrationsWithTransaction:(firebase::firestore::local::LevelDbTransaction *)transaction;
39+
+ (void)runMigrationsWithDatabase:(leveldb::DB *)database;
40+
41+
/**
42+
* Runs any migrations needed to bring the given database up to the given schema version
43+
*/
44+
+ (void)runMigrationsWithDatabase:(leveldb::DB *)database
45+
upToVersion:(FSTLevelDBSchemaVersion)version;
4046

4147
@end
4248

Firestore/Source/Local/FSTLevelDBMigrations.mm

Lines changed: 60 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,34 @@
2424

2525
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
2626
#include "absl/base/macros.h"
27+
#include "absl/memory/memory.h"
2728
#include "absl/strings/match.h"
2829
#include "leveldb/write_batch.h"
2930

3031
NS_ASSUME_NONNULL_BEGIN
3132

32-
// Current version of the schema defined in this file.
33-
static FSTLevelDBSchemaVersion kSchemaVersion = 2;
33+
/**
34+
* Schema version for the iOS client.
35+
*
36+
* Note that tables aren't a concept in LevelDB. They exist in our schema as just prefixes on keys.
37+
* This means tables don't need to be created but they also can't easily be dropped and re-created.
38+
*
39+
* Migrations:
40+
* * Migration 1 used to ensure the target_global row existed, without clearing it. No longer
41+
* required because migration 3 unconditionally clears it.
42+
* * Migration 2 used to ensure that the target_global row had a correct count of targets. No
43+
* longer required because migration 3 deletes them all.
44+
* * Migration 3 deletes the entire query cache to deal with cache corruption related to
45+
* limbo resolution. Addresses https://github.com/firebase/firebase-ios-sdk/issues/1548.
46+
*/
47+
static FSTLevelDBSchemaVersion kSchemaVersion = 3;
3448

3549
using firebase::firestore::local::LevelDbTransaction;
36-
using leveldb::DB;
3750
using leveldb::Iterator;
3851
using leveldb::Status;
3952
using leveldb::Slice;
4053
using leveldb::WriteOptions;
4154

42-
/**
43-
* Ensures that the global singleton target metadata row exists in LevelDB.
44-
*/
45-
static void EnsureTargetGlobal(LevelDbTransaction *transaction) {
46-
FSTPBTargetGlobal *targetGlobal =
47-
[FSTLevelDBQueryCache readTargetMetadataWithTransaction:transaction];
48-
if (!targetGlobal) {
49-
transaction->Put([FSTLevelDBTargetGlobalKey key], [FSTPBTargetGlobal message]);
50-
}
51-
}
52-
5355
/**
5456
* Save the given version number as the current version of the schema of the database.
5557
* @param version The version to save
@@ -61,30 +63,39 @@ static void SaveVersion(FSTLevelDBSchemaVersion version, LevelDbTransaction *tra
6163
transaction->Put(key, version_string);
6264
}
6365

64-
/**
65-
* This function counts the number of targets that currently exist in the given db. It
66-
* then reads the target global row, adds the count to the metadata from that row, and writes
67-
* the metadata back.
68-
*
69-
* It assumes the metadata has already been written and is able to be read in this transaction.
70-
*/
71-
static void AddTargetCount(LevelDbTransaction *transaction) {
72-
auto it = transaction->NewIterator();
73-
std::string start_key = [FSTLevelDBTargetKey keyPrefix];
74-
it->Seek(start_key);
75-
76-
int32_t count = 0;
77-
while (it->Valid() && absl::StartsWith(it->key(), start_key)) {
78-
count++;
79-
it->Next();
66+
static void DeleteEverythingWithPrefix(const std::string &prefix, leveldb::DB *db) {
67+
bool more_deletes = true;
68+
while (more_deletes) {
69+
LevelDbTransaction transaction(db, "Delete everything with prefix");
70+
auto it = transaction.NewIterator();
71+
72+
more_deletes = false;
73+
for (it->Seek(prefix); it->Valid() && absl::StartsWith(it->key(), prefix); it->Next()) {
74+
if (transaction.changed_keys() >= 1000) {
75+
more_deletes = true;
76+
break;
77+
}
78+
transaction.Delete(it->key());
79+
}
80+
81+
transaction.Commit();
8082
}
83+
}
84+
85+
/** Migration 3. */
86+
static void ClearQueryCache(leveldb::DB *db) {
87+
DeleteEverythingWithPrefix([FSTLevelDBTargetKey keyPrefix], db);
88+
DeleteEverythingWithPrefix([FSTLevelDBDocumentTargetKey keyPrefix], db);
89+
DeleteEverythingWithPrefix([FSTLevelDBTargetDocumentKey keyPrefix], db);
90+
DeleteEverythingWithPrefix([FSTLevelDBQueryTargetKey keyPrefix], db);
91+
92+
LevelDbTransaction transaction(db, "Drop query cache");
8193

82-
FSTPBTargetGlobal *targetGlobal =
83-
[FSTLevelDBQueryCache readTargetMetadataWithTransaction:transaction];
84-
HARD_ASSERT(targetGlobal != nil,
85-
"We should have a metadata row as it was added in an earlier migration");
86-
targetGlobal.targetCount = count;
87-
transaction->Put([FSTLevelDBTargetGlobalKey key], targetGlobal);
94+
// Reset the target global entry too (to reset the target count).
95+
transaction.Put([FSTLevelDBTargetGlobalKey key], [FSTPBTargetGlobal message]);
96+
97+
SaveVersion(3, &transaction);
98+
transaction.Commit();
8899
}
89100

90101
@implementation FSTLevelDBMigrations
@@ -101,23 +112,19 @@ + (FSTLevelDBSchemaVersion)schemaVersionWithTransaction:
101112
}
102113
}
103114

104-
+ (void)runMigrationsWithTransaction:(firebase::firestore::local::LevelDbTransaction *)transaction {
105-
FSTLevelDBSchemaVersion currentVersion = [self schemaVersionWithTransaction:transaction];
106-
// Each case in this switch statement intentionally falls through. This lets us
107-
// start at the current schema version and apply any migrations that have not yet
108-
// been applied, to bring us up to current, as defined by the kSchemaVersion constant.
109-
switch (currentVersion) {
110-
case 0:
111-
EnsureTargetGlobal(transaction);
112-
ABSL_FALLTHROUGH_INTENDED;
113-
case 1:
114-
// We're now guaranteed that the target global exists. We can safely add a count to it.
115-
AddTargetCount(transaction);
116-
ABSL_FALLTHROUGH_INTENDED;
117-
default:
118-
if (currentVersion < kSchemaVersion) {
119-
SaveVersion(kSchemaVersion, transaction);
120-
}
115+
+ (void)runMigrationsWithDatabase:(leveldb::DB *)database {
116+
[self runMigrationsWithDatabase:database upToVersion:kSchemaVersion];
117+
}
118+
119+
+ (void)runMigrationsWithDatabase:(leveldb::DB *)database
120+
upToVersion:(FSTLevelDBSchemaVersion)toVersion {
121+
LevelDbTransaction transaction{database, "Read schema version"};
122+
FSTLevelDBSchemaVersion fromVersion = [self schemaVersionWithTransaction:&transaction];
123+
124+
// This must run unconditionally because schema migrations were added to iOS after the first
125+
// release. There may be clients that have never run any migrations that have existing targets.
126+
if (fromVersion < 3 && toVersion >= 3) {
127+
ClearQueryCache(database);
121128
}
122129
}
123130

Firestore/core/src/firebase/firestore/local/leveldb_transaction.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ class LevelDbTransaction {
144144
*/
145145
static const leveldb::WriteOptions& DefaultWriteOptions();
146146

147+
size_t changed_keys() const {
148+
return mutations_.size() + deletions_.size();
149+
}
150+
147151
/**
148152
* Remove the database entry (if any) for "key". It is not an error if "key"
149153
* did not exist in the database.

0 commit comments

Comments
 (0)