Skip to content

Commit

Permalink
CBL-6131: Race creating the expiration column in a collection table (
Browse files Browse the repository at this point in the history
…#2160)

Introduced a new schema version, v502. In this version, 
- Kv tables will be created with "expiration" as a new column. 
- Code that adds the column on-demand is stripped.
- Tables of prior versions are properly upgraded.
  • Loading branch information
callumbirks authored Oct 25, 2024
1 parent 66d66a4 commit 6c0e685
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 44 deletions.
5 changes: 0 additions & 5 deletions LiteCore/Query/SQLiteQuery.cc
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,6 @@ namespace litecore {
error::_throw(error::NoSuchIndex, "'match' test requires a full-text index");
}

// If expiration is queried, ensure the table(s) have the expiration column:
if ( qp.usesExpiration() ) {
for ( auto ks : _keyStores ) ks->addExpiration();
}

LogTo(SQL, "Compiled {Query#%u}: %s", getObjectRef(), sql.c_str());
_statement = dataFile.compile(sql.c_str());

Expand Down
5 changes: 0 additions & 5 deletions LiteCore/Storage/BothKeyStore.hh
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,6 @@ namespace litecore {

bool mayHaveExpiration() override { return _liveStore->mayHaveExpiration() || _deadStore->mayHaveExpiration(); }

void addExpiration() override {
_liveStore->addExpiration();
_deadStore->addExpiration();
}

bool setExpiration(slice key, expiration_t exp) override {
return _liveStore->setExpiration(key, exp) || _deadStore->setExpiration(key, exp);
}
Expand Down
2 changes: 0 additions & 2 deletions LiteCore/Storage/KeyStore.hh
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,6 @@ namespace litecore {
/** Does this KeyStore potentially have records that expire? (May return false positives.) */
virtual bool mayHaveExpiration() = 0;

virtual void addExpiration() = 0;

/** Sets a record's expiration time. Zero means 'never'.
@return true if the time was set, false if no record with that key exists. */
virtual bool setExpiration(slice key, expiration_t) = 0;
Expand Down
22 changes: 19 additions & 3 deletions LiteCore/Storage/SQLiteDataFile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,25 @@ namespace litecore {
if ( sql.find("lastSeq") == string::npos ) { _exec("ALTER TABLE indexes ADD COLUMN lastSeq TEXT"); }
}
});

(void)upgradeSchema(SchemaVersion::WithExpirationColumn, "Adding `expiration` column", [&] {
// Add the 'expiration' column to every KeyStore:
for ( string& name : allKeyStoreNames() ) {
if ( name.find("::") == string::npos ) {
string sql;
// We need to check for existence of the expiration column first.
// Do not add it if it already exists in the table.
if ( getSchema("kv_" + name, "table", "kv_" + name, sql)
&& sql.find("expiration") != string::npos )
continue;
// Only update data tables, not FTS index tables
_exec(format(
"ALTER TABLE \"kv_%s\" ADD COLUMN expiration INTEGER; "
"CREATE INDEX \"kv_%s_expiration\" ON \"kv_%s\" (expiration) WHERE expiration not null",
name.c_str(), name.c_str(), name.c_str()));
}
}
});
});

// Configure number of extra threads to be used by SQLite:
Expand Down Expand Up @@ -602,9 +621,6 @@ namespace litecore {
// Wrap the store in a BothKeyStore that manages it and the deleted store:
auto deletedStore = new SQLiteKeyStore(*this, kDeletedKeyStorePrefix + name, options);

keyStore->addExpiration();
deletedStore->addExpiration();

// Create a SQLite view of a union of both stores, for use in queries:
#define COLUMNS "key,sequence,flags,version,body,extra,expiration"
// Invarient: keyStore->tablaName() == kv_<tableName>
Expand Down
9 changes: 5 additions & 4 deletions LiteCore/Storage/SQLiteDataFile.hh
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,12 @@ namespace litecore {

WithNewDocs = 400, // New document/revision storage (CBL 3.0)

WithDeletedTable = 500, // Added 'deleted' KeyStore for deleted docs (CBL 3.0?)
WithIndexesLastSeq = 501, // Added 'lastSeq' column to 'indexes' table (CBL 3.2)
MaxReadable = 599, // Cannot open versions newer than this
WithDeletedTable = 500, // Added 'deleted' KeyStore for deleted docs (CBL 3.0?)
WithIndexesLastSeq = 501, // Added 'lastSeq' column to 'indexes' table (CBL 3.2)
WithExpirationColumn = 502, // Added 'expiration' column to KeyStore
MaxReadable = 599, // Cannot open versions newer than this

Current = WithDeletedTable
Current = WithExpirationColumn
};

void reopenSQLiteHandle();
Expand Down
32 changes: 11 additions & 21 deletions LiteCore/Storage/SQLiteKeyStore.cc
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ namespace litecore {
// more efficient in SQLite to keep large columns at the end of a row.
// Create the sequence and flags columns regardless of options, otherwise it's too
// complicated to customize all the SQL queries to conditionally use them...
db().execWithLock(subst("CREATE TABLE IF NOT EXISTS kv_@ ("
" key TEXT PRIMARY KEY,"
" sequence INTEGER,"
" flags INTEGER DEFAULT 0,"
" version BLOB,"
" body BLOB,"
" extra BLOB)"));
db().execWithLock(
subst("CREATE TABLE IF NOT EXISTS kv_@ ("
" key TEXT PRIMARY KEY,"
" sequence INTEGER,"
" flags INTEGER DEFAULT 0,"
" version BLOB,"
" body BLOB,"
" expiration INTEGER,"
" extra BLOB);"
"CREATE INDEX IF NOT EXISTS \"kv_@_expiration\" ON kv_@ (expiration) WHERE expiration not null"));
_uncommitedTable = db().inTransaction();
}

Expand Down Expand Up @@ -179,12 +182,10 @@ namespace litecore {
_purgeCountValid = false;

if ( !commit ) {
if ( _uncommittedExpirationColumn ) _hasExpirationColumn = false;
if ( _uncommitedTable ) { close(); }
}

_uncommittedExpirationColumn = false;
_uncommitedTable = false;
_uncommitedTable = false;
}

/*static*/ slice SQLiteKeyStore::columnAsSlice(const SQLite::Column& col) {
Expand Down Expand Up @@ -520,19 +521,8 @@ namespace litecore {
return _hasExpirationColumn;
}

// Adds the 'expiration' column to the table.
void SQLiteKeyStore::addExpiration() {
if ( mayHaveExpiration() ) return;
db()._logVerbose("Adding the `expiration` column & index to kv_%s", name().c_str());
db().execWithLock(subst("ALTER TABLE kv_@ ADD COLUMN expiration INTEGER; "
"CREATE INDEX \"kv_@_expiration\" ON kv_@ (expiration) WHERE expiration not null"));
_hasExpirationColumn = true;
_uncommittedExpirationColumn = true;
}

bool SQLiteKeyStore::setExpiration(slice key, expiration_t expTime) {
Assert(expTime >= expiration_t(0), "Invalid (negative) expiration time");
addExpiration();
auto& stmt = compileCached("UPDATE kv_@ SET expiration=? WHERE key=?");
UsingStatement u(stmt);
if ( expTime > expiration_t::None ) stmt.bind(1, (long long)expTime);
Expand Down
4 changes: 0 additions & 4 deletions LiteCore/Storage/SQLiteKeyStore.hh
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,6 @@ namespace litecore {
void createConflictsIndex();
void createBlobsIndex();

/// Adds the `expiration` column to the table. Called only by SQLiteQuery.
void addExpiration() override;

void shareSequencesWith(KeyStore&) override;

protected:
Expand Down Expand Up @@ -161,7 +158,6 @@ namespace litecore {
mutable std::optional<sequence_t> _lastSequence;
mutable std::atomic<uint64_t> _purgeCount{0};
bool _hasExpirationColumn{false};
bool _uncommittedExpirationColumn{false};
bool _uncommitedTable{false};
SQLiteKeyStore* _sequencesOwner{nullptr};
};
Expand Down
29 changes: 29 additions & 0 deletions LiteCore/tests/c4BaseTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "c4Test.hh"
#include "c4Internal.hh"
#include "c4Collection.h"
#include "c4ExceptionUtils.hh"
#include "fleece/InstanceCounted.hh"
#include "catch.hpp"
Expand All @@ -27,6 +28,7 @@
# include "Error.hh"
# include <winerror.h>
#endif
#include <future>
#include <sstream>

using namespace fleece;
Expand Down Expand Up @@ -139,6 +141,33 @@ TEST_CASE("C4Error Reporting Macros", "[Errors][C]") {
#endif
}

TEST_CASE_METHOD(C4Test, "Create collection concurrently", "[Database][C]") {
const slice dbName = db->getName();
const C4DatabaseConfig2 config = db->getConfiguration();

c4::ref db2 = c4db_openNamed(dbName, &config, ERROR_INFO());
REQUIRE(db2);

char buf[6]{};
for ( int i = 0; i < 5; i++ ) {
C4Error err{};
C4Error err2{};

snprintf(buf, 6, "coll%i", i);

{
slice collName{buf};
const C4CollectionSpec spec{collName, "scope"_sl};

auto a1 = std::async(std::launch::async, c4db_createCollection, db, spec, ERROR_INFO(&err));
auto a2 = std::async(std::launch::async, c4db_createCollection, db2.get(), spec, ERROR_INFO(&err2));
}

CHECK(err.code == 0);
CHECK(err2.code == 0);
}
}

TEST_CASE_METHOD(C4Test, "Database Flag FullSync", "[Database][C]") {
// Ensure that, by default, diskSyncFull is false.
CHECK(!litecore::asInternal(db)->dataFile()->options().diskSyncFull);
Expand Down

0 comments on commit 6c0e685

Please sign in to comment.