use openehr::base::HierObjectId;
use openehr_sqlite::SqliteStore;
use openehr_store::{Store, StoreError, conformance};
use std::path::PathBuf;
use std::sync::{Arc, Barrier};
fn scratch(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"openehr-concurrency-{}-{name}.sqlite3",
std::process::id()
));
let _ = std::fs::remove_file(&p);
p
}
fn container() -> HierObjectId {
HierObjectId::from_uid_str(conformance::RECORD).expect("literal")
}
fn seeded(path: &std::path::Path) {
let mut store = SqliteStore::open(path).expect("open");
store.install().expect("install");
let ehr = conformance::sample_ehr();
store.create_ehr(&ehr).expect("ehr");
store
.create_contribution(ehr.ehr_id(), &conformance::sample_contribution("c1", &[1]))
.expect("contribution");
store
.commit_composition(ehr.ehr_id(), &conformance::sample_version(1, None, 0), "c1")
.expect("version 1");
}
#[test]
fn racing_commits_to_one_position_produce_one_winner() {
const WRITERS: usize = 8;
let path = scratch("race");
seeded(&path);
let barrier = Arc::new(Barrier::new(WRITERS));
let outcomes: Vec<_> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..WRITERS)
.map(|i| {
let path = path.clone();
let barrier = Arc::clone(&barrier);
let minute = u32::try_from(i).expect("writer count fits");
scope.spawn(move || {
let mut store = SqliteStore::open(&path).expect("open");
let ehr_id = container();
let version = conformance::sample_version(2, Some(1), 10 + minute);
barrier.wait();
store.commit_composition(&ehr_id, &version, "c1")
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("thread"))
.collect()
});
let winners = outcomes.iter().filter(|r| r.is_ok()).count();
assert_eq!(
winners, 1,
"exactly one writer may take a position in a version tree; {winners} did"
);
for outcome in &outcomes {
if let Err(error) = outcome {
assert!(
matches!(error, StoreError::Commit(_)),
"a losing writer must be refused by the commit rules, \
not by the engine: {error}"
);
}
}
let store = SqliteStore::open(&path).expect("open");
let all = store.all_versions(&container()).expect("all_versions");
assert_eq!(all.len(), 2, "the version tree gained a duplicate position");
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_reader_never_observes_a_torn_commit() {
const COMMITS: u32 = 24;
let path = scratch("torn");
seeded(&path);
let ehr_id = container();
std::thread::scope(|scope| {
let writer_path = path.clone();
let writer = scope.spawn(move || {
let mut store = SqliteStore::open(&writer_path).expect("open");
let id = container();
for n in 2..=COMMITS {
let version = conformance::sample_version(n, Some(n - 1), n);
store
.commit_composition(&id, &version, "c1")
.unwrap_or_else(|e| panic!("writer failed at version {n}: {e}"));
}
});
let reader_path = path.clone();
let reader = scope.spawn(move || {
let store = SqliteStore::open(&reader_path).expect("open");
let id = container();
for _ in 0..400 {
let Ok(head) = store.latest_version(&id) else {
continue;
};
let indexed = store
.find_compositions_by_archetype(&ehr_id, "openEHR-EHR-COMPOSITION.encounter.v1")
.expect("archetype query");
assert!(
indexed.iter().any(|row| row.version_uid == head.uid),
"read a version ({}) whose index row was not visible — a torn commit",
head.uid
);
}
});
writer.join().expect("writer");
reader.join().expect("reader");
});
let store = SqliteStore::open(&path).expect("open");
let all = store.all_versions(&container()).expect("all_versions");
assert_eq!(all.len(), COMMITS as usize, "a commit was lost");
let _ = std::fs::remove_file(&path);
}