DioxusLabs / DioxusLabs/dioxus
[Feature]: Document Proper Use of Axum `State`/`Extension` Instead of Global Mutexes In FullStack
- Dominant language
- Rust
- Stars
- 39.1k
- Forks
- 1.9k
- Avg merge
- 4d 10h
- Merged PRs (30d)
- 4
Description
Server functions use a single, global `Mutex` without any safeguards against nested locks. When one function holding the lock calls another that also acquires it, you get a deterministic deadlock.
## Issue
- **Nested `.lock().await` on the same `Mutex`**
```rust
static GLOBAL_LOCK: Lazy> = Lazy::new(|| Mutex::new(()));
#[server]
async fn foo() -> Result<(), ServerFnError> {
let _guard = GLOBAL_LOCK.lock().await; // <- acquire the lock
bar().await?; // <- call while still holding
Ok(())
}
#[server]
async fn bar() -> Result<(), ServerFnError> {
let _guard = GLOBAL_LOCK.lock().await; // <- deadlock
Ok(())
}
```
Because `foo()` never drops its guard before invoking `bar()`, `bar()` blocks forever attempting to re-acquire the same lock.
- **Deterministic Hang**: Every invocation of `foo()` will stall at `bar().await`, freezing the server function and any UI interaction that triggered it.
- **Async Executor Starvation**: Although `async_std::sync::Mutex` is non-blocking at the thread level, tasks awaiting the mutex still deadlock the executor's task queue, leading to application unresponsiveness.
## PoC
1. Create a new dioxus fullstack app with the following code in `main.rs`:
```rust
use dioxus::prelude::*;
use once_cell::sync::Lazy;
use async_std::sync::Mutex;
static GLOBAL_LOCK: Lazy> = Lazy::new(|| Mutex::new(()));
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
let mut status = use_signal(|| "Idle".to_string());
rsx! {
div {
h1 { "Status: {status}" }
button {
onclick: move |_| {
async move {
let _ = foo().await;
status.set("Done!".to_string());
}
},
"Trigger Deadlock"
}
}
}
}
#[server]
async fn foo() -> Result<(), ServerFnError> {
let _guard = GLOBAL_LOCK.lock().await;
bar().await?;
Ok(())
}
#[server]
async fn bar() -> Result<(), ServerFnError> {
let _guard = GLOBAL_LOCK.lock().await;
Ok(())
}
```
1. Run the client:
```sh
dx serve --port 3000
```
1. Click the "Trigger Deadlock" button in the UI about 10 times.
1. The server-side `foo()` holds `GLOBAL_LOCK` and calls `bar()`.
1. `bar()` awaits the same mutex, no other code can proceed, and the UI freezes on page refresh.
https://github.com/user-attachments/assets/45be398c-d1ae-4725-9dac-acff14c0ea23
## Fix
Add a static-analysis check that flags any call where a single `Mutex`'s `.lock().await` appears more than once before the first guard is dropped.
```sh
error[E0502]: Deadlock detected: `GLOBAL_LOCK` is awaited in `foo()` and again in `bar()` before the first guard is released.
```
- **Release Mode**: In Release mode, treat this as a hard error to enforce lock-safety on all global mutexes.
## Conclusion
By detecting nested `.lock().await` calls on the same mutex at compile time, Dioxus cli can **fail fast** on deadlock patterns. This prevents unresponsive server functions, and enforces safer async practices across all full-stack applications.
## Edit
I encountered this issue while building Dioxus fullstack projects (e.g. [aibook](https://github.com/opensass/aibook)). At the time, I was storing [the AI client](https://github.com/opensass/aibook/blob/2d954e22da791dd6e21b9cbe75de4bc318b11f3f/src/ai.rs#L18) in a shared `Mutex`. Occasionally, requests would hang, but due to the project's relatively large codebase (~15k LoC), it wasn't immediately clear that a deadlock was the cause.
Now, as a more experienced dev, I realize that the AI client should be stored as shared Axum state using [State](https://docs.rs/axum/latest/axum/extract/struct.State.html)/[Extension](https://docs.rs/axum/latest/axum/struct.Extension.html) (e.g. [this db state example](https://github.com/opensass/opensass/blob/9dffb23072723226c7a897de83f929cd1bef9866/src/main.rs#L61)) rather than behind a mutex. This approach avoids unnecessary locking and aligns with idiomatic async Rust.
Deadlocks in server environments are especially dangerous, they're silent, hard to detect, and disproportionately hard to debug in large codebases. To prevent such issues, I strongly recommend updating the documentation to advise against using mutexes for shared state in async contexts, and to promote Axum state for correct and safe usage.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.