use super::*;
#[cfg(test)]
struct EpochUpdateGuard<'a>(&'a AtomicU64);
#[cfg(test)]
impl Drop for EpochUpdateGuard<'_> {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
#[cfg(test)]
pub(crate) struct ProcessorRegistryState {
registry: std::sync::Arc<parking_lot::RwLock<crate::plugins::registry::PostProcessorRegistry>>,
cache: parking_lot::RwLock<Option<ProcessorCache>>,
registration_lock: Mutex<()>,
registration_epoch: AtomicU64,
active_processor_snapshots: std::sync::Arc<AtomicUsize>,
}
#[cfg(test)]
impl ProcessorRegistryState {
pub(crate) fn new_isolated() -> Self {
Self {
registry: std::sync::Arc::new(parking_lot::RwLock::new(
crate::plugins::registry::PostProcessorRegistry::new(),
)),
cache: parking_lot::RwLock::new(None),
registration_lock: Mutex::new(()),
registration_epoch: AtomicU64::new(0),
active_processor_snapshots: std::sync::Arc::new(AtomicUsize::new(0)),
}
}
fn with_mutation<T>(&self, update: impl FnOnce() -> Result<T>) -> Result<T> {
let _registration_guard = self
.registration_lock
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
self.registration_epoch.fetch_add(1, Ordering::SeqCst);
let _epoch_guard = EpochUpdateGuard(&self.registration_epoch);
let registry_in_use = self.active_processor_snapshots.load(Ordering::SeqCst) != 0;
run_after_registration_update_began_hook();
if registry_in_use {
return Err(crate::XbergError::Other(
"post-processor registry is in use by an active extraction; retry the lifecycle mutation after extraction completes"
.to_string(),
));
}
update()
}
pub(crate) fn register(&self, processor: std::sync::Arc<dyn crate::plugins::PostProcessor>) -> Result<()> {
self.with_mutation(|| self.registry.write().register(processor))
}
pub(crate) fn unregister(&self, name: &str) -> Result<()> {
self.with_mutation(|| self.registry.write().remove(name))
}
pub(crate) fn with_gate<T>(&self, update: impl FnOnce() -> Result<T>) -> Result<T> {
self.with_mutation(update)
}
fn try_initialize_cache(&self, registration_epoch: u64) -> Result<bool> {
let current_generation = self.registry.read().generation();
if self.registration_epoch.load(Ordering::SeqCst) != registration_epoch {
return Ok(false);
}
let mut cache_lock = self.cache.write();
let is_stale = cache_lock
.as_ref()
.is_some_and(|cache| cache.generation != current_generation);
if cache_lock.is_none() || is_stale {
let candidate = ProcessorCache::from_registry(&self.registry, registration_epoch)?;
if self.registration_epoch.load(Ordering::SeqCst) != registration_epoch {
return Ok(false);
}
*cache_lock = Some(candidate);
} else if let Some(cache) = cache_lock.as_mut() {
cache.registration_epoch = registration_epoch;
}
Ok(self.registration_epoch.load(Ordering::SeqCst) == registration_epoch)
}
fn wait_for_update(&self) {
let _registration_guard = self
.registration_lock
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
}
pub(crate) fn ensure_cache_current(&self) -> Result<()> {
loop {
let registration_epoch = self.registration_epoch.load(Ordering::SeqCst);
if !registration_update_in_progress(registration_epoch) && self.try_initialize_cache(registration_epoch)? {
return Ok(());
}
self.wait_for_update();
}
}
pub(crate) fn try_get_snapshot(&self) -> Option<ProcessorSnapshot> {
let registration_epoch = self.registration_epoch.load(Ordering::SeqCst);
if registration_update_in_progress(registration_epoch) {
return None;
}
run_before_processor_snapshot_hook();
let lease = ProcessorSnapshotLease::acquire(&self.active_processor_snapshots);
let stages = self.cache.try_read()?.as_ref().and_then(|cache| {
(cache.registration_epoch == registration_epoch).then(|| cached_processor_stages(cache))
})?;
if self.registration_epoch.load(Ordering::SeqCst) != registration_epoch {
return None;
}
run_after_processor_snapshot_validated_hook();
Some(ProcessorSnapshot {
early: stages.0,
middle: stages.1,
late: stages.2,
_lease: lease,
})
}
fn build_snapshot_sync(&self) -> Result<ProcessorSnapshot> {
loop {
self.ensure_cache_current()?;
if let Some(snapshot) = self.try_get_snapshot() {
return Ok(snapshot);
}
self.wait_for_update();
}
}
}
#[cfg(all(test, feature = "tokio-runtime"))]
pub(crate) async fn processor_snapshot_from_state(
state: &std::sync::Arc<ProcessorRegistryState>,
) -> Result<ProcessorSnapshot> {
if let Some(snapshot) = state.try_get_snapshot() {
return Ok(snapshot);
}
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
return state.build_snapshot_sync();
};
let state = std::sync::Arc::clone(state);
runtime
.spawn_blocking(move || state.build_snapshot_sync())
.await
.map_err(|error| crate::XbergError::Other(format!("processor cache task failed to join: {error}")))?
}