use std::collections::BTreeSet;
use std::sync::Arc;
use crate::server::record::{PiniMode, RecordCell, ScanList, ScanType};
use super::PvDatabase;
struct ScanBucket {
revision: std::sync::atomic::AtomicU64,
entries: crate::runtime::sync::PriorityInheritanceMutex<Bucket>,
}
struct Bucket {
keys: BTreeSet<super::ScanKey>,
ordered: Option<Arc<[super::ScanKey]>>,
}
impl ScanBucket {
fn new() -> Self {
Self {
revision: std::sync::atomic::AtomicU64::new(0),
entries: crate::runtime::sync::PriorityInheritanceMutex::new(Bucket {
keys: BTreeSet::new(),
ordered: None,
}),
}
}
fn transition(&self, f: impl FnOnce(&mut BTreeSet<super::ScanKey>)) {
let mut entries = self.entries.lock();
f(&mut entries.keys);
entries.ordered = None;
self.revision
.fetch_add(1, std::sync::atomic::Ordering::Release);
}
fn snapshot(&self) -> (u64, Arc<[super::ScanKey]>) {
let mut entries = self.entries.lock();
if entries.ordered.is_none() {
let ordered: Arc<[super::ScanKey]> = entries.keys.iter().cloned().collect();
entries.ordered = Some(ordered);
}
(
self.revision.load(std::sync::atomic::Ordering::Relaxed),
entries.ordered.clone().expect("just materialised"),
)
}
}
pub(super) struct ScanIndex {
buckets: std::sync::OnceLock<Box<[ScanBucket]>>,
overruns: std::sync::OnceLock<Box<[std::sync::atomic::AtomicU64]>>,
}
impl ScanIndex {
pub(super) fn new() -> Self {
Self {
buckets: std::sync::OnceLock::new(),
overruns: std::sync::OnceLock::new(),
}
}
fn bucket(&self, list: ScanList) -> &ScanBucket {
&self
.buckets
.get_or_init(|| (0..ScanList::count()).map(|_| ScanBucket::new()).collect())
.as_ref()[list.slot()]
}
fn overrun(&self, list: ScanList) -> &std::sync::atomic::AtomicU64 {
&self
.overruns
.get_or_init(|| {
(0..ScanList::count())
.map(|_| std::sync::atomic::AtomicU64::new(0))
.collect()
})
.as_ref()[list.slot()]
}
}
impl PvDatabase {
pub(super) fn add_to_scan_list(
&self,
scan: ScanType,
phas: i16,
record_type: &str,
load_order: u64,
name: &str,
) {
let Some(list) = scan.scan_list() else {
return;
};
let handle = self
.get_record_no_resolve(name)
.map(|rec| std::sync::Arc::downgrade(&rec))
.unwrap_or_default();
self.inner.scan_index.bucket(list).transition(|keys| {
keys.insert(super::ScanKey::new(
phas,
record_type,
load_order,
name,
handle,
));
});
}
pub(super) fn delete_from_scan_list(&self, scan: ScanType, name: &str) {
let Some(list) = scan.scan_list() else {
return;
};
self.inner
.scan_index
.bucket(list)
.transition(|keys| keys.retain(|k| k.name.as_ref() != name));
}
pub fn update_scan_index(
&self,
name: &str,
old_scan: ScanType,
_new_scan: ScanType,
old_phas: i16,
_new_phas: i16,
) {
let _ = old_phas; let (rec_arc, _record_gate, _gate) = loop {
let rec_arc = self.inner.records.read().get(name).cloned();
let record_gate = rec_arc.as_ref().map(|rec| self.lock_instance(rec));
let gate = self.lock_registration("update_scan_index");
let live = self.inner.records.read().get(name).cloned();
match (&rec_arc, &live) {
(Some(locked), Some(live)) if Arc::ptr_eq(locked, live) => {}
(None, None) => {}
_ => continue,
}
break (rec_arc, record_gate, gate);
};
self.delete_from_scan_list(old_scan, name);
let Some(rec_arc) = rec_arc else {
return;
};
let (cur_scan, cur_phas, cur_type) = {
let inst = rec_arc.read();
(
inst.common.scan,
inst.common.phas,
inst.record.record_type(),
)
};
let seq = self.inner.load_order.load().get(name).copied().unwrap_or(0);
self.add_to_scan_list(cur_scan, cur_phas, cur_type, seq, name);
}
pub(crate) fn record_scan_overrun(&self, scan: ScanType) {
if let Some(list) = scan.scan_list() {
self.inner
.scan_index
.overrun(list)
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
pub(crate) fn scan_overruns(&self, list: crate::server::record::ScanList) -> u64 {
self.inner
.scan_index
.overrun(list)
.load(std::sync::atomic::Ordering::Relaxed)
}
pub async fn records_for_scan(&self, scan_type: ScanType) -> Vec<String> {
let Some(list) = scan_type.scan_list() else {
return Vec::new();
};
self.inner
.scan_index
.bucket(list)
.snapshot()
.1
.iter()
.map(|k| k.name.to_string())
.collect()
}
fn live_scan_key(&self, name: &str) -> Option<super::ScanKey> {
let rec = self.get_record_no_resolve(name)?;
let (phas, record_type) = {
let inst = rec.read();
(inst.common.phas, inst.record.record_type())
};
let seq = self.inner.load_order.load().get(name).copied().unwrap_or(0);
Some(super::ScanKey::new(
phas,
record_type,
seq,
name,
std::sync::Arc::downgrade(&rec),
))
}
pub(crate) fn scan_cursor(&self, list: ScanList) -> ScanCursor {
ScanCursor {
list,
snapshot: None,
next: 0,
revision: 0,
}
}
pub(crate) async fn scan_list_once(&self, list: ScanList) {
let mut cursor = self.scan_cursor(list);
let mut visited = crate::server::database::ProcStack::new();
while let Some((name, rec)) = cursor.next(self) {
let _ = match rec {
Some(rec) => self.process_record_with_links_resolved(name, rec, &mut visited),
None => self.process_record_with_links_sync(name, &mut visited),
};
debug_assert!(
visited.is_empty(),
"a returned process frame left its cycle marker behind"
);
}
}
pub async fn pini_records(&self, mode: PiniMode) -> Vec<String> {
let mut result = Vec::new();
for (name, rec) in self.records_in_load_order().await {
if rec.read().common.pini == mode.to_u16() as i16 {
result.push(name);
}
}
result
}
async fn records_in_load_order(&self) -> Vec<(String, std::sync::Arc<RecordCell>)> {
let snapshot: Vec<_> = {
let records = self.inner.records.read();
records
.iter()
.map(|(n, r)| (n.to_string(), r.clone()))
.collect()
};
let mut keyed: Vec<_> = {
let load_order = self.inner.load_order.load();
snapshot
.into_iter()
.map(|(name, rec)| {
(
load_order.get(name.as_str()).copied().unwrap_or(0),
name,
rec,
)
})
.collect()
};
keyed.sort_unstable_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
keyed
.into_iter()
.map(|(_seq, name, rec)| (name, rec))
.collect()
}
pub async fn pini_process(&self, mode: PiniMode) {
const MIN_PHASE: i32 = i16::MIN as i32;
const NO_NEXT_PHASE: i32 = i16::MAX as i32 + 1;
let mut next = MIN_PHASE;
loop {
let this = next;
next = NO_NEXT_PHASE;
let mut visited = crate::server::database::ProcStack::new();
for (name, rec) in self.records_in_load_order().await {
let (pini, phas) = {
let instance = rec.read();
(instance.common.pini, i32::from(instance.common.phas))
};
if pini != mode.to_u16() as i16 {
continue;
}
if phas == this {
let _ =
self.process_record_with_links_resolved(&name, rec.clone(), &mut visited);
debug_assert!(
visited.is_empty(),
"a returned process frame left its cycle marker behind"
);
} else if phas > this && phas < next {
next = phas;
}
}
if next == NO_NEXT_PHASE {
return;
}
}
}
pub async fn post_event(&self) {
if !crate::server::scan::scan_is_running() {
return;
}
if let Some(list) = ScanType::Event.scan_list() {
self.scan_list_once(list).await;
}
}
pub async fn post_event_named(&self, event_name: &str) {
if !crate::server::scan::scan_is_running() {
return;
}
let want = normalize_event_name(event_name);
if want.is_empty() {
return;
}
let Some(list) = ScanType::Event.scan_list() else {
return;
};
let mut cursor = self.scan_cursor(list);
let mut visited = crate::server::database::ProcStack::new();
while let Some((name, rec)) = cursor.next(self) {
let Some(rec) = rec.or_else(|| self.get_record(name)) else {
continue;
};
let evnt = rec.read().common.evnt.clone();
if normalize_event_name(&evnt) != want {
continue;
}
let _ = self.process_record_with_links_resolved(name, rec, &mut visited);
debug_assert!(
visited.is_empty(),
"a returned process frame left its cycle marker behind"
);
}
}
}
pub(crate) struct ScanCursor {
list: ScanList,
snapshot: Option<Arc<[super::ScanKey]>>,
next: usize,
revision: u64,
}
impl ScanCursor {
fn standing(&self) -> Option<&super::ScanKey> {
self.snapshot.as_ref()?.get(self.next.checked_sub(1)?)
}
fn resync(&mut self, db: &PvDatabase) {
let was = self.standing().cloned();
let live = was.as_ref().and_then(|w| db.live_scan_key(&w.name));
let (revision, snapshot) = db.inner.scan_index.bucket(self.list).snapshot();
let resume = match live {
Some(k) if snapshot.binary_search(&k).is_ok() => Some(k),
_ => was,
};
self.next = resume.map_or(0, |r| snapshot.partition_point(|k| *k <= r));
self.revision = revision;
self.snapshot = Some(snapshot);
}
#[allow(clippy::type_complexity)]
pub(crate) fn next(&mut self, db: &PvDatabase) -> Option<(&str, Option<Arc<RecordCell>>)> {
let bucket = db.inner.scan_index.bucket(self.list);
if self.snapshot.is_none()
|| bucket.revision.load(std::sync::atomic::Ordering::Acquire) != self.revision
{
self.resync(db);
}
let key = self.snapshot.as_ref()?.get(self.next)?;
self.next += 1;
Some((&key.name, key.handle.upgrade()))
}
}
pub(crate) fn normalize_event_name(name: &str) -> String {
let trimmed = name.trim();
if trimmed.is_empty() {
return String::new();
}
if let Ok(num) = trimmed.parse::<f64>() {
if num >= 0.0 && num < 256.0 {
let int = num as i64;
if int < 1 {
return String::new();
}
return int.to_string();
}
}
trimmed.to_string()
}
#[cfg(test)]
mod tests {
use super::PvDatabase;
use super::normalize_event_name;
use crate::server::record::ScanType;
#[test]
fn a_frame_that_finds_no_record_leaves_no_cycle_marker() {
let db = PvDatabase::new();
let mut visited = crate::server::database::ProcStack::new();
let result = db.process_record_with_links_sync("NO:SUCH:RECORD", &mut visited);
assert!(
result.is_err(),
"a name the database does not hold is an error"
);
assert!(
visited.is_empty(),
"the frame left its cycle marker behind: {visited:?}"
);
}
#[test]
fn a_caller_holding_l46_cannot_reach_the_scan_index_owner() {
let db = PvDatabase::new();
let held = db.lock_registration("a_test_standing_in_for_a_registration_entry_point");
let violation = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
db.update_scan_index("ANY", ScanType::Passive, ScanType::SEC01, 0, 0);
}));
let payload = violation
.expect_err("holding L46 across update_scan_index must panic, not park the thread");
let msg = payload
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| payload.downcast_ref::<&str>().copied())
.unwrap_or("");
assert!(
msg.contains("not reentrant") && msg.contains("update_scan_index"),
"the panic must name the rule and the violating site, got: {msg}"
);
drop(held);
}
#[test]
fn the_scan_index_owner_takes_l46_itself_when_no_caller_holds_it() {
let db = PvDatabase::new();
db.update_scan_index("ANY", ScanType::Passive, ScanType::SEC01, 0, 0);
}
#[test]
fn the_registration_gate_clears_on_drop_and_on_unwind() {
let db = PvDatabase::new();
drop(db.lock_registration("first"));
let _second = db.lock_registration("second");
drop(_second);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _g = db.lock_registration("panics_while_held");
panic!("unwind with the gate live");
}));
drop(db.lock_registration("after_unwind"));
}
#[test]
fn every_scan_bucket_transition_moves_the_revision() {
let db = PvDatabase::new();
let list = ScanType::SEC01.scan_list().expect("1 second names a list");
let revision = || {
db.inner
.scan_index
.bucket(list)
.revision
.load(std::sync::atomic::Ordering::Relaxed)
};
let empty = revision();
db.add_to_scan_list(ScanType::SEC01, 0, "calc", 0, "R:ONE");
let added = revision();
assert_ne!(added, empty, "an insert is a transition");
db.add_to_scan_list(ScanType::SEC01, 0, "calc", 0, "R:ONE");
let re_added = revision();
assert_ne!(
re_added, added,
"re-inserting a key the bucket already holds is a transition too"
);
db.delete_from_scan_list(ScanType::SEC01, "R:ONE");
assert_ne!(revision(), re_added, "a removal is a transition");
}
#[test]
fn event_name_numeric_normalisation() {
assert_eq!(normalize_event_name(" 5 "), "5");
assert_eq!(normalize_event_name("5.0"), "5");
assert_eq!(normalize_event_name("5"), "5");
assert_eq!(normalize_event_name("0"), "");
assert_eq!(normalize_event_name(""), "");
assert_eq!(normalize_event_name(" "), "");
assert_eq!(normalize_event_name("myEvent"), "myEvent");
assert_eq!(normalize_event_name(" myEvent "), "myEvent");
assert_eq!(normalize_event_name("999"), "999");
}
}