Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion C/tests/c4DatabaseTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,20 @@ N_WAY_TEST_CASE_METHOD(C4DatabaseTest, "Document expiration torture test", "[Dat
int setCount = 1;
C4Error error{};
for ( ; setCount <= total; ++setCount ) {
if ( !c4coll_setDocExpiration(collection, c4str(docID(setCount)), expire, &error) ) {
// CBL-8202: Under this artificial contention (a no-pause reader on another connection
// plus the background purge) setDocExpiration can momentarily fail with a transient
// "busy" error when it can't grab the write lock. That is expected and retryable, so
// retry rather than aborting the loop and leaving documents without an expiration.
constexpr int kMaxBusyRetries = 200;
bool ok = false;
for ( int busyRetries = 0; !ok; ) {
ok = c4coll_setDocExpiration(collection, c4str(docID(setCount)), expire, &error);
if ( ok ) break;
if ( error.domain != LiteCoreDomain || error.code != kC4ErrorBusy || ++busyRetries > kMaxBusyRetries )
break;
std::this_thread::sleep_for(5ms);
}
Comment thread
borrrden marked this conversation as resolved.
if ( !ok ) {
stop.store(true);
break;
}
Expand Down
38 changes: 30 additions & 8 deletions LiteCore/Database/Housekeeper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "SequenceTracker.hh"
#include "BackgroundDB.hh"
#include "DataFile.hh"
#include "Error.hh"
#include "Logging.hh"
#include "SQLiteKeyStore.hh"
#include "StringUtil.hh"
Expand Down Expand Up @@ -98,18 +99,39 @@ namespace litecore {
enqueue(FUNCTION_TO_QUEUE(Housekeeper::_doExpiration));
}

// CBL-8202: How long to wait before retrying expiration after a transient "database busy"
// error. Another connection (or thread) held the write lock while we tried to purge; we just
// need to try again shortly.
static constexpr chrono::milliseconds kRetryExpirationAfterBusyMS{100};

void Housekeeper::_doExpiration() {
if ( _isStopped() ) return;

logInfo("Housekeeper: expiring documents...");
_bgdb->useInTransaction(_keyStoreName, [&](KeyStore& keyStore, SequenceTracker* sequenceTracker) -> bool {
if ( sequenceTracker ) {
keyStore.expireRecords([&](slice docID) { sequenceTracker->documentPurged(docID); });
} else {
keyStore.expireRecords();
logVerbose("Housekeeper: expiring documents...");
try {
_bgdb->useInTransaction(_keyStoreName, [&](KeyStore& keyStore, SequenceTracker* sequenceTracker) -> bool {
if ( sequenceTracker ) {
keyStore.expireRecords([&](slice docID) { sequenceTracker->documentPurged(docID); });
} else {
keyStore.expireRecords();
}
return true;
});
} catch ( const exception& x ) {
// CBL-8202: A transient lock conflict (another connection holding the write lock) must
// not abort the Housekeeper. If we let the exception propagate, the actor swallows it
// and the _scheduleExpiration() below is skipped, so the timer never fires again and
// expired documents are never purged once the app stops calling setDocExpiration.
// Instead, reschedule a near-term retry of the purge.
error e = error::convertException(x).standardized();
if ( e.domain == error::LiteCore && e.code == error::Busy ) {
logVerbose("Housekeeper: expiration deferred (database busy); retrying in %lldms",
(long long)kRetryExpirationAfterBusyMS.count());
_expiryTimer->fireAfter(kRetryExpirationAfterBusyMS);
return;
}
return true;
});
throw;
}

_scheduleExpiration(false);
}
Expand Down
Loading