Skip to content

Commit be346a1

Browse files
jordanhunt22Convex, Inc.
authored and
Convex, Inc.
committed
[Retention] Improvements to document retention functionality (#25336)
@ldanilek and I discussed many improvements to the way document retention works. Namely: - making the cursor follow the scanned documents not the deleted documents - removing the selecting of documents before we delete them - bounding the number of documents we scan, not the number of documents we delete This PR includes the first two changes, and the third will be done in a follow-up. GitOrigin-RevId: 620e3a4b33abeb0849b72acf151964ad40751a61
1 parent 6a40149 commit be346a1

File tree

5 files changed

+28
-90
lines changed

5 files changed

+28
-90
lines changed

crates/common/src/persistence.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,6 @@ pub trait Persistence: Sync + Send + 'static {
196196
) -> anyhow::Result<Vec<IndexEntry>>;
197197
async fn delete_index_entries(&self, entries: Vec<IndexEntry>) -> anyhow::Result<usize>;
198198

199-
// Retrieves expired documents
200-
async fn documents_to_delete(
201-
&self,
202-
expired_documents: &Vec<(Timestamp, InternalDocumentId)>,
203-
) -> anyhow::Result<Vec<(Timestamp, InternalDocumentId)>>;
204-
205199
// Deletes documents
206200
async fn delete(
207201
&self,

crates/common/src/testing/persistence_test_suite.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,10 +1570,7 @@ pub async fn persistence_delete_documents<P: Persistence>(p: Arc<P>) -> anyhow::
15701570
.map(|(ts, id, _)| (*ts, *id))
15711571
.collect_vec();
15721572

1573-
let expired_docs = p.documents_to_delete(&docs_to_delete).await?;
1574-
assert_eq!(docs_to_delete, expired_docs);
1575-
1576-
assert_eq!(p.delete(expired_docs).await?, 3);
1573+
assert_eq!(p.delete(docs_to_delete).await?, 3);
15771574

15781575
let stream = reader.load_all_documents();
15791576
pin_mut!(stream);

crates/common/src/testing/test_persistence.rs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -220,21 +220,6 @@ impl Persistence for TestPersistence {
220220
Ok(total_deleted)
221221
}
222222

223-
async fn documents_to_delete(
224-
&self,
225-
expired_documents: &Vec<(Timestamp, InternalDocumentId)>,
226-
) -> anyhow::Result<Vec<(Timestamp, InternalDocumentId)>> {
227-
let inner = self.inner.lock();
228-
let log = &inner.log;
229-
let mut new_expired_rows = Vec::new();
230-
for expired_doc in expired_documents {
231-
if log.get(expired_doc).is_some() {
232-
new_expired_rows.push(*expired_doc);
233-
}
234-
}
235-
Ok(new_expired_rows)
236-
}
237-
238223
async fn delete(
239224
&self,
240225
documents: Vec<(Timestamp, InternalDocumentId)>,

crates/database/src/retention.rs

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,10 @@ impl<RT: Runtime> LeaderRetentionManager<RT> {
703703
Ok(())
704704
}
705705

706-
#[try_stream(ok = (Timestamp, InternalDocumentId), error = anyhow::Error)]
706+
/// Finds expired documents in the documents log and returns a tuple of the
707+
/// form (scanned_document_ts, (expired_document_ts,
708+
/// internal_document_ts))
709+
#[try_stream(ok = (Timestamp, (Timestamp, InternalDocumentId)), error = anyhow::Error)]
707710
async fn expired_documents(
708711
rt: &RT,
709712
reader: RepeatablePersistence,
@@ -724,7 +727,8 @@ impl<RT: Runtime> LeaderRetentionManager<RT> {
724727
.try_chunks2(*RETENTION_READ_CHUNK)
725728
.map(move |chunk| async move {
726729
let chunk = chunk?.to_vec();
727-
let mut entries_to_delete: Vec<(Timestamp, InternalDocumentId)> = vec![];
730+
let mut entries_to_delete: Vec<(Timestamp, (Timestamp, InternalDocumentId))> =
731+
vec![];
728732
// Prev revs are the documents we are deleting.
729733
// Each prev rev has 1 or 2 entries to delete per document -- one entry at
730734
// the prev rev's ts, and a tombstone at the current rev's ts if
@@ -752,7 +756,7 @@ impl<RT: Runtime> LeaderRetentionManager<RT> {
752756
the retention window"
753757
);
754758

755-
entries_to_delete.push((ts, id));
759+
entries_to_delete.push((ts, (ts, id)));
756760
}
757761
log_document_retention_scanned_document(maybe_doc.is_none(), false);
758762
continue;
@@ -775,11 +779,11 @@ impl<RT: Runtime> LeaderRetentionManager<RT> {
775779
the retention window"
776780
);
777781

778-
entries_to_delete.push((*prev_rev_ts, id));
782+
entries_to_delete.push((ts, (*prev_rev_ts, id)));
779783

780784
// Deletes tombstones
781785
if maybe_doc.is_none() {
782-
entries_to_delete.push((ts, id));
786+
entries_to_delete.push((ts, (ts, id)));
783787
}
784788

785789
log_document_retention_scanned_document(maybe_doc.is_none(), true);
@@ -889,8 +893,8 @@ impl<RT: Runtime> LeaderRetentionManager<RT> {
889893
/// Partitions documents into RETENTION_DELETE_PARALLEL parts where each
890894
/// document id only exists in one part
891895
fn partition_document_chunk(
892-
to_partition: Vec<(Timestamp, InternalDocumentId)>,
893-
) -> Vec<Vec<(Timestamp, InternalDocumentId)>> {
896+
to_partition: Vec<(Timestamp, (Timestamp, InternalDocumentId))>,
897+
) -> Vec<Vec<(Timestamp, (Timestamp, InternalDocumentId))>> {
894898
let mut parts = Vec::new();
895899
for _ in 0..*RETENTION_DELETE_PARALLEL {
896900
parts.push(vec![]);
@@ -948,7 +952,7 @@ impl<RT: Runtime> LeaderRetentionManager<RT> {
948952
}
949953

950954
async fn delete_document_chunk(
951-
delete_chunk: Vec<(Timestamp, InternalDocumentId)>,
955+
delete_chunk: Vec<(Timestamp, (Timestamp, InternalDocumentId))>,
952956
persistence: Arc<dyn Persistence>,
953957
mut new_cursor: Timestamp,
954958
) -> anyhow::Result<(Timestamp, usize)> {
@@ -957,34 +961,21 @@ impl<RT: Runtime> LeaderRetentionManager<RT> {
957961
return Ok((new_cursor, delete_chunk.len()));
958962
}
959963
let _timer = retention_delete_document_chunk_timer();
960-
let delete_chunk = delete_chunk.to_vec();
961-
let documents_to_delete = persistence.documents_to_delete(&delete_chunk).await?;
962-
let total_documents_to_delete = documents_to_delete.len();
963-
tracing::trace!("delete_documents: got documents to delete {total_documents_to_delete:?}");
964-
// If there are more entries to delete than we see in the delete chunk,
965-
// it means retention skipped deleting entries before, and we
966-
// incorrectly bumped DocumentRetentionConfirmedDeletedTimestamp anyway.
967-
if documents_to_delete.len() > delete_chunk.len() {
968-
report_error(&mut anyhow::anyhow!(
969-
"retention wanted to delete {} documents but found {total_documents_to_delete} to \
970-
delete",
971-
delete_chunk.len(),
972-
));
973-
anyhow::bail!(
974-
"Retention wanted to delete {} documents but found {total_documents_to_delete} to
975-
delete. Likely DocumentRetentionConfirmedDeletedTimestamp was bumped incorrectly",
976-
delete_chunk.len()
977-
)
978-
}
979-
for document_to_delete in documents_to_delete.iter() {
980-
// If we're deleting a document, we've definitely deleted
981-
// entries for documents at all prior timestamps.
964+
let total_documents_to_delete = delete_chunk.len();
965+
tracing::trace!(
966+
"delete_documents: there are {total_documents_to_delete:?} documents to delete"
967+
);
968+
for document_to_delete in delete_chunk.iter() {
969+
// If we're deleting the previous revision of a document, we've definitely
970+
// deleted entries for documents at all prior timestamps.
982971
if document_to_delete.0 > Timestamp::MIN {
983972
new_cursor = cmp::max(new_cursor, document_to_delete.0.pred()?);
984973
}
985974
}
986975
let deleted_rows = if total_documents_to_delete > 0 {
987-
persistence.delete(documents_to_delete).await?
976+
persistence
977+
.delete(delete_chunk.into_iter().map(|doc| doc.1).collect())
978+
.await?
988979
} else {
989980
0
990981
};
@@ -1811,7 +1802,11 @@ mod tests {
18111802
let expired: Vec<_> = expired_stream.try_collect().await?;
18121803

18131804
assert_eq!(expired.len(), 5);
1814-
assert_eq!(p.delete(expired).await?, 5);
1805+
assert_eq!(
1806+
p.delete(expired.into_iter().map(|doc| doc.1).collect())
1807+
.await?,
1808+
5
1809+
);
18151810

18161811
let reader = p.reader();
18171812

crates/sqlite/src/lib.rs

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -433,35 +433,6 @@ impl Persistence for SqlitePersistence {
433433
Ok(count_deleted)
434434
}
435435

436-
async fn documents_to_delete(
437-
&self,
438-
expired_documents: &Vec<(Timestamp, InternalDocumentId)>,
439-
) -> anyhow::Result<Vec<(Timestamp, InternalDocumentId)>> {
440-
let connection = &self.inner.lock().connection;
441-
let mut all_entries = BTreeSet::new();
442-
for expired_entry in expired_documents {
443-
let table_id: &TableId = expired_entry.1.table();
444-
let id = expired_entry.1.internal_id();
445-
let params = params![&table_id.0[..], &id[..], &u64::from(expired_entry.0),];
446-
let mut entries_query = connection.prepare(DOCUMENTS_TO_DELETE)?;
447-
let row_iter = entries_query.query_map(params, |row| {
448-
let table_id: Vec<u8> = row.get(0)?;
449-
let id: Vec<u8> = row.get(1)?;
450-
let ts =
451-
Timestamp::try_from(row.get::<_, u64>(2)?).expect("timestamp out of bounds");
452-
Ok((table_id, id, ts))
453-
})?;
454-
for row in row_iter {
455-
let (table_id, id, ts) = row?;
456-
all_entries.insert((
457-
ts,
458-
InternalDocumentId::new(TableId(table_id.try_into()?), id.try_into()?),
459-
));
460-
}
461-
}
462-
Ok(all_entries.into_iter().collect())
463-
}
464-
465436
async fn delete(
466437
&self,
467438
documents: Vec<(Timestamp, InternalDocumentId)>,
@@ -699,10 +670,6 @@ ORDER BY index_id DESC, key DESC, ts DESC
699670
"#;
700671
const DELETE_INDEX: &str = "DELETE FROM indexes WHERE index_id = ? AND ts <= ? AND key = ?";
701672

702-
const DOCUMENTS_TO_DELETE: &str = r#"SELECT table_id, id, ts
703-
FROM documents WHERE table_id = ? AND id = ? AND ts <= ?
704-
ORDER BY table_id DESC, id DESC, ts DESC
705-
"#;
706673
const DELETE_DOCUMENT: &str = "DELETE FROM documents WHERE table_id = ? AND id = ? AND ts = ?";
707674

708675
const CHECK_IS_READ_ONLY: &str = "SELECT 1 FROM read_only LIMIT 1";

0 commit comments

Comments
 (0)