Skip to content

Commit 4c86de9

Browse files
author
Greg Soltis
authored
Schema migration to ensure sentinel rows exist (#1911)
* Add schema migration to ensure each document has a sentinel row
1 parent 86dc058 commit 4c86de9

File tree

8 files changed

+177
-17
lines changed

8 files changed

+177
-17
lines changed

Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
#include "Firestore/core/src/firebase/firestore/local/leveldb_key.h"
2929
#include "Firestore/core/src/firebase/firestore/local/leveldb_migrations.h"
30+
#include "Firestore/core/src/firebase/firestore/model/document_key.h"
31+
#include "Firestore/core/src/firebase/firestore/model/types.h"
3032
#include "Firestore/core/src/firebase/firestore/util/ordered_code.h"
3133
#include "Firestore/core/src/firebase/firestore/util/status.h"
3234
#include "Firestore/core/test/firebase/firestore/testutil/testutil.h"
@@ -43,10 +45,14 @@
4345
using firebase::firestore::local::LevelDbMutationKey;
4446
using firebase::firestore::local::LevelDbMutationQueueKey;
4547
using firebase::firestore::local::LevelDbQueryTargetKey;
48+
using firebase::firestore::local::LevelDbRemoteDocumentKey;
4649
using firebase::firestore::local::LevelDbTargetDocumentKey;
50+
using firebase::firestore::local::LevelDbTargetGlobalKey;
4751
using firebase::firestore::local::LevelDbTargetKey;
4852
using firebase::firestore::local::LevelDbTransaction;
4953
using firebase::firestore::model::BatchId;
54+
using firebase::firestore::model::DocumentKey;
55+
using firebase::firestore::model::ListenSequenceNumber;
5056
using firebase::firestore::model::TargetId;
5157
using firebase::firestore::testutil::Key;
5258
using firebase::firestore::util::OrderedCode;
@@ -199,6 +205,64 @@ - (void)testDropsTheQueryCacheWithThousandsOfEntries {
199205
}
200206
}
201207

208+
- (void)testAddsSentinelRows {
209+
ListenSequenceNumber old_sequence_number = 1;
210+
ListenSequenceNumber new_sequence_number = 2;
211+
std::string encoded_old_sequence_number =
212+
LevelDbDocumentTargetKey::EncodeSentinelValue(old_sequence_number);
213+
LevelDbMigrations::RunMigrations(_db.get(), 3);
214+
{
215+
std::string empty_buffer;
216+
LevelDbTransaction transaction(_db.get(), "Setup");
217+
218+
// Set up target global
219+
FSTPBTargetGlobal *metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()];
220+
// Expect that documents missing a row will get the new number
221+
metadata.highestListenSequenceNumber = new_sequence_number;
222+
transaction.Put(LevelDbTargetGlobalKey::Key(), metadata);
223+
224+
// Set up some documents (we only need the keys)
225+
// For the odd ones, add sentinel rows.
226+
for (int i = 0; i < 10; i++) {
227+
DocumentKey key = DocumentKey::FromSegments({"docs", std::to_string(i)});
228+
transaction.Put(LevelDbRemoteDocumentKey::Key(key), empty_buffer);
229+
if (i % 2 == 1) {
230+
std::string sentinel_key = LevelDbDocumentTargetKey::SentinelKey(key);
231+
transaction.Put(sentinel_key, encoded_old_sequence_number);
232+
}
233+
}
234+
235+
transaction.Commit();
236+
}
237+
238+
LevelDbMigrations::RunMigrations(_db.get(), 4);
239+
{
240+
LevelDbTransaction transaction(_db.get(), "Verify");
241+
auto it = transaction.NewIterator();
242+
std::string documents_prefix = LevelDbRemoteDocumentKey::KeyPrefix();
243+
it->Seek(documents_prefix);
244+
int count = 0;
245+
LevelDbRemoteDocumentKey document_key;
246+
std::string buffer;
247+
for (; it->Valid() && absl::StartsWith(it->key(), documents_prefix); it->Next()) {
248+
count++;
249+
XCTAssertTrue(document_key.Decode(it->key()));
250+
const DocumentKey &key = document_key.document_key();
251+
std::string sentinel_key = LevelDbDocumentTargetKey::SentinelKey(key);
252+
XCTAssertTrue(transaction.Get(sentinel_key, &buffer).ok());
253+
int doc_number = atoi(key.path().last_segment().c_str());
254+
// If the document number is odd, we expect the original old sequence number that we wrote.
255+
// If it's even, we expect that the migration added the new sequence number from the target
256+
// global
257+
ListenSequenceNumber expected_sequence_number =
258+
doc_number % 2 == 1 ? old_sequence_number : new_sequence_number;
259+
ListenSequenceNumber sequence_number = LevelDbDocumentTargetKey::DecodeSentinelValue(buffer);
260+
XCTAssertEqual(expected_sequence_number, sequence_number);
261+
}
262+
XCTAssertEqual(10, count);
263+
}
264+
}
265+
202266
/**
203267
* Creates the name of a dummy entry to make sure the iteration is correctly bounded.
204268
*/

Firestore/Source/Local/FSTLevelDB.mm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ - (FSTLRUGarbageCollector *)gc {
234234
}
235235

236236
- (void)writeSentinelForKey:(const DocumentKey &)key {
237-
std::string encodedSequenceNumber;
238-
OrderedCode::WriteSignedNumIncreasing(&encodedSequenceNumber, [self currentSequenceNumber]);
239237
std::string sentinelKey = LevelDbDocumentTargetKey::SentinelKey(key);
238+
std::string encodedSequenceNumber =
239+
LevelDbDocumentTargetKey::EncodeSentinelValue([self currentSequenceNumber]);
240240
_db.currentTransaction->Put(sentinelKey, encodedSequenceNumber);
241241
}
242242

Firestore/Source/Local/FSTLevelDBQueryCache.mm

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
#include "Firestore/core/src/firebase/firestore/model/document_key.h"
3131
#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h"
3232
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
33-
#include "Firestore/core/src/firebase/firestore/util/ordered_code.h"
3433
#include "Firestore/core/src/firebase/firestore/util/string_apple.h"
3534
#include "absl/strings/match.h"
3635

@@ -49,22 +48,10 @@
4948
using firebase::firestore::model::SnapshotVersion;
5049
using firebase::firestore::model::TargetId;
5150
using firebase::firestore::util::MakeString;
52-
using firebase::firestore::util::OrderedCode;
5351
using leveldb::DB;
5452
using leveldb::Slice;
5553
using leveldb::Status;
5654

57-
namespace {
58-
59-
ListenSequenceNumber ReadSequenceNumber(absl::string_view slice) {
60-
ListenSequenceNumber decoded;
61-
if (!OrderedCode::ReadSignedNumIncreasing(&slice, &decoded)) {
62-
HARD_FAIL("Failed to read sequence number from a sentinel row");
63-
}
64-
return decoded;
65-
}
66-
} // namespace
67-
6855
@interface FSTLevelDBQueryCache ()
6956

7057
/** A write-through cached copy of the metadata for the query cache. */
@@ -201,7 +188,7 @@ - (void)enumerateOrphanedDocumentsUsingBlock:
201188
}
202189
// set nextToReport to be this sequence number. It's the next one we might
203190
// report, if we don't find any targets for this document.
204-
nextToReport = ReadSequenceNumber(it->value());
191+
nextToReport = LevelDbDocumentTargetKey::DecodeSentinelValue(it->value());
205192
keyToReport = key.document_key();
206193
} else {
207194
// set nextToReport to be 0, we know we don't need to report this one since

Firestore/core/src/firebase/firestore/local/leveldb_key.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,22 @@ std::string LevelDbDocumentTargetKey::SentinelKey(
793793
return Key(document_key, kInvalidTargetId);
794794
}
795795

796+
std::string LevelDbDocumentTargetKey::EncodeSentinelValue(
797+
model::ListenSequenceNumber sequence_number) {
798+
std::string encoded;
799+
OrderedCode::WriteSignedNumIncreasing(&encoded, sequence_number);
800+
return encoded;
801+
}
802+
803+
model::ListenSequenceNumber LevelDbDocumentTargetKey::DecodeSentinelValue(
804+
absl::string_view slice) {
805+
model::ListenSequenceNumber decoded;
806+
if (!OrderedCode::ReadSignedNumIncreasing(&slice, &decoded)) {
807+
HARD_FAIL("Failed to read sequence number from a sentinel row");
808+
}
809+
return decoded;
810+
}
811+
796812
bool LevelDbDocumentTargetKey::Decode(absl::string_view key) {
797813
Reader reader{key};
798814
reader.ReadTableNameMatching(kDocumentTargetsTable);

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,18 @@ class LevelDbDocumentTargetKey {
416416
*/
417417
static std::string SentinelKey(const model::DocumentKey& document_key);
418418

419+
/**
420+
* Given a sequence number, encodes it for storage in a sentinel row.
421+
*/
422+
static std::string EncodeSentinelValue(
423+
model::ListenSequenceNumber sequence_number);
424+
425+
/**
426+
* Given an encoded sentinel row, return the sequence number.
427+
*/
428+
static model::ListenSequenceNumber DecodeSentinelValue(
429+
absl::string_view slice);
430+
419431
/**
420432
* Decodes the contents of a document target key, storing the decoded values
421433
* in this instance.

Firestore/core/src/firebase/firestore/local/leveldb_migrations.cc

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
#include "Firestore/Protos/nanopb/firestore/local/target.nanopb.h"
2323
#include "Firestore/core/src/firebase/firestore/local/leveldb_key.h"
24+
#include "Firestore/core/src/firebase/firestore/model/document_key.h"
25+
#include "Firestore/core/src/firebase/firestore/model/types.h"
2426
#include "Firestore/core/src/firebase/firestore/nanopb/writer.h"
2527
#include "absl/strings/match.h"
2628

@@ -32,6 +34,7 @@ using leveldb::Iterator;
3234
using leveldb::Slice;
3335
using leveldb::Status;
3436
using leveldb::WriteOptions;
37+
using nanopb::Reader;
3538
using nanopb::Writer;
3639

3740
namespace {
@@ -52,8 +55,10 @@ namespace {
5255
* * Migration 3 deletes the entire query cache to deal with cache corruption
5356
* related to limbo resolution. Addresses
5457
* https://github.com/firebase/firebase-ios-sdk/issues/1548.
58+
* * Migration 4 ensures that every document in the remote document cache
59+
* has a sentinel row with a sequence number.
5560
*/
56-
const LevelDbMigrations::SchemaVersion kSchemaVersion = 3;
61+
const LevelDbMigrations::SchemaVersion kSchemaVersion = 4;
5762

5863
/**
5964
* Save the given version number as the current version of the schema of the
@@ -110,6 +115,63 @@ void ClearQueryCache(leveldb::DB* db) {
110115
transaction.Commit();
111116
}
112117

118+
/**
119+
* Reads the highest sequence number from the target global row.
120+
*/
121+
model::ListenSequenceNumber GetHighestSequenceNumber(
122+
LevelDbTransaction* transaction) {
123+
std::string bytes;
124+
transaction->Get(LevelDbTargetGlobalKey::Key(), &bytes);
125+
126+
firestore_client_TargetGlobal target_global{};
127+
Reader reader = Reader::Wrap(bytes);
128+
reader.ReadNanopbMessage(firestore_client_TargetGlobal_fields,
129+
&target_global);
130+
return target_global.highest_listen_sequence_number;
131+
}
132+
133+
/**
134+
* Given a document key, ensure it has a sentinel row. If it doesn't have one,
135+
* add it with the given value.
136+
*/
137+
void EnsureSentinelRow(LevelDbTransaction* transaction,
138+
const model::DocumentKey& key,
139+
const std::string& sentinel_value) {
140+
std::string sentinel_key = LevelDbDocumentTargetKey::SentinelKey(key);
141+
std::string unused_value;
142+
if (transaction->Get(sentinel_key, &unused_value).IsNotFound()) {
143+
transaction->Put(sentinel_key, sentinel_value);
144+
}
145+
}
146+
147+
/**
148+
* Ensure each document in the remote document table has a corresponding
149+
* sentinel row in the document target index.
150+
*/
151+
void EnsureSentinelRows(leveldb::DB* db) {
152+
LevelDbTransaction transaction(db, "Ensure sentinel rows");
153+
154+
// Get the value we'll use for anything that's missing a row.
155+
model::ListenSequenceNumber sequence_number =
156+
GetHighestSequenceNumber(&transaction);
157+
std::string sentinel_value =
158+
LevelDbDocumentTargetKey::EncodeSentinelValue(sequence_number);
159+
160+
std::string documents_prefix = LevelDbRemoteDocumentKey::KeyPrefix();
161+
auto it = transaction.NewIterator();
162+
it->Seek(documents_prefix);
163+
LevelDbRemoteDocumentKey document_key;
164+
for (; it->Valid() && absl::StartsWith(it->key(), documents_prefix);
165+
it->Next()) {
166+
HARD_ASSERT(document_key.Decode(it->key()),
167+
"Failed to decode document key");
168+
EnsureSentinelRow(&transaction, document_key.document_key(),
169+
sentinel_value);
170+
}
171+
SaveVersion(4, &transaction);
172+
transaction.Commit();
173+
}
174+
113175
} // namespace
114176

115177
LevelDbMigrations::SchemaVersion LevelDbMigrations::ReadSchemaVersion(
@@ -139,6 +201,10 @@ void LevelDbMigrations::RunMigrations(leveldb::DB* db,
139201
if (from_version < 3 && to_version >= 3) {
140202
ClearQueryCache(db);
141203
}
204+
205+
if (from_version < 4 && to_version >= 4) {
206+
EnsureSentinelRows(db);
207+
}
142208
}
143209

144210
} // namespace local

Firestore/core/src/firebase/firestore/nanopb/reader.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ Reader Reader::Wrap(const uint8_t* bytes, size_t length) {
3030
return Reader{pb_istream_from_buffer(bytes, length)};
3131
}
3232

33+
Reader Reader::Wrap(absl::string_view string_view) {
34+
return Reader{pb_istream_from_buffer(
35+
reinterpret_cast<const uint8_t*>(string_view.data()),
36+
string_view.size())};
37+
}
38+
3339
uint32_t Reader::ReadTag() {
3440
Tag tag;
3541
if (!status_.ok()) return 0;

Firestore/core/src/firebase/firestore/nanopb/reader.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ class Reader {
5959
*/
6060
static Reader Wrap(const uint8_t* bytes, size_t length);
6161

62+
/**
63+
* Creates an input stream from bytes backing the string_view. Note that
64+
* the backing buffer must remain valid for the lifetime of this Reader.
65+
*
66+
* (This is roughly equivalent to the nanopb function
67+
* pb_istream_from_buffer())
68+
*/
69+
static Reader Wrap(absl::string_view);
70+
6271
/**
6372
* Reads a message type from the input stream.
6473
*

0 commit comments

Comments
 (0)