use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use crate::runtime::sync::{
MutexInfo, PriorityInheritanceMutex, PriorityInheritanceMutexGuard, mutex_report,
};
use super::PvDatabase;
struct LockSet {
id: u64,
lock: PriorityInheritanceMutex<()>,
mutex_seq: u64,
owner: AtomicU64,
depth: AtomicUsize,
many_holds: AtomicUsize,
}
type Set = &'static LockSet;
const FIRST_SET_ID: u64 = 2;
static SET_MUTEX_SEQ: std::sync::Mutex<u64> = std::sync::Mutex::new(0);
fn thread_key() -> u64 {
static NEXT: AtomicU64 = AtomicU64::new(1);
thread_local! {
static KEY: u64 = NEXT.fetch_add(1, Ordering::Relaxed);
}
KEY.with(|k| *k)
}
impl LockSet {
fn acquire(&'static self, many: bool) -> SetGuard {
let me = thread_key();
if self.owner.load(Ordering::Acquire) == me {
self.depth.fetch_add(1, Ordering::Relaxed);
if many {
self.many_holds.fetch_add(1, Ordering::Relaxed);
}
return SetGuard {
set: self,
guard: None,
many,
};
}
let guard = self.lock.lock();
self.owner.store(me, Ordering::Release);
self.depth.store(1, Ordering::Relaxed);
if many {
self.many_holds.fetch_add(1, Ordering::Relaxed);
}
SetGuard {
set: self,
guard: Some(guard),
many,
}
}
fn is_locked(&self) -> bool {
self.lock.try_lock().is_none()
}
}
struct SetGuard {
set: Set,
guard: Option<PriorityInheritanceMutexGuard<'static, ()>>,
many: bool,
}
impl Drop for SetGuard {
fn drop(&mut self) {
if self.many {
self.set.many_holds.fetch_sub(1, Ordering::Relaxed);
}
if self.guard.is_some() {
self.set.depth.store(0, Ordering::Relaxed);
self.set.owner.store(0, Ordering::Release);
} else {
self.set.depth.fetch_sub(1, Ordering::Relaxed);
}
}
}
struct Registry {
active: BTreeMap<u64, SetState>,
free: VecDeque<Set>,
of_record: HashMap<String, u64>,
next_id: u64,
}
struct SetState {
set: Set,
members: BTreeSet<String>,
}
impl Default for Registry {
fn default() -> Self {
Self {
active: BTreeMap::new(),
free: VecDeque::new(),
of_record: HashMap::new(),
next_id: FIRST_SET_ID - 1,
}
}
}
impl Registry {
fn make_set(&mut self) -> Set {
if let Some(set) = self.free.pop_front() {
debug_assert_eq!(
set.many_holds.load(Ordering::Relaxed),
0,
"C asserts refcount==0 for a set on the free list (dbLock.c:571)"
);
return set;
}
self.next_id += 1;
let id = self.next_id;
let mut seq = SET_MUTEX_SEQ.lock().unwrap_or_else(|e| e.into_inner());
let mutex_seq = *seq;
*seq += 1;
let set: Set = Box::leak(Box::new(LockSet {
id,
lock: PriorityInheritanceMutex::new(()),
mutex_seq,
owner: AtomicU64::new(0),
depth: AtomicUsize::new(0),
many_holds: AtomicUsize::new(0),
}));
drop(seq);
set
}
fn set_of(&mut self, record: &str) -> Set {
if let Some(id) = self.of_record.get(record) {
return self.active[id].set;
}
let set = self.make_set();
self.of_record.insert(record.to_string(), set.id);
self.active.insert(
set.id,
SetState {
set,
members: BTreeSet::from([record.to_string()]),
},
);
set
}
fn merge(&mut self, first: &str, second: &str) {
let a = self.set_of(first).id;
let b = self.set_of(second).id;
if a == b {
return;
}
let moved = self
.active
.remove(&b)
.expect("every id in of_record names an active set");
for name in &moved.members {
self.of_record.insert(name.clone(), a);
}
let target = self
.active
.get_mut(&a)
.expect("every id in of_record names an active set");
target.members.extend(moved.members);
self.free.push_back(moved.set);
}
fn repartition(&mut self, seed: &str, adjacency: &HashMap<String, BTreeSet<String>>) {
let seed_id = if adjacency.contains_key(seed) {
self.set_of(seed).id
} else {
let Some(id) = self.of_record.get(seed).copied() else {
return;
};
id
};
let mut affected: BTreeSet<String> = BTreeSet::new();
let mut work: Vec<String> = vec![seed.to_string()];
while let Some(name) = work.pop() {
if !affected.insert(name.clone()) {
continue;
}
if let Some(targets) = adjacency.get(&name) {
work.extend(targets.iter().cloned());
}
if let Some(id) = self.of_record.get(&name).copied() {
work.extend(self.active[&id].members.iter().cloned());
}
}
let touched: BTreeSet<u64> = affected
.iter()
.filter_map(|name| self.of_record.get(name).copied())
.collect();
for name in &affected {
if !adjacency.contains_key(name) {
self.of_record.remove(name);
}
}
let seed_present = adjacency.contains_key(seed);
let mut components: Vec<BTreeSet<String>> = Vec::new();
let mut placed: BTreeSet<String> = BTreeSet::new();
let starts = seed_present
.then(|| seed.to_string())
.into_iter()
.chain(affected.iter().cloned());
for start in starts {
if placed.contains(&start) || !adjacency.contains_key(&start) {
continue;
}
let mut component: BTreeSet<String> = BTreeSet::new();
let mut walk = vec![start];
while let Some(name) = walk.pop() {
if !component.insert(name.clone()) {
continue;
}
if let Some(targets) = adjacency.get(&name) {
walk.extend(targets.iter().cloned());
}
}
placed.extend(component.iter().cloned());
components.push(component);
}
let mut keeps: BTreeSet<u64> = BTreeSet::new();
if seed_present {
keeps.insert(seed_id);
}
let mut assigned: Vec<Option<u64>> = Vec::with_capacity(components.len());
for component in &components {
if seed_present && component.contains(seed) {
assigned.push(Some(seed_id));
continue;
}
let mut ids = component
.iter()
.map(|name| self.of_record.get(name).copied());
let first = ids.next().flatten();
let uniform =
first.filter(|id| !keeps.contains(id) && ids.all(|other| other == Some(*id)));
if let Some(id) = uniform {
keeps.insert(id);
}
assigned.push(uniform);
}
let fresh: Vec<Set> = assigned
.iter()
.filter(|id| id.is_none())
.map(|_| self.make_set())
.collect();
let mut fresh = fresh.into_iter();
for (component, id) in components.into_iter().zip(assigned) {
let set = match id {
Some(id) => self.active[&id].set,
None => fresh
.next()
.expect("one fresh set per unassigned component"),
};
for name in &component {
self.of_record.insert(name.clone(), set.id);
}
self.active.insert(
set.id,
SetState {
set,
members: component,
},
);
}
for id in touched.difference(&keeps) {
let dropped = self
.active
.remove(id)
.expect("every touched id named an active set");
self.free.push_back(dropped.set);
}
}
fn init_records(&mut self, names: &[String]) {
for name in names {
self.set_of(name);
}
}
fn info(&self, id: u64, rows: &HashMap<u64, MutexInfo>) -> LockSetInfo {
let state = &self.active[&id];
LockSetInfo {
id,
members: state.members.iter().cloned().collect(),
refs: state.members.len() + state.set.many_holds.load(Ordering::Relaxed),
locked: state.set.is_locked(),
mutex: rows.get(&state.set.mutex_seq).cloned(),
}
}
}
fn lock_set_mutex_rows() -> HashMap<u64, MutexInfo> {
mutex_report(false)
.shown
.into_iter()
.filter(|info| info.file() == file!())
.enumerate()
.map(|(seq, info)| (seq as u64, info))
.collect()
}
pub struct LockSetInfo {
pub id: u64,
pub members: Vec<String>,
pub refs: usize,
pub locked: bool,
pub mutex: Option<MutexInfo>,
}
pub struct LockSetReport {
pub active: Vec<LockSetInfo>,
pub free: usize,
}
#[derive(Default)]
pub(crate) struct RecordLockRegistry {
inner: std::sync::Mutex<Registry>,
}
impl RecordLockRegistry {
fn lock(&self) -> std::sync::MutexGuard<'_, Registry> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn set_of(&self, record: &str) -> Set {
self.lock().set_of(record)
}
fn set_id_of(&self, record: &str) -> Option<u64> {
self.lock().of_record.get(record).copied()
}
}
impl PvDatabase {
pub fn build_lock_sets(&self) {
let mut names: Vec<String> = self.inner.records.read().keys().cloned().collect();
names.sort();
let mut edges: Vec<(String, String)> = Vec::new();
for name in &names {
for target in self.db_link_targets(name) {
edges.push((name.clone(), target));
}
}
let mut registry = self.inner.record_locks.lock();
registry.init_records(&names);
for (from, to) in edges {
registry.merge(&from, &to);
}
}
fn db_link_targets(&self, record: &str) -> Vec<String> {
use crate::server::record::ParsedLink;
self.record_link_fields(record)
.into_iter()
.filter_map(|(_, _, parsed)| match parsed {
ParsedLink::Db(link) => {
let name = self
.resolve_alias(&link.target().record)
.unwrap_or_else(|| link.target().record);
self.get_record_no_resolve(&name).map(|_| name)
}
_ => None,
})
.collect()
}
pub fn lock_set_report(&self) -> LockSetReport {
let rows = lock_set_mutex_rows();
let registry = self.inner.record_locks.lock();
LockSetReport {
active: registry
.active
.keys()
.map(|id| registry.info(*id, &rows))
.collect(),
free: registry.free.len(),
}
}
pub fn lock_set_of(&self, record: &str) -> Option<LockSetInfo> {
let canonical = self
.resolve_alias(record)
.unwrap_or_else(|| record.to_string());
let rows = lock_set_mutex_rows();
let registry = self.inner.record_locks.lock();
let id = registry.of_record.get(&canonical).copied()?;
Some(registry.info(id, &rows))
}
pub(crate) fn link_field_write<'a>(
&'a self,
record: &str,
field: &str,
) -> Option<LockSetEdit<'a>> {
let canonical = self
.resolve_alias(record)
.unwrap_or_else(|| record.to_string());
if !self.is_dbf_link_field(&canonical, field) {
return None;
}
if self.inner.record_locks.lock().of_record.is_empty() {
return None;
}
Some(LockSetEdit {
db: self,
record: canonical,
})
}
pub(crate) fn lock_set_membership_change<'a>(
&'a self,
record: &str,
) -> Option<LockSetEdit<'a>> {
if self.inner.record_locks.lock().of_record.is_empty() {
return None;
}
Some(LockSetEdit {
db: self,
record: self
.resolve_alias(record)
.unwrap_or_else(|| record.to_string()),
})
}
fn relink_lock_sets(&self, record: &str) {
let adjacency = self.db_link_adjacency();
let mut registry = self.inner.record_locks.lock();
registry.repartition(record, &adjacency);
}
fn db_link_adjacency(&self) -> HashMap<String, BTreeSet<String>> {
let names: Vec<String> = self.inner.records.read().keys().cloned().collect();
let mut adjacency: HashMap<String, BTreeSet<String>> = HashMap::new();
for name in &names {
adjacency.entry(name.clone()).or_default();
}
for name in &names {
for target in self.db_link_targets(name) {
adjacency
.entry(name.clone())
.or_default()
.insert(target.clone());
adjacency.entry(target).or_default().insert(name.clone());
}
}
adjacency
}
}
#[must_use = "the lock-set graph is only re-derived when this is dropped; binding it to `_` drops it immediately and relinks too early"]
pub(crate) struct LockSetEdit<'a> {
db: &'a PvDatabase,
record: String,
}
impl Drop for LockSetEdit<'_> {
fn drop(&mut self) {
self.db.relink_lock_sets(&self.record);
}
}
#[must_use = "the lock set is released as soon as the guard is dropped"]
pub struct RecordWriteGuard {
_guard: SetGuard,
}
#[must_use = "the locked epoch ends as soon as the guard is dropped"]
pub struct ManyRecordWriteGuard {
_guards: Vec<SetGuard>,
}
impl PvDatabase {
pub fn lock_record(&self, record: &str) -> RecordWriteGuard {
let canonical = self
.resolve_alias(record)
.unwrap_or_else(|| record.to_string());
loop {
let set = self.inner.record_locks.set_of(&canonical);
let guard = set.acquire(false);
if self.inner.record_locks.set_id_of(&canonical) == Some(set.id) {
return RecordWriteGuard { _guard: guard };
}
drop(guard);
}
}
pub fn lock_records<I, S>(&self, records: I) -> ManyRecordWriteGuard
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let names: Vec<String> = records
.into_iter()
.map(|record| {
let record = record.as_ref();
self.resolve_alias(record)
.unwrap_or_else(|| record.to_string())
})
.collect();
loop {
let mut sets: Vec<Set> = names
.iter()
.map(|name| self.inner.record_locks.set_of(name))
.collect();
sets.sort_unstable_by_key(|set| set.id);
sets.dedup_by_key(|set| set.id);
let guards: Vec<SetGuard> = sets.iter().map(|set| set.acquire(true)).collect();
let held: BTreeSet<u64> = sets.iter().map(|set| set.id).collect();
if names
.iter()
.all(|name| matches!(self.inner.record_locks.set_id_of(name), Some(id) if held.contains(&id)))
{
return ManyRecordWriteGuard { _guards: guards };
}
drop(guards);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
const SETTLE: Duration = Duration::from_millis(50);
#[test]
fn lock_record_excludes_same_record() {
let db = PvDatabase::new();
let order = Arc::new(AtomicUsize::new(0));
let g = db.lock_record("ai:1");
let db2 = db.clone();
let order2 = order.clone();
let h = std::thread::spawn(move || {
let _g2 = db2.lock_record("ai:1");
order2.fetch_add(10, Ordering::SeqCst);
});
std::thread::sleep(SETTLE);
assert_eq!(order.load(Ordering::SeqCst), 0);
order.fetch_add(1, Ordering::SeqCst);
drop(g);
h.join().unwrap();
assert_eq!(order.load(Ordering::SeqCst), 11);
}
#[test]
fn lock_records_excludes_single_member_write() {
let db = PvDatabase::new();
let many = db.lock_records(["g:a", "g:b", "g:c"]);
let db2 = db.clone();
let acquired = Arc::new(AtomicUsize::new(0));
let acquired2 = acquired.clone();
let h = std::thread::spawn(move || {
let _g = db2.lock_record("g:b");
acquired2.store(1, Ordering::SeqCst);
});
std::thread::sleep(SETTLE);
assert_eq!(
acquired.load(Ordering::SeqCst),
0,
"single-member write must block while ManyRecordWriteGuard is held"
);
drop(many);
h.join().unwrap();
assert_eq!(acquired.load(Ordering::SeqCst), 1);
}
#[test]
fn lock_records_overlapping_sets_no_deadlock() {
let db = PvDatabase::new();
let done = Arc::new(AtomicUsize::new(0));
let handles: Vec<_> = [["x", "y", "z"], ["z", "y", "x"]]
.into_iter()
.map(|set| {
let db = db.clone();
let done = done.clone();
std::thread::spawn(move || {
for _ in 0..500 {
let _g = db.lock_records(set);
std::thread::yield_now();
}
done.fetch_add(1, Ordering::SeqCst);
})
})
.collect();
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while done.load(Ordering::SeqCst) < 2 {
assert!(
std::time::Instant::now() < deadline,
"overlapping lock_records sets must not deadlock"
);
std::thread::sleep(Duration::from_millis(10));
}
for h in handles {
h.join().unwrap();
}
}
#[test]
fn overlapping_epochs_are_mutually_exclusive_and_deadlock_free() {
let db = PvDatabase::new();
let a = vec!["RECA".to_string(), "RECB".to_string()];
let b = vec!["RECB".to_string(), "RECC".to_string()];
let guard_a = db.lock_records(&a);
let db2 = db.clone();
let entered = Arc::new(AtomicUsize::new(0));
let entered2 = entered.clone();
let handle = std::thread::spawn(move || {
let _guard_b = db2.lock_records(&b);
entered2.store(1, Ordering::SeqCst);
});
std::thread::sleep(SETTLE);
assert_eq!(
entered.load(Ordering::SeqCst),
0,
"epoch B must block on shared RECB"
);
drop(guard_a);
handle.join().expect("epoch B thread");
assert_eq!(entered.load(Ordering::SeqCst), 1);
}
#[test]
fn disjoint_epochs_do_not_block_each_other() {
let db = PvDatabase::new();
let _g1 = db.lock_records(&["X1".to_string()]);
let _g2 = db.lock_records(&["X2".to_string()]);
}
#[test]
fn a_name_reaches_one_set_and_two_unlinked_records_reach_two() {
let db = PvDatabase::new();
let registry = &db.inner.record_locks;
assert!(
std::ptr::eq(registry.set_of("REC:A"), registry.set_of("REC:A")),
"the same canonical name must map to the same lock set"
);
assert!(
!std::ptr::eq(registry.set_of("REC:A"), registry.set_of("REC:B")),
"records no link joins must not share a lock set"
);
}
#[test]
fn a_second_overlapping_epoch_on_one_thread_recurses() {
let db = PvDatabase::new();
let _epoch = db.lock_records(&["RE:A".to_string(), "RE:B".to_string()]);
let _overlapping = db.lock_records(&["RE:B".to_string(), "RE:C".to_string()]);
}
#[test]
fn re_taking_one_record_s_set_recurses_like_db_scan_lock() {
let db = PvDatabase::new();
let _held = db.lock_record("RE:SELF");
let _again = db.lock_record("RE:SELF");
}
#[test]
fn recursion_does_not_let_a_second_thread_in() {
let db = PvDatabase::new();
let outer = db.lock_record("RE:DEPTH");
let inner = db.lock_record("RE:DEPTH");
let db2 = db.clone();
let entered = Arc::new(AtomicUsize::new(0));
let entered2 = entered.clone();
let h = std::thread::spawn(move || {
let _g = db2.lock_record("RE:DEPTH");
entered2.store(1, Ordering::SeqCst);
});
std::thread::sleep(SETTLE);
assert_eq!(entered.load(Ordering::SeqCst), 0, "outer level still held");
drop(inner);
std::thread::sleep(SETTLE);
assert_eq!(
entered.load(Ordering::SeqCst),
0,
"one release of two must not hand the set over"
);
drop(outer);
h.join().unwrap();
assert_eq!(entered.load(Ordering::SeqCst), 1);
}
#[test]
fn this_file_creates_no_mutex_but_lock_sets() {
let db = PvDatabase::new();
for name in ["MS:1", "MS:2", "MS:3"] {
drop(db.lock_record(name));
}
let made = *SET_MUTEX_SEQ.lock().unwrap();
assert_eq!(
lock_set_mutex_rows().len() as u64,
made,
"a second mutex created in this file would shift every set's row"
);
for set in db.lock_set_report().active {
assert!(set.mutex.is_some(), "set {} has no row", set.id);
}
}
#[test]
fn the_set_is_released_when_the_guard_drops() {
let db = PvDatabase::new();
drop(db.lock_record("RE:SEQ"));
drop(db.lock_record("RE:SEQ"));
drop(db.lock_records(&["RE:SEQ".to_string()]));
let _a = db.lock_record("RE:ONE");
let _b = db.lock_record("RE:TWO");
}
}