Skip to main content

khora_core/lane/
lock.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Lock helpers shared by lanes and backends.
16//!
17//! Replace `.lock().unwrap()` and `.read()/.write().unwrap()` in hot paths
18//! with [`mutex_lock`] / [`read_lock`] / [`write_lock`] so a poisoned
19//! lock yields a [`LaneError::LockPoisoned`] (or a [`RenderError`]) instead
20//! of crashing the frame.
21//!
22//! These helpers live in `khora-core` so both `khora-lanes` and
23//! `khora-infra` can use them without crossing crate boundaries.
24
25use crate::lane::LaneError;
26use crate::renderer::error::{RenderError, ResourceError};
27use std::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
28
29/// Locks a `Mutex<T>`, mapping poisoning to [`LaneError::LockPoisoned`].
30///
31/// ```ignore
32/// let guard = mutex_lock(&shared, "my-lane.shared")?;
33/// ```
34pub fn mutex_lock<'a, T: ?Sized>(
35    lock: &'a Mutex<T>,
36    context: &'static str,
37) -> Result<MutexGuard<'a, T>, LaneError> {
38    lock.lock().map_err(|_| LaneError::lock_poisoned(context))
39}
40
41/// Acquires a read lock, mapping poisoning to [`LaneError::LockPoisoned`].
42pub fn read_lock<'a, T: ?Sized>(
43    lock: &'a RwLock<T>,
44    context: &'static str,
45) -> Result<RwLockReadGuard<'a, T>, LaneError> {
46    lock.read().map_err(|_| LaneError::lock_poisoned(context))
47}
48
49/// Acquires a write lock, mapping poisoning to [`LaneError::LockPoisoned`].
50pub fn write_lock<'a, T: ?Sized>(
51    lock: &'a RwLock<T>,
52    context: &'static str,
53) -> Result<RwLockWriteGuard<'a, T>, LaneError> {
54    lock.write().map_err(|_| LaneError::lock_poisoned(context))
55}
56
57/// Locks a `Mutex<T>`, mapping poisoning to a [`RenderError`].
58///
59/// Use from GPU-init / GPU-shutdown paths that return
60/// `Result<_, RenderError>` (the GPU error type) — the
61/// `LaneError`-returning [`mutex_lock`] does not compose with `?`
62/// there.
63pub fn mutex_lock_render<'a, T: ?Sized>(
64    lock: &'a Mutex<T>,
65    context: &'static str,
66) -> Result<MutexGuard<'a, T>, RenderError> {
67    lock.lock().map_err(|_| {
68        RenderError::ResourceError(ResourceError::BackendError(format!(
69            "{}: lock poisoned",
70            context
71        )))
72    })
73}
74
75/// Like [`mutex_lock_render`] but for `RwLock<T>` write access.
76pub fn write_lock_render<'a, T: ?Sized>(
77    lock: &'a RwLock<T>,
78    context: &'static str,
79) -> Result<RwLockWriteGuard<'a, T>, RenderError> {
80    lock.write().map_err(|_| {
81        RenderError::ResourceError(ResourceError::BackendError(format!(
82            "{}: lock poisoned",
83            context
84        )))
85    })
86}
87
88/// Like [`mutex_lock_render`] but for `RwLock<T>` read access.
89pub fn read_lock_render<'a, T: ?Sized>(
90    lock: &'a RwLock<T>,
91    context: &'static str,
92) -> Result<RwLockReadGuard<'a, T>, RenderError> {
93    lock.read().map_err(|_| {
94        RenderError::ResourceError(ResourceError::BackendError(format!(
95            "{}: lock poisoned",
96            context
97        )))
98    })
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::sync::Arc;
105
106    #[test]
107    fn mutex_lock_succeeds() {
108        let m = Mutex::new(42);
109        let guard = mutex_lock(&m, "test").unwrap();
110        assert_eq!(*guard, 42);
111    }
112
113    #[test]
114    fn mutex_lock_returns_lane_error_when_poisoned() {
115        let m = Arc::new(Mutex::new(0));
116        let m_clone = Arc::clone(&m);
117        let _ = std::thread::spawn(move || {
118            let _g = m_clone.lock().unwrap();
119            panic!("poison the mutex");
120        })
121        .join();
122        let err = mutex_lock(&m, "poisoned").unwrap_err();
123        assert!(matches!(
124            err,
125            LaneError::LockPoisoned {
126                context: "poisoned"
127            }
128        ));
129    }
130
131    #[test]
132    fn rwlock_read_succeeds() {
133        let l = RwLock::new(7);
134        let g = read_lock(&l, "test").unwrap();
135        assert_eq!(*g, 7);
136    }
137
138    #[test]
139    fn rwlock_write_succeeds() {
140        let l = RwLock::new(0);
141        {
142            let mut g = write_lock(&l, "test").unwrap();
143            *g = 9;
144        }
145        assert_eq!(*l.read().unwrap(), 9);
146    }
147}