aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src/asset
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-12 00:42:07 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-12 00:56:52 +0800
commitd5a039e9d882f8f50b3cc9cdc10bc7bd84a5fa4d (patch)
treec0ae87e58690685183e28925a4bc7b42188f2162 /mingling_core/src/asset
parent5f8bd86c982dbfb3d8ed47f84432a3347752157f (diff)
refactor(core): extract global resource store into standalone container
Refactor `GlobalResources` from a type-erased `Arc<Mutex<HashMap<...>>>` alias into a `GlobalResContainer` struct with per-resource mutexes. This prevents nested `modify_res` calls from deadlocking, allows independent containers to coexist, and keeps `Program`'s public resource API unchanged.
Diffstat (limited to 'mingling_core/src/asset')
-rw-r--r--mingling_core/src/asset/global_resource.rs495
1 files changed, 429 insertions, 66 deletions
diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs
index f37edd0..07d0b5e 100644
--- a/mingling_core/src/asset/global_resource.rs
+++ b/mingling_core/src/asset/global_resource.rs
@@ -6,22 +6,64 @@ use std::{
use crate::{ChainProcess, Program, ProgramCollect, this};
-/// A thread-safe, type-erased container for storing global resources keyed by their type.
+/// A standalone, thread-safe container for storing global resources keyed by their type.
///
-/// Use `Program::with_resource` to insert resources and `Program::res` to retrieve them.
-pub type GlobalResources = Arc<Mutex<HashMap<TypeId, Box<dyn Any + Sync + Send>>>>;
+/// This is the resource store behind [`Program`]'s resource API: every `Program`
+/// owns one of these containers and all of its resource operations delegate to it.
+///
+/// Unlike the resource API on [`Program`], this container is **not** coupled to a
+/// program instance nor to the global `this::<C>()` context, so any number of
+/// containers can be created and used at the same time — each with fully
+/// independent storage.
+///
+/// Each resource is stored behind its **own** [`Mutex`]. The container lock is
+/// only held for the brief lookup/clone of the entry, so two nested
+/// `modify_res` calls (e.g. two `&mut` resource parameters generated by
+/// `#[chain]`) lock **different** mutexes and cannot deadlock against each other.
+pub struct GlobalResContainer {
+ /// Thread-safe storage for resources, keyed by their `TypeId` and protected by a `Mutex`.
+ ///
+ /// Each entry is a `Box<dyn Any>` holding an `Arc<Mutex<Arc<Res>>>`: the outer
+ /// `Mutex` guards the entry itself (so a resource can be locked without
+ /// holding the container lock), and the inner `Arc<Res>` is the shared
+ /// immutable snapshot returned by `res()`.
+ map: Mutex<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
+}
+
+impl GlobalResContainer {
+ /// Creates an empty resource container.
+ #[must_use]
+ pub fn new() -> Self {
+ Self {
+ map: Mutex::new(HashMap::new()),
+ }
+ }
+
+ /// Clones the per-resource entry out from under the container lock.
+ ///
+ /// The container lock is released as soon as the entry `Arc` is cloned,
+ /// so all subsequent operations lock only the resource's own mutex.
+ fn res_entry<Res: 'static>(&self) -> Option<Arc<Mutex<Arc<Res>>>> {
+ let guard = self.map.lock().ok()?;
+ let entry = guard
+ .get(&TypeId::of::<Res>())?
+ .as_ref()
+ .downcast_ref::<Arc<Mutex<Arc<Res>>>>()
+ .map(Arc::clone);
+ drop(guard);
+ entry
+ }
-impl<C> Program<C>
-where
- C: ProgramCollect<Enum = C>,
-{
/// Insert a resource of the given type, cloning the provided value into the store
pub fn with_resource<Res: 'static + Send + Sync + ResourceMarker>(
&mut self,
res: Res,
) -> &mut Self {
- if let Ok(mut guard) = self.resources.lock() {
- guard.insert(TypeId::of::<Res>(), Box::new(Arc::new(res)));
+ if let Ok(mut guard) = self.map.lock() {
+ guard.insert(
+ TypeId::of::<Res>(),
+ Box::new(Arc::new(Mutex::new(Arc::new(res)))),
+ );
}
self
}
@@ -32,95 +74,102 @@ where
Res: 'static + Default + ResourceMarker + Send + Sync,
Return: Default,
{
- let Ok(mut guard) = self.resources.lock() else {
+ let Some(entry) = self.res_entry::<Res>() else {
return Return::default();
};
- if let Some(arc_res) = guard
- .get_mut(&TypeId::of::<Res>())
- .and_then(|a| a.downcast_mut::<Arc<Res>>())
- {
- let mut new_res = match Arc::try_unwrap(std::mem::take(arc_res)) {
- Ok(val) => val,
- Err(arc) => (*arc).__resource_marker_clone(),
- };
- let r = f(&mut new_res);
- *arc_res = Arc::new(new_res);
- return r;
- }
- Return::default()
+ let Ok(mut guard) = entry.lock() else {
+ return Return::default();
+ };
+ let mut new_res = match Arc::try_unwrap(std::mem::take(&mut *guard)) {
+ Ok(val) => val,
+ Err(arc) => (*arc).__resource_marker_clone(),
+ };
+ let r = f(&mut new_res);
+ *guard = Arc::new(new_res);
+ r
}
/// Internal syntax for the `&mut MyResource` syntax of #[chain], do not use directly
#[doc(hidden)]
- pub fn __modify_res_and_return_route<Res>(
+ pub fn __modify_res_and_return_route<Res, C>(
&self,
f: impl FnOnce(&mut Res) -> ChainProcess<C>,
) -> ChainProcess<C>
where
Res: 'static + Default + ResourceMarker + Send + Sync,
+ C: ProgramCollect<Enum = C>,
{
- let Ok(mut guard) = self.resources.lock() else {
+ let Some(entry) = self.res_entry::<Res>() else {
let mut default_res = Res::__resource_marker_default();
return f(&mut default_res);
};
- if let Some(arc_res) = guard
- .get_mut(&TypeId::of::<Res>())
- .and_then(|a| a.downcast_mut::<Arc<Res>>())
- {
- let mut new_res = match Arc::try_unwrap(std::mem::take(arc_res)) {
- Ok(val) => val,
- Err(arc) => (*arc).__resource_marker_clone(),
- };
- let r = f(&mut new_res);
- *arc_res = Arc::new(new_res);
- r
- } else {
+ let Ok(mut guard) = entry.lock() else {
let mut default_res = Res::__resource_marker_default();
- f(&mut default_res)
- }
+ return f(&mut default_res);
+ };
+ let mut new_res = match Arc::try_unwrap(std::mem::take(&mut *guard)) {
+ Ok(val) => val,
+ Err(arc) => (*arc).__resource_marker_clone(),
+ };
+ let r = f(&mut new_res);
+ *guard = Arc::new(new_res);
+ r
}
/// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly.
///
- /// Extracts a mutable resource from the global store (clone-out), returning an
+ /// Extracts a mutable resource from the store (clone-out), returning an
/// owned value. The caller must call [`__store_res`] to write back modifications.
#[doc(hidden)]
#[must_use]
pub fn __extract_res_mut<Res: 'static + Default + ResourceMarker + Send + Sync>(&self) -> Res {
- let Ok(mut guard) = self.resources.lock() else {
+ let Some(entry) = self.res_entry::<Res>() else {
return Res::__resource_marker_default();
};
- guard
- .get_mut(&TypeId::of::<Res>())
- .and_then(|a| a.downcast_mut::<Arc<Res>>())
- .map_or_else(
- Res::__resource_marker_default,
- |arc_res| match Arc::try_unwrap(std::mem::take(arc_res)) {
- Ok(val) => val,
- Err(arc) => (*arc).__resource_marker_clone(),
- },
- )
+ let Ok(mut guard) = entry.lock() else {
+ return Res::__resource_marker_default();
+ };
+ match Arc::try_unwrap(std::mem::take(&mut *guard)) {
+ Ok(val) => val,
+ Err(arc) => (*arc).__resource_marker_clone(),
+ }
}
/// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly.
///
- /// Stores a modified resource value back into the global store.
+ /// Stores a modified resource value back into the store.
#[doc(hidden)]
pub fn __store_res<Res: 'static + Send + Sync + ResourceMarker>(&self, val: Res) {
- if let Ok(mut guard) = self.resources.lock() {
- guard.insert(TypeId::of::<Res>(), Box::new(Arc::new(val)));
+ let Ok(mut guard) = self.map.lock() else {
+ return;
+ };
+ let Some(boxed_any) = guard.get_mut(&TypeId::of::<Res>()) else {
+ guard.insert(
+ TypeId::of::<Res>(),
+ Box::new(Arc::new(Mutex::new(Arc::new(val)))),
+ );
+ return;
+ };
+ if let Some(entry) = boxed_any.downcast_mut::<Arc<Mutex<Arc<Res>>>>()
+ && let Ok(mut entry_guard) = entry.lock()
+ {
+ *entry_guard = Arc::new(val);
+ return;
}
+ // The entry exists but cannot be updated (type mismatch or poisoned
+ // lock): replace it wholesale.
+ guard.insert(
+ TypeId::of::<Res>(),
+ Box::new(Arc::new(Mutex::new(Arc::new(val)))),
+ );
}
/// Get an resources by type, returning `Res` if present
#[must_use]
pub fn res<Res: 'static + Send + Sync>(&self) -> Option<GlobalResource<Res>> {
- let guard = self.resources.lock().ok()?;
- let boxed_any = guard.get(&TypeId::of::<Res>())?;
- let arc_res = boxed_any.as_ref().downcast_ref::<Arc<Res>>()?;
- let result = GlobalResource::from(Arc::clone(arc_res));
- drop(guard);
- Some(result)
+ let entry = self.res_entry::<Res>()?;
+ let guard = entry.lock().ok()?;
+ Some(GlobalResource::from(Arc::clone(&*guard)))
}
/// Get a resource by type, returning `GlobalResource<Res>` if present.
@@ -130,10 +179,14 @@ where
/// # Errors
///
/// Returns `Err(route)` when the resource of type `Res` is not present in the store.
- pub fn res_or_route<Res: 'static + Send + Sync>(
+ pub fn res_or_route<Res, C>(
&self,
route: ChainProcess<C>,
- ) -> Result<GlobalResource<Res>, ChainProcess<C>> {
+ ) -> Result<GlobalResource<Res>, ChainProcess<C>>
+ where
+ Res: 'static + Send + Sync,
+ C: ProgramCollect<Enum = C>,
+ {
self.res().map_or_else(|| Err(route), Ok)
}
@@ -147,6 +200,93 @@ where
}
}
+impl Default for GlobalResContainer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<C> Program<C>
+where
+ C: ProgramCollect<Enum = C>,
+{
+ /// Insert a resource of the given type, cloning the provided value into the store
+ pub fn with_resource<Res: 'static + Send + Sync + ResourceMarker>(
+ &mut self,
+ res: Res,
+ ) -> &mut Self {
+ self.resources.with_resource(res);
+ self
+ }
+
+ /// Modify a resource by type, applying a closure to the resource if present
+ pub fn modify_res<Res, Return>(&self, f: impl FnOnce(&mut Res) -> Return) -> Return
+ where
+ Res: 'static + Default + ResourceMarker + Send + Sync,
+ Return: Default,
+ {
+ self.resources.modify_res(f)
+ }
+
+ /// Internal syntax for the `&mut MyResource` syntax of #[chain], do not use directly
+ #[doc(hidden)]
+ pub fn __modify_res_and_return_route<Res>(
+ &self,
+ f: impl FnOnce(&mut Res) -> ChainProcess<C>,
+ ) -> ChainProcess<C>
+ where
+ Res: 'static + Default + ResourceMarker + Send + Sync,
+ {
+ self.resources.__modify_res_and_return_route(f)
+ }
+
+ /// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly.
+ ///
+ /// Extracts a mutable resource from the global store (clone-out), returning an
+ /// owned value. The caller must call [`__store_res`] to write back modifications.
+ #[doc(hidden)]
+ #[must_use]
+ pub fn __extract_res_mut<Res: 'static + Default + ResourceMarker + Send + Sync>(&self) -> Res {
+ self.resources.__extract_res_mut()
+ }
+
+ /// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly.
+ ///
+ /// Stores a modified resource value back into the global store.
+ #[doc(hidden)]
+ pub fn __store_res<Res: 'static + Send + Sync + ResourceMarker>(&self, val: Res) {
+ self.resources.__store_res(val);
+ }
+
+ /// Get an resources by type, returning `Res` if present
+ #[must_use]
+ pub fn res<Res: 'static + Send + Sync>(&self) -> Option<GlobalResource<Res>> {
+ self.resources.res()
+ }
+
+ /// Get a resource by type, returning `GlobalResource<Res>` if present.
+ ///
+ /// If the resource is not present, returns the provided [`ChainProcess`] as an `Err`.
+ ///
+ /// # Errors
+ ///
+ /// Returns `Err(route)` when the resource of type `Res` is not present in the store.
+ pub fn res_or_route<Res: 'static + Send + Sync>(
+ &self,
+ route: ChainProcess<C>,
+ ) -> Result<GlobalResource<Res>, ChainProcess<C>> {
+ self.resources.res_or_route(route)
+ }
+
+ /// Get a resource by type, returning `GlobalResource<Res>` or inserting a default
+ #[must_use]
+ pub fn res_or_default<Res: 'static + Send + Sync + ResourceMarker>(
+ &self,
+ ) -> GlobalResource<Res> {
+ self.resources.res_or_default()
+ }
+}
+
/// Global assets for storing Program global state information
pub struct GlobalResource<ResType: 'static + Send + Sync> {
res_arc: Arc<ResType>,
@@ -225,6 +365,8 @@ where
#[cfg(test)]
mod tests {
use super::*;
+ use crate::MockProgramCollect;
+ use crate::error::ChainProcessError;
#[test]
fn global_resource_new_and_deref() {
@@ -285,8 +427,229 @@ mod tests {
);
}
- // Note: Tests for Program::with_resource, res(), res_or_route(), res_or_default(),
- // and modify_res() require a concrete ProgramCollect implementation, which is
- // complex and outside the scope of these unit tests.
- // Those are better covered by integration tests.
+ #[test]
+ fn container_new_creates_empty_store() {
+ let container = GlobalResContainer::new();
+ assert!(container.res::<i32>().is_none());
+ }
+
+ #[test]
+ fn container_insert_then_res() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(42i32);
+ let res = container.res::<i32>();
+ assert_eq!(*res.unwrap(), 42);
+ }
+
+ #[test]
+ fn container_missing_res_returns_none() {
+ let container = GlobalResContainer::new();
+ assert!(container.res::<String>().is_none());
+ }
+
+ #[test]
+ fn container_res_or_default_creates_default() {
+ let container = GlobalResContainer::new();
+ assert_eq!(*container.res_or_default::<i32>(), 0);
+ }
+
+ #[test]
+ fn container_res_or_default_returns_existing() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(7i32);
+ assert_eq!(*container.res_or_default::<i32>(), 7);
+ }
+
+ #[test]
+ fn container_modify_res_updates_value() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32);
+ let doubled = container.modify_res(|v: &mut i32| {
+ *v *= 2;
+ *v
+ });
+ assert_eq!(doubled, 2);
+ assert_eq!(*container.res::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn container_modify_res_missing_returns_default() {
+ let container = GlobalResContainer::new();
+ let value: i32 = container.modify_res(|v: &mut i32| *v);
+ assert_eq!(value, 0);
+ }
+
+ #[test]
+ fn container_modify_res_through_shared_reference() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(10i32);
+ let shared = &container;
+ shared.modify_res(|v: &mut i32| *v += 5);
+ assert_eq!(*container.res::<i32>().unwrap(), 15);
+ }
+
+ #[test]
+ fn container_modify_res_clones_when_shared() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(10i32);
+ // Hold a shared handle so `Arc::try_unwrap` fails and the resource is cloned out
+ let handle = container.res::<i32>().unwrap();
+ container.modify_res(|v: &mut i32| *v += 5);
+ assert_eq!(*container.res::<i32>().unwrap(), 15);
+ assert_eq!(*handle, 10);
+ }
+
+ #[test]
+ fn container_extract_res_mut_takes_value_out() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(3i32);
+ let extracted: i32 = container.__extract_res_mut();
+ assert_eq!(extracted, 3);
+ // The slot is reset to a default value after the extraction
+ assert_eq!(*container.res::<i32>().unwrap(), 0);
+ }
+
+ #[test]
+ fn container_extract_res_mut_clones_when_shared() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(3i32);
+ // Hold a shared handle so `Arc::try_unwrap` fails and the resource is cloned out
+ let _handle = container.res::<i32>().unwrap();
+ let extracted: i32 = container.__extract_res_mut();
+ assert_eq!(extracted, 3);
+ // The slot is reset to a default value after the extraction
+ assert_eq!(*container.res::<i32>().unwrap(), 0);
+ }
+
+ #[test]
+ fn container_store_res_inserts_value() {
+ let container = GlobalResContainer::new();
+ container.__store_res(8i32);
+ assert_eq!(*container.res::<i32>().unwrap(), 8);
+ }
+
+ #[test]
+ fn container_extract_store_roundtrip() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(7i32);
+ let value: i32 = container.__extract_res_mut();
+ container.__store_res(value + 1);
+ assert_eq!(*container.res::<i32>().unwrap(), 8);
+ }
+
+ #[test]
+ fn container_res_shared_handles_share_the_arc() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(String::from("hello"));
+ let handle_a = container.res::<String>().unwrap();
+ let handle_b = container.res::<String>().unwrap();
+ assert_eq!(*handle_a, "hello");
+ assert_eq!(*handle_b, "hello");
+ assert!(Arc::ptr_eq(&handle_a.res_arc, &handle_b.res_arc));
+ }
+
+ #[test]
+ fn container_res_or_route_missing_returns_route() {
+ let container = GlobalResContainer::new();
+ let route: ChainProcess<MockProgramCollect> =
+ ChainProcess::Err(ChainProcessError::Other("missing".into()));
+ let result = container.res_or_route::<i32, MockProgramCollect>(route);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn container_res_or_route_present_returns_resource() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(5i32);
+ let route: ChainProcess<MockProgramCollect> =
+ ChainProcess::Err(ChainProcessError::Other("missing".into()));
+ let Ok(resource) = container.res_or_route::<i32, MockProgramCollect>(route) else {
+ panic!("expected the resource to be present");
+ };
+ assert_eq!(*resource, 5);
+ }
+
+ #[test]
+ fn container_modify_res_and_return_route_works() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32);
+ let route: ChainProcess<MockProgramCollect> =
+ container.__modify_res_and_return_route(|v: &mut i32| {
+ *v += 1;
+ ChainProcess::Err(ChainProcessError::Other("done".into()))
+ });
+ assert!(matches!(route, ChainProcess::Err(_)));
+ assert_eq!(*container.res::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn container_nested_modify_res_different_types_do_not_deadlock() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32).with_resource("a".to_string());
+ // Two `&mut` injections nest `modify_res` calls; each resource has its
+ // own mutex, so the inner call must not deadlock against the outer one.
+ container.modify_res(|count: &mut i32| {
+ *count += 10;
+ container.modify_res(|text: &mut String| {
+ text.push('b');
+ });
+ });
+ assert_eq!(*container.res::<i32>().unwrap(), 11);
+ assert_eq!(*container.res::<String>().unwrap(), "ab");
+ }
+
+ #[test]
+ fn container_nested_modify_res_and_return_route_no_deadlock() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32).with_resource("a".to_string());
+ let route: ChainProcess<MockProgramCollect> =
+ container.__modify_res_and_return_route(|count: &mut i32| {
+ *count += 10;
+ container.__modify_res_and_return_route(|text: &mut String| {
+ text.push('b');
+ ChainProcess::Err(ChainProcessError::Other("done".into()))
+ })
+ });
+ assert!(matches!(route, ChainProcess::Err(_)));
+ assert_eq!(*container.res::<i32>().unwrap(), 11);
+ assert_eq!(*container.res::<String>().unwrap(), "ab");
+ }
+
+ #[test]
+ fn container_res_inside_modify_of_another_resource() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32).with_resource("a".to_string());
+ container.modify_res(|count: &mut i32| {
+ // Reading a different resource while holding this one's lock must
+ // not deadlock either.
+ assert_eq!(*container.res::<String>().unwrap(), "a");
+ *count += 10;
+ });
+ assert_eq!(*container.res::<i32>().unwrap(), 11);
+ }
+
+ #[test]
+ fn container_multiple_instances_are_independent() {
+ let mut first = GlobalResContainer::new();
+ let mut second = GlobalResContainer::new();
+ first.with_resource(1i32);
+ second.with_resource(2i32);
+ first.modify_res(|v: &mut i32| *v += 10);
+ assert_eq!(*first.res::<i32>().unwrap(), 11);
+ assert_eq!(*second.res::<i32>().unwrap(), 2);
+ // A resource present in one container is invisible to the other
+ assert!(first.res::<String>().is_none());
+ assert!(second.res::<String>().is_none());
+ }
+
+ #[test]
+ fn program_resource_methods_delegate_to_container() {
+ let mut program = crate::Program::<MockProgramCollect>::new_with_args(Vec::<String>::new());
+ program.with_resource(1i32);
+ assert_eq!(*program.res::<i32>().unwrap(), 1);
+ program.modify_res(|v: &mut i32| *v += 1);
+ assert_eq!(*program.res::<i32>().unwrap(), 2);
+ assert_eq!(*program.res_or_default::<i32>(), 2);
+ assert_eq!(*program.res_or_default::<String>(), "");
+ }
}