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
12 changes: 9 additions & 3 deletions crates/ropy-core/src/repository/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,17 @@ pub trait KvTree: Send + Sync {
fn remove(&self, key: &[u8]) -> Result<bool, RepositoryError>;

/// Return the number of entries currently visible in the tree.
fn len(&self) -> usize;
///
/// # Errors
/// Returns an error if the backend cannot read the entry count.
fn len(&self) -> Result<usize, RepositoryError>;

/// Whether this tree currently contains no entries.
fn is_empty(&self) -> bool {
self.len() == 0
///
/// # Errors
/// Returns an error if the backend cannot read the entry count.
fn is_empty(&self) -> Result<bool, RepositoryError> {
self.len().map(|count| count == 0)
}

/// Remove all entries from this test tree.
Expand Down
4 changes: 2 additions & 2 deletions crates/ropy-core/src/repository/backend/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,10 @@ impl KvTree for MemoryTreeHandle {
Ok(entries.remove(key).is_some())
}

fn len(&self) -> usize {
fn len(&self) -> Result<usize, RepositoryError> {
let entries_lock = self.tree.entries.read();
let entries = recover_lock(entries_lock);
entries.len()
Ok(entries.len())
}

#[cfg(test)]
Expand Down
23 changes: 18 additions & 5 deletions crates/ropy-core/src/repository/backend/redb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ pub struct RedbTree {
db: Arc<Database>,
name: String,
write_lock: Mutex<()>,
#[cfg(test)]
fail_next_len: std::sync::atomic::AtomicBool,
}

impl RedbTree {
Expand All @@ -199,9 +201,17 @@ impl RedbTree {
db,
name,
write_lock: Mutex::new(()),
#[cfg(test)]
fail_next_len: std::sync::atomic::AtomicBool::new(false),
})
}

#[cfg(test)]
pub(crate) fn fail_next_len(&self) {
self.fail_next_len
.store(true, std::sync::atomic::Ordering::SeqCst);
}

const fn table_definition(name: &str) -> RedbTableDefinition<'_> {
TableDefinition::new(name)
}
Expand Down Expand Up @@ -281,17 +291,20 @@ impl KvTree for RedbTree {
Ok(removed)
}

fn len(&self) -> usize {
fn len(&self) -> Result<usize, RepositoryError> {
self.with_read_table(|table| {
#[cfg(test)]
if self
.fail_next_len
.swap(false, std::sync::atomic::Ordering::SeqCst)
{
return Err(RepositoryError::Query("injected count read failure".into()));
}
table
.len()
.map(|len| usize::try_from(len).unwrap_or(usize::MAX))
.map_err(|error| RepositoryError::Query(error.to_string()))
})
.unwrap_or_else(|error| {
tracing::warn!(error = %error, tree = %self.name, "failed to read tree length");
0
})
}

#[cfg(test)]
Expand Down
54 changes: 44 additions & 10 deletions crates/ropy-core/src/repository/cleanup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl<B: StorageBackend> ClipboardRepository<B> {
/// Returns an error if selecting or deleting ordinary records fails.
pub fn clear_ordinary_records(&self) -> Result<usize, RepositoryError> {
let _operation = self.lock_operation();
let total = self.count();
let total = self.count()?;
let favorite_ids = self.favorite_id_set()?;
let ordinary_total = self.ordinary_record_count(total, &favorite_ids)?;

Expand All @@ -41,7 +41,7 @@ impl<B: StorageBackend> ClipboardRepository<B> {
/// Returns an error if querying retention exemptions or deleting old records fails.
pub fn cleanup_old_records(&self, keep_count: usize) -> Result<usize, RepositoryError> {
let _operation = self.lock_operation();
let total = self.count();
let total = self.count()?;
let favorite_ids = self.favorite_id_set()?;
let ordinary_total = self.ordinary_record_count(total, &favorite_ids)?;
self.cleanup_old_records_with_ordinary_total(
Expand All @@ -64,7 +64,7 @@ impl<B: StorageBackend> ClipboardRepository<B> {
keep_count: usize,
) -> Result<usize, RepositoryError> {
let _operation = self.lock_operation();
let total = self.count();
let total = self.count()?;
let trigger = ClipboardRepository::<RedbBackend>::cleanup_trigger_record_count(keep_count);
// Ordinary records cannot outnumber all records. Avoid reading retention
// exemptions until even the total exceeds the buffered limit.
Expand Down Expand Up @@ -181,6 +181,40 @@ mod tests {
use super::*;
use crate::repository::test_helpers::{create_test_repo, save_numbered_records, save_text_at};

#[rstest]
#[case::clear(0)]
#[case::trim(1)]
#[case::buffered_trim(2)]
fn test_cleanup_count_failure_preserves_history_and_allows_retry(#[case] operation: u8) {
let (_directory, repo) = crate::repository::test_helpers::create_test_repo_with(
crate::repository::backend::redb::redb_backend_factory,
);
let record = repo
.save_rich_text("keep".into(), Some("<b>keep</b>"), None)
.unwrap();
let cleanup = || match operation {
0 => repo.clear_ordinary_records(),
1 => repo.cleanup_old_records(0),
_ => repo.cleanup_old_records_if_needed(0),
};
// Two records exceed the buffered limit, so a successful retry must delete both.
repo.save_text("other".into()).unwrap();
repo.records.fail_next_len();
assert!(matches!(cleanup(), Err(RepositoryError::Query(_))));
assert_eq!(repo.get_display_records(10).unwrap().len(), 2);
assert_eq!(
crate::repository::assets::load_rich_text_html(record.rich_text_meta.as_ref().unwrap())
.as_deref(),
Some("<b>keep</b>"),
);
assert_eq!(cleanup().unwrap(), 2);
assert_eq!(repo.get_display_records(10).unwrap(), []);
assert!(
crate::repository::assets::load_rich_text_html(record.rich_text_meta.as_ref().unwrap())
.is_none()
);
}

#[rstest]
#[case(0, 1)]
#[case(1, 1)]
Expand Down Expand Up @@ -235,7 +269,7 @@ mod tests {
let removed = repo.cleanup_old_records(1).expect("Failed to cleanup");

assert_eq!(removed, 0);
assert_eq!(repo.count(), 3);
assert_eq!(repo.count().expect("count"), 3);
}

#[test]
Expand All @@ -253,7 +287,7 @@ mod tests {
let removed = repo.cleanup_old_records(2).expect("Failed to cleanup");

assert_eq!(removed, 3);
assert_eq!(repo.count(), 4);
assert_eq!(repo.count().expect("count"), 4);
assert!(repo.get_by_id(pinned.id).expect("query").is_some());
assert!(repo.get_by_id(favorited.id).expect("query").is_some());
}
Expand Down Expand Up @@ -283,7 +317,7 @@ mod tests {
.expect("Failed to cleanup");

assert_eq!(removed, 0);
assert_eq!(repo.count(), 11);
assert_eq!(repo.count().expect("count"), 11);
}

#[test]
Expand All @@ -298,7 +332,7 @@ mod tests {
.expect("Failed to cleanup");

assert_eq!(removed, 2);
assert_eq!(repo.count(), 10);
assert_eq!(repo.count().expect("count"), 10);
}

#[test]
Expand All @@ -320,7 +354,7 @@ mod tests {
.expect("Failed to cleanup");

assert_eq!(removed, 0);
assert_eq!(repo.count(), 12);
assert_eq!(repo.count().expect("count"), 12);
}

#[test]
Expand All @@ -330,7 +364,7 @@ mod tests {
let removed = repo.clear_ordinary_records().expect("Failed to clear");

assert_eq!(removed, 0);
assert_eq!(repo.count(), 0);
assert_eq!(repo.count().expect("count"), 0);
}

#[test]
Expand All @@ -341,6 +375,6 @@ mod tests {
let removed = repo.clear_ordinary_records().expect("Failed to clear");

assert_eq!(removed, 5);
assert_eq!(repo.count(), 0);
assert_eq!(repo.count().expect("count"), 0);
}
}
5 changes: 4 additions & 1 deletion crates/ropy-core/src/repository/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,10 @@ impl<B: StorageBackend> ClipboardRepository<B> {
}

/// Return the number of stored records.
pub fn count(&self) -> usize {
///
/// # Errors
/// Returns an error if the backend cannot read the record count.
pub fn count(&self) -> Result<usize, RepositoryError> {
self.records.len()
}
}
Expand Down
34 changes: 17 additions & 17 deletions crates/ropy-core/src/repository/tests/dedup_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn test_dedup_same_content() {
let r2 = save_text_at(&repo, "duplicate", 2);

assert_eq!(r1.id, r2.id);
assert_eq!(repo.count(), 1);
assert_eq!(repo.count().expect("count"), 1);
assert!(r2.created_at > r1.created_at);
}

Expand All @@ -48,7 +48,7 @@ fn test_dedup_aba_pattern() {
save_text_at(&repo, "B", 2);
let a2 = save_text_at(&repo, "A", 3);

assert_eq!(repo.count(), 2);
assert_eq!(repo.count().expect("count"), 2);
let recent = load_display_records(&repo, 2);
assert_eq!(recent[0].content, "A");
assert_eq!(recent[0].created_at, a2.created_at);
Expand All @@ -74,13 +74,13 @@ fn assert_cleanup_old_records_with<B: StorageBackend>(factory: BackendFactory<B>
let (_temp_dir, repo) = create_test_repo_with(factory);

save_numbered_records(&repo, 10);
assert_eq!(repo.count(), 10);
assert_eq!(repo.count().expect("count"), 10);

let removed = repo
.cleanup_old_records(5)
.unwrap_or_else(|error| panic!("Failed to clean up: {error}"));
assert_eq!(removed, 5);
assert_eq!(repo.count(), 5);
assert_eq!(repo.count().expect("count"), 5);

let recent = load_display_records(&repo, 5);
assert_eq!(recent[0].content, "Record 10");
Expand All @@ -105,7 +105,7 @@ fn test_cleanup_old_records_zero_keep() {

let removed = repo.cleanup_old_records(0).expect("Failed to clean up");
assert_eq!(removed, 1);
assert_eq!(repo.count(), 0);
assert_eq!(repo.count().expect("count"), 0);
}

#[test]
Expand All @@ -116,7 +116,7 @@ fn test_cleanup_old_records_greater_than_total() {

let removed = repo.cleanup_old_records(100).expect("Failed to clean up");
assert_eq!(removed, 0);
assert_eq!(repo.count(), 1);
assert_eq!(repo.count().expect("count"), 1);
}

#[test]
Expand All @@ -127,7 +127,7 @@ fn test_cleanup_old_records_equal_to_total() {

let removed = repo.cleanup_old_records(1).expect("Failed to clean up");
assert_eq!(removed, 0);
assert_eq!(repo.count(), 1);
assert_eq!(repo.count().expect("count"), 1);
}

#[rstest]
Expand All @@ -144,7 +144,7 @@ fn test_cleanup_old_records_if_needed_at_threshold_preserves_records(#[case] kee
.unwrap_or_else(|err| panic!("Failed to conditionally clean up: {err}"));

assert_eq!(removed, 0);
assert_eq!(repo.count(), threshold);
assert_eq!(repo.count().expect("count"), threshold);
}

#[rstest]
Expand All @@ -163,7 +163,7 @@ fn test_cleanup_old_records_if_needed_above_threshold_trims_to_keep_count(
.unwrap_or_else(|err| panic!("Failed to conditionally clean up: {err}"));

assert_eq!(removed, threshold + 1 - keep_count);
assert_eq!(repo.count(), keep_count);
assert_eq!(repo.count().expect("count"), keep_count);

let recent = repo
.get_display_records(keep_count)
Expand All @@ -186,7 +186,7 @@ fn test_cleanup_old_records_when_favorited_record_present_counts_only_ordinary_r

let removed = repo.cleanup_old_records(3).expect("Failed to clean up");
assert_eq!(removed, 2);
assert_eq!(repo.count(), 4);
assert_eq!(repo.count().expect("count"), 4);
assert!(
repo.get_by_id(favorite.id)
.expect("Failed to get by id")
Expand Down Expand Up @@ -253,11 +253,11 @@ fn test_delete() {
let record = repo
.save_text("To be deleted".to_string())
.expect("Failed to save");
assert_eq!(repo.count(), 1);
assert_eq!(repo.count().expect("count"), 1);

let deleted = repo.delete(record.id).expect("Failed to delete");
assert!(deleted);
assert_eq!(repo.count(), 0);
assert_eq!(repo.count().expect("count"), 0);

let deleted_again = repo.delete(record.id).expect("Failed to delete");
assert!(!deleted_again);
Expand Down Expand Up @@ -353,10 +353,10 @@ fn test_clear() {
repo.save_text("One".to_string()).expect("Failed to save");
repo.save_text("Two".to_string()).expect("Failed to save");
repo.save_text("Three".to_string()).expect("Failed to save");
assert_eq!(repo.count(), 3);
assert_eq!(repo.count().expect("count"), 3);

repo.clear().expect("Failed to clear");
assert_eq!(repo.count(), 0);
assert_eq!(repo.count().expect("count"), 0);
}

#[test]
Expand Down Expand Up @@ -407,7 +407,7 @@ fn test_clear_ordinary_records_when_special_records_present_preserves_them() {
.expect("Failed to clear ordinary records");

assert_eq!(removed, 2);
assert_eq!(repo.count(), 3);
assert_eq!(repo.count().expect("count"), 3);
assert!(
repo.get_by_id(pinned.id)
.expect("Failed to load pinned")
Expand Down Expand Up @@ -455,7 +455,7 @@ fn test_clear_ordinary_records_when_no_ordinary_records_returns_zero() {
.expect("Failed to clear ordinary records");

assert_eq!(removed, 0);
assert_eq!(repo.count(), 2);
assert_eq!(repo.count().expect("count"), 2);
assert!(
repo.get_by_id(pinned.id)
.expect("Failed to load pinned")
Expand All @@ -473,5 +473,5 @@ fn test_clear_empty_repo() {
let (_temp_dir, repo) = create_test_repo();

repo.clear().expect("Failed to clear empty repo");
assert_eq!(repo.count(), 0);
assert_eq!(repo.count().expect("count"), 0);
}
Loading
Loading