use super::{
capability::{DbCore, DbMetadata, DbRead, DbWrite},
finalised_source::FinalisedSource,
};
use crate::{
chain_index::finalised_state::capability::{Capability, CapabilityRequest},
error::FinalisedStateError,
BlockHash, BlockchainSource, Height, IndexedBlock, StatusType,
};
use arc_swap::{ArcSwap, ArcSwapOption};
use std::sync::{
atomic::{AtomicU32, AtomicUsize, Ordering},
Arc, Mutex,
};
use tokio::runtime::Handle;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum EphemeralMode {
ReadOnly,
Full,
}
#[derive(Debug)]
pub(crate) struct EphemeralReference<T>
where
T: BlockchainSource + Send + Sync + 'static,
{
router: Arc<Router<T>>,
ephemeral: Option<Arc<FinalisedSource<T>>>,
mode: EphemeralMode,
}
impl<T> EphemeralReference<T>
where
T: BlockchainSource + Send + Sync + 'static,
{
fn new(
router: Arc<Router<T>>,
ephemeral: Arc<FinalisedSource<T>>,
mode: EphemeralMode,
) -> Self {
Self {
router,
ephemeral: Some(ephemeral),
mode,
}
}
pub(crate) fn backend(&self) -> &Arc<FinalisedSource<T>> {
self.ephemeral
.as_ref()
.expect("ephemeral reference missing backend")
}
pub(crate) fn mode(&self) -> EphemeralMode {
self.mode
}
}
impl<T> Drop for EphemeralReference<T>
where
T: BlockchainSource + Send + Sync + 'static,
{
fn drop(&mut self) {
let Some(ephemeral) = self.ephemeral.take() else {
return;
};
self.router
.release_ephemeral_reference(ephemeral, self.mode);
}
}
pub(super) struct BackgroundOpGuard<T: BlockchainSource> {
router: Arc<Router<T>>,
}
impl<T: BlockchainSource> Drop for BackgroundOpGuard<T> {
fn drop(&mut self) {
self.router.background_ops.fetch_sub(1, Ordering::AcqRel);
}
}
#[derive(Debug)]
pub(crate) struct Router<T: BlockchainSource> {
primary: ArcSwap<FinalisedSource<T>>,
ephemeral: ArcSwapOption<FinalisedSource<T>>,
ephemeral_lifecycle_lock: Mutex<()>,
ephemeral_read_only_reference_count: AtomicU32,
ephemeral_full_reference_count: AtomicU32,
primary_mask: AtomicU32,
ephemeral_mask: AtomicU32,
background_ops: AtomicUsize,
}
impl<T: BlockchainSource> Router<T> {
pub(crate) fn new(primary: Arc<FinalisedSource<T>>) -> Self {
let cap = primary.capability();
Self {
primary: ArcSwap::from(primary),
ephemeral: ArcSwapOption::empty(),
ephemeral_lifecycle_lock: Mutex::new(()),
ephemeral_read_only_reference_count: AtomicU32::new(0),
ephemeral_full_reference_count: AtomicU32::new(0),
primary_mask: AtomicU32::new(cap.bits()),
ephemeral_mask: AtomicU32::new(0),
background_ops: AtomicUsize::new(0),
}
}
#[inline]
pub(crate) fn backend(
&self,
cap: CapabilityRequest,
) -> Result<Arc<FinalisedSource<T>>, FinalisedStateError> {
let bit = cap.as_capability().bits();
if self.ephemeral_mask.load(Ordering::Acquire) & bit != 0 {
if let Some(ephemeral) = self.ephemeral.load().as_ref() {
return Ok(Arc::clone(ephemeral));
}
}
if self.primary_mask.load(Ordering::Acquire) & bit != 0 {
return Ok(self.primary.load_full());
}
Err(FinalisedStateError::FeatureUnavailable(cap.name()))
}
pub(crate) async fn init_or_take_ephemeral(
self: &Arc<Self>,
source: T,
network: zebra_chain::parameters::Network,
mode: EphemeralMode,
db_height: Option<Height>,
) -> Result<EphemeralReference<T>, FinalisedStateError>
where
T: Send + Sync + 'static,
{
let _ephemeral_lifecycle_guard = self
.ephemeral_lifecycle_lock
.lock()
.expect("ephemeral lifecycle mutex poisoned");
match mode {
EphemeralMode::ReadOnly => {
self.ephemeral_read_only_reference_count
.fetch_add(1, Ordering::AcqRel);
}
EphemeralMode::Full => {
self.ephemeral_full_reference_count
.fetch_add(1, Ordering::AcqRel);
}
}
let ephemeral = match self.ephemeral.load_full() {
Some(ephemeral) => {
match ephemeral.as_ref() {
FinalisedSource::Ephemeral(ephemeral_backend) => {
ephemeral_backend.update_db_height(db_height)?;
}
FinalisedSource::V1(_) => {
self.decrement_ephemeral_reference_count(mode);
return Err(FinalisedStateError::Custom(
"router ephemeral slot contained a persistent database backend"
.to_string(),
));
}
}
ephemeral
}
None => {
let ephemeral = Arc::new(FinalisedSource::ephemeral(source, network, db_height));
self.ephemeral.store(Some(Arc::clone(&ephemeral)));
ephemeral
}
};
let active_mode = self.active_ephemeral_mode().ok_or_else(|| {
FinalisedStateError::Custom(
"ephemeral routing mode missing after incrementing reference count".to_string(),
)
})?;
self.apply_ephemeral_mode(ephemeral.as_ref(), active_mode);
Ok(EphemeralReference::new(Arc::clone(self), ephemeral, mode))
}
fn release_ephemeral_reference(
&self,
ephemeral_reference: Arc<FinalisedSource<T>>,
mode: EphemeralMode,
) where
T: Send + Sync + 'static,
{
let ephemeral_to_shutdown = {
let _ephemeral_lifecycle_guard = self
.ephemeral_lifecycle_lock
.lock()
.expect("ephemeral lifecycle mutex poisoned");
self.decrement_ephemeral_reference_count(mode);
let ephemeral_guard = self.ephemeral.load();
let Some(active_ephemeral) = ephemeral_guard.as_ref() else {
return;
};
if !Arc::ptr_eq(&ephemeral_reference, active_ephemeral) {
return;
}
match self.active_ephemeral_mode() {
Some(active_mode) => {
self.apply_ephemeral_mode(active_ephemeral.as_ref(), active_mode);
return;
}
None => {
self.restore_primary_capability();
self.ephemeral.swap(None)
}
}
};
drop(ephemeral_reference);
if let Some(ephemeral) = ephemeral_to_shutdown {
match Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
if let Err(error) = ephemeral.shutdown().await {
tracing::warn!("ephemeral shutdown failed during release: {error}");
}
});
}
Err(error) => {
tracing::warn!(
"ephemeral backend removed from routing but could not be shut down asynchronously: {error}"
);
}
}
}
}
pub(crate) fn update_ephemeral_db_height(
&self,
db_height: Option<Height>,
) -> Result<(), FinalisedStateError> {
let Some(ephemeral) = self.ephemeral.load_full() else {
return Ok(());
};
match ephemeral.as_ref() {
FinalisedSource::Ephemeral(ephemeral) => ephemeral.update_db_height(db_height),
FinalisedSource::V1(_) => Err(FinalisedStateError::Custom(
"router ephemeral slot contained a persistent database backend".to_string(),
)),
}
}
pub(crate) fn primary_is_ephemeral(&self) -> bool {
matches!(self.primary.load().as_ref(), FinalisedSource::Ephemeral(_))
}
pub(crate) fn has_full_ephemeral_reference(&self) -> bool {
self.ephemeral_full_reference_count.load(Ordering::Acquire) > 0
}
fn active_ephemeral_mode(&self) -> Option<EphemeralMode> {
if self.ephemeral_full_reference_count.load(Ordering::Acquire) > 0 {
Some(EphemeralMode::Full)
} else if self
.ephemeral_read_only_reference_count
.load(Ordering::Acquire)
> 0
{
Some(EphemeralMode::ReadOnly)
} else {
None
}
}
fn primary_capability(&self) -> Capability {
self.primary.load_full().capability()
}
fn read_only_ephemeral_capability(ephemeral: &FinalisedSource<T>) -> Capability {
ephemeral.capability() & !Capability::WRITE_CORE
}
fn primary_write_capability(&self) -> Capability {
self.primary_capability() & Capability::WRITE_CORE
}
fn apply_ephemeral_mode(&self, ephemeral: &FinalisedSource<T>, mode: EphemeralMode) {
match mode {
EphemeralMode::ReadOnly => {
self.ephemeral_mask.store(
Self::read_only_ephemeral_capability(ephemeral).bits(),
Ordering::Release,
);
self.primary_mask
.store(self.primary_write_capability().bits(), Ordering::Release);
}
EphemeralMode::Full => {
self.ephemeral_mask
.store(ephemeral.capability().bits(), Ordering::Release);
self.primary_mask.store(0, Ordering::Release);
}
}
}
fn restore_primary_capability(&self) {
self.primary_mask
.store(self.primary_capability().bits(), Ordering::Release);
self.ephemeral_mask.store(0, Ordering::Release);
}
fn decrement_ephemeral_reference_count(&self, mode: EphemeralMode) {
match mode {
EphemeralMode::ReadOnly => {
let previous_reference_count = self
.ephemeral_read_only_reference_count
.fetch_sub(1, Ordering::AcqRel);
if previous_reference_count == 0 {
self.ephemeral_read_only_reference_count
.store(0, Ordering::Release);
tracing::warn!("ephemeral read-only reference count underflow");
}
}
EphemeralMode::Full => {
let previous_reference_count = self
.ephemeral_full_reference_count
.fetch_sub(1, Ordering::AcqRel);
if previous_reference_count == 0 {
self.ephemeral_full_reference_count
.store(0, Ordering::Release);
tracing::warn!("ephemeral full reference count underflow");
}
}
}
}
pub(crate) fn primary_backend(&self) -> Arc<FinalisedSource<T>> {
self.primary.load_full()
}
pub(crate) fn replace_primary(
&self,
new_primary: Arc<FinalisedSource<T>>,
) -> Arc<FinalisedSource<T>> {
let _ephemeral_lifecycle_guard = self
.ephemeral_lifecycle_lock
.lock()
.expect("ephemeral lifecycle mutex poisoned");
let old_primary = self.primary.swap(new_primary);
match self.ephemeral.load().as_ref() {
Some(ephemeral) => match self.active_ephemeral_mode() {
Some(active_mode) => {
self.apply_ephemeral_mode(ephemeral.as_ref(), active_mode);
}
None => {
self.restore_primary_capability();
}
},
None => {
self.restore_primary_capability();
}
}
old_primary
}
pub(crate) fn store_primary_status(&self, status: StatusType) {
self.primary.load_full().store_status(status);
}
pub(super) fn begin_background_op(self: &Arc<Self>) -> BackgroundOpGuard<T> {
self.background_ops.fetch_add(1, Ordering::AcqRel);
BackgroundOpGuard {
router: Arc::clone(self),
}
}
pub(super) fn has_background_ops(&self) -> bool {
self.background_ops.load(Ordering::Acquire) != 0
}
}
impl<T: BlockchainSource> DbCore for Router<T> {
fn status(&self) -> StatusType {
let primary_status = self.primary.load_full().status();
if primary_status == StatusType::CriticalError {
return primary_status;
}
match self.backend(CapabilityRequest::ReadCore) {
Ok(backend) => backend.status(),
Err(_) => StatusType::Busy,
}
}
async fn shutdown(&self) -> Result<(), FinalisedStateError> {
self.ephemeral_mask.store(0, Ordering::Release);
let ephemeral = self.ephemeral.swap(None);
self.primary_mask.store(
self.primary.load_full().capability().bits(),
Ordering::Release,
);
let primary_shutdown_result = self.primary.load_full().shutdown().await;
let ephemeral_shutdown_result = match ephemeral {
Some(ephemeral) => ephemeral.shutdown().await,
None => Ok(()),
};
primary_shutdown_result?;
ephemeral_shutdown_result?;
Ok(())
}
}
impl<T: BlockchainSource> DbWrite for Router<T> {
async fn write_block(&self, blk: IndexedBlock) -> Result<(), FinalisedStateError> {
self.backend(CapabilityRequest::WriteCore)?
.write_block(blk)
.await
}
async fn write_blocks_to_height<S: crate::chain_index::source::BlockchainSource>(
&self,
height: Height,
source: &S,
) -> Result<(), FinalisedStateError> {
self.backend(CapabilityRequest::WriteCore)?
.write_blocks_to_height(height, source)
.await
}
async fn delete_block_at_height(&self, h: Height) -> Result<(), FinalisedStateError> {
self.backend(CapabilityRequest::WriteCore)?
.delete_block_at_height(h)
.await
}
async fn delete_block(&self, blk: &IndexedBlock) -> Result<(), FinalisedStateError> {
self.backend(CapabilityRequest::WriteCore)?
.delete_block(blk)
.await
}
async fn update_metadata(&self, metadata: DbMetadata) -> Result<(), FinalisedStateError> {
self.backend(CapabilityRequest::WriteCore)?
.update_metadata(metadata)
.await
}
}
impl<T: BlockchainSource> DbRead for Router<T> {
async fn db_height(&self) -> Result<Option<Height>, FinalisedStateError> {
self.backend(CapabilityRequest::ReadCore)?.db_height().await
}
async fn get_block_height(
&self,
hash: BlockHash,
) -> Result<Option<Height>, FinalisedStateError> {
self.backend(CapabilityRequest::ReadCore)?
.get_block_height(hash)
.await
}
async fn get_block_hash(&self, h: Height) -> Result<Option<BlockHash>, FinalisedStateError> {
self.backend(CapabilityRequest::ReadCore)?
.get_block_hash(h)
.await
}
async fn get_metadata(&self) -> Result<DbMetadata, FinalisedStateError> {
self.backend(CapabilityRequest::ReadCore)?
.get_metadata()
.await
}
}