Skip to content

[feature request]: Add #[must_not_suspend] lint attribue for Refs. #348

@javalsai

Description

@javalsai

Using dashmap in an async context has a high risk of accidentally deadlocking by awaiting while holding a reference into the hashmap.

Demo Project

main.rs

use std::sync::Arc;

use dashmap::DashMap;

async fn sleep() {
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}

async fn other_task(map: Arc<DashMap<u8, u8>>) {
    println!("other task locks");
    let mut n = map.get_mut(&0).unwrap();
    *n += 1;
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let map = Arc::new(DashMap::<u8, u8>::default());
    map.insert(0, 0);

    tokio::task::spawn(other_task(map.clone()));
    println!("main task locks");
    let mut n = map.get_mut(&0).unwrap();
    sleep().await;
    *n += 1;

    println!("Hello, world!");
}

Cargo.toml

[package]
name = "lint"
version = "0.1.0"
edition = "2024"

[dependencies]
dashmap = "6.1.0"
tokio = { version = "1.46.1", features = [
    "macros",
    "rt",
    "rt-multi-thread",
    "time",
] }

Such code deadlocks with no warnings:

➜ cargo run
    [... updates and compiles ...]
     Running `target/debug/lint`
main task locks
other task locks
^C

Thankfully, there's a lint attribute in rust nightly that prevents marked structs from being held through an action that might suspend (e.g. .await) called #[must_not_suspend].

Sadly is still in nightly but It would be very useful to have this as an opt-in unstable feature to help catch potential deadlocks in projects that are on nightly.

I took the time to clone the repo and with just the final patch it's possible to make the demo code warn about the deadlock.

I'm not sure about the name of the feature and probably should be added to "multiple" Refs too (not sure as I haven't read about those at all).

Demo Project Changes

Add to main.rs

#![feature(must_not_suspend)]
#![warn(must_not_suspend)]

And the output finally is:

Compiling lint v0.1.0 (/tmp/lint)
warning: `dashmap::mapref::one::RefMut` held across a suspend point, but should not be
  --> src/main.rs:25:9
   |
25 |     let mut n = map.get_mut(&0).unwrap();
   |         ^^^^^
26 |     sleep().await;
   |             ----- the value is held across this suspend point
   |
help: consider using a block (`{ ... }`) to shrink the value's scope, ending before the suspend point
  --> src/main.rs:25:9
   |
25 |     let mut n = map.get_mut(&0).unwrap();
   |         ^^^^^
note: the lint level is defined here
  --> src/main.rs:2:9
   |
2  | #![warn(must_not_suspend)]
   |         ^^^^^^^^^^^^^^^^

warning: `lint` (bin "lint") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/lint`
main task locks
other task locks

EDIT: If someone really wants to use this I've made a fork of the project implementing this for all types of Refs at https://github.com/javalsai/dashmap-async.


diff --git a/Cargo.toml b/Cargo.toml
index 946e9e2..d072594 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,7 @@ all = ["raw-api", "typesize", "serde", "rayon", "arbitrary"]
 raw-api = []
 typesize = ["dep:typesize"]
 inline-more = ["hashbrown/inline-more"]
+unstable-must-not-suspend = []
 
 [dependencies]
 lock_api = "0.4.12"
diff --git a/src/lib.rs b/src/lib.rs
index 2a9c620..bff9859 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,6 @@
 #![doc = include_str!("../README.md")]
 #![allow(clippy::type_complexity)]
+#![cfg_attr(feature = "unstable-must-not-suspend", feature(must_not_suspend))]
 
 #[cfg(feature = "arbitrary")]
 mod arbitrary;
diff --git a/src/mapref/one.rs b/src/mapref/one.rs
index faf47d9..eac11e4 100644
--- a/src/mapref/one.rs
+++ b/src/mapref/one.rs
@@ -3,6 +3,7 @@ use core::hash::Hash;
 use core::ops::{Deref, DerefMut};
 use std::fmt::{Debug, Formatter};
 
+#[cfg_attr(feature = "unstable-must-not-suspend", must_not_suspend)]
 pub struct Ref<'a, K, V> {
     _guard: RwLockReadGuardDetached<'a>,
     k: &'a K,
@@ -74,6 +75,7 @@ impl<'a, K: Eq + Hash, V> Deref for Ref<'a, K, V> {
     }
 }
 
+#[cfg_attr(feature = "unstable-must-not-suspend", must_not_suspend)]
 pub struct RefMut<'a, K, V> {
     guard: RwLockWriteGuardDetached<'a>,
     k: &'a K,

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions