use crate::store::{OpenStore, StorageResult};
use crate::{MigrationReport, Store, StoreBackend, StoreBuilder};
use std::fmt;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{PoisonError, RwLock};
static GLOBAL_STORE: RwLock<Option<(u64, Store)>> = RwLock::new(None);
static INSTALLS: AtomicU64 = AtomicU64::new(1);
fn held() -> Option<Store> {
GLOBAL_STORE
.read()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
.map(|(_, store)| store.clone())
}
fn installed_by(install: u64) -> Option<Store> {
GLOBAL_STORE
.read()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
.filter(|(held, _)| *held == install)
.map(|(_, store)| store.clone())
}
#[must_use = "dropped here, the global store is closed here - bind it in `main` \
(`let _ame = ...`) so the last writes are flushed on the way out"]
#[derive(Debug)]
pub struct GlobalStoreGuard {
install: u64,
}
impl GlobalStoreGuard {
#[allow(clippy::needless_doctest_main)]
pub fn close(self) -> StorageResult<()> {
let closing = match installed_by(self.install) {
Some(store) => Ok(store.close()?),
None => Ok(()),
};
std::mem::forget(self);
closing
}
pub fn close_async(self) -> crate::store::Commit {
let closing = match installed_by(self.install) {
Some(store) => StoreBackend::close_async(&store),
None => crate::store::Commit::ready(Ok(())),
};
std::mem::forget(self);
closing
}
}
impl Drop for GlobalStoreGuard {
fn drop(&mut self) {
let Some(store) = installed_by(self.install) else {
return;
};
if let Err(report) = store.close() {
tracing::error!(
target: "amethystate",
error = ?report,
"the global store's closing flush failed: what it still held is not on disk",
);
}
}
}
pub trait IntoGlobalStore: Sized {
fn into_store_builder(self) -> StoreBuilder;
fn init_global(self) -> GlobalStoreGuard {
self.into_store_builder()
.build_global()
.unwrap_or_else(|why| {
panic!("amethystate: the global store was not put in place: {why:?}")
})
}
}
impl StoreBuilder {
pub fn build_global(self) -> Result<GlobalStoreGuard, InitGlobal> {
refuse_a_second()?;
let store = self.build().map_err(InitGlobal::Open)?;
installed(store)
}
pub fn migrate_global(self) -> Result<(GlobalStoreGuard, MigrationReport), InitGlobal> {
refuse_a_second()?;
let (store, report) = self.migrate().map_err(InitGlobal::Open)?;
Ok((installed(store)?, report))
}
}
impl crate::store::builder::WithSteps {
pub fn migrate_global(self) -> Result<(GlobalStoreGuard, MigrationReport), InitGlobal> {
self.into_builder().migrate_global()
}
}
#[cfg(feature = "memory")]
impl crate::store::builder::OrInMemory<crate::store::builder::WithSteps> {
pub fn migrate_global(
self,
) -> Result<(GlobalStoreGuard, MigrationReport, crate::store::Persistence), InitGlobal> {
refuse_a_second()?;
let (store, report, persistence) = self.migrate();
Ok((installed(store)?, report, persistence))
}
}
#[cfg(feature = "memory")]
impl crate::store::builder::OrInMemory {
pub fn build_global(self) -> Result<(GlobalStoreGuard, crate::store::Persistence), InitGlobal> {
refuse_a_second()?;
let (store, persistence) = self.build();
Ok((installed(store)?, persistence))
}
pub fn migrate_global(
self,
) -> Result<(GlobalStoreGuard, MigrationReport, crate::store::Persistence), InitGlobal> {
refuse_a_second()?;
let (store, report, persistence) = self.migrate();
Ok((installed(store)?, report, persistence))
}
}
fn refuse_a_second() -> Result<(), InitGlobal> {
match held() {
Some(store) if !store.is_closed() => Err(InitGlobal::AlreadyInstalled),
_ => Ok(()),
}
}
fn installed(store: Store) -> Result<GlobalStoreGuard, InitGlobal> {
install_global(store).map_err(|_| InitGlobal::AlreadyInstalled)
}
pub enum InitGlobal {
Open(OpenStore),
AlreadyInstalled,
}
impl fmt::Display for InitGlobal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Open(_) => f.write_str("the global store would not open"),
Self::AlreadyInstalled => f.write_str("a global store is in place already"),
}
}
}
impl fmt::Debug for InitGlobal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Open(why) => write!(f, "{self}: {why:?}"),
Self::AlreadyInstalled => fmt::Display::fmt(self, f),
}
}
}
impl std::error::Error for InitGlobal {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Open(why) => Some(why),
Self::AlreadyInstalled => None,
}
}
}
pub struct AlreadyInstalled {
pub store: Store,
}
impl fmt::Display for AlreadyInstalled {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a global store is in place already")
}
}
impl fmt::Debug for AlreadyInstalled {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl std::error::Error for AlreadyInstalled {}
pub fn install_global(store: Store) -> Result<GlobalStoreGuard, AlreadyInstalled> {
let mut slot = GLOBAL_STORE.write().unwrap_or_else(PoisonError::into_inner);
if slot.as_ref().is_some_and(|(_, held)| !held.is_closed()) {
return Err(AlreadyInstalled { store });
}
let install = INSTALLS.fetch_add(1, Ordering::Relaxed);
*slot = Some((install, store));
Ok(GlobalStoreGuard { install })
}
impl IntoGlobalStore for StoreBuilder {
fn into_store_builder(self) -> StoreBuilder {
self
}
}
impl IntoGlobalStore for &str {
fn into_store_builder(self) -> StoreBuilder {
StoreBuilder::new(self)
}
}
impl IntoGlobalStore for &Path {
fn into_store_builder(self) -> StoreBuilder {
StoreBuilder::new(self)
}
}
#[allow(clippy::needless_doctest_main)]
pub fn init_global<T: IntoGlobalStore>(source: T) -> GlobalStoreGuard {
source.init_global()
}
pub fn global_store() -> Store {
held().expect(
"amethystate: the global store is not initialized.\n\
Call `init_global`, `build_global` or `migrate_global` during startup, \
and keep the guard it returns alive for as long as the store is used.",
)
}
pub fn shutdown() -> StorageResult<()> {
match held() {
Some(store) => Ok(store.close()?),
None => Ok(()),
}
}
pub fn shutdown_async() -> crate::store::Commit {
match held() {
Some(store) => StoreBackend::close_async(&store),
None => crate::store::Commit::ready(Ok(())),
}
}