While working on a fix, I ran into a deadlock with Dashmap and when I tried to look up the reasons online, I found a bunch of people facing the same issue!
Here's a little deeper dive into the problem and the fix.
The problematic code:
if clients.contains_key(client_name) {
let client = clients.get(client_name).unwrap();
// if the env vars haven't changed, return the cached client
if !client.has_env_vars_changed(ctx.env_vars()) {
return Ok(client.provider.clone());
}else{
// if the env vars have changed, remove the client from the cache, and create a new one.
println!("removing client {}", client_name);
clients.remove(client_name); // <-------- Deadlock!!
println!("removed client {}", client_name);
}
}
So if you see Dashmap's get
method, it returns a Ref
to the map value which means we're holding a reference into the map
and the author makes it explicitly clear in the docs that it may deadlock if called when holding a mutable reference into the map.
But that's not the case here because a deadlock happens when we try to remove the client from the map since remove tries to use the same mutable reference that we're holding.
/// Get an immutable reference to an entry in the map
///
/// **Locking behaviour:** May deadlock if called when holding a mutable reference into the map.
///
/// # Examples
///
/// ```
/// use dashmap::DashMap;
///
/// let youtubers = DashMap::new();
/// youtubers.insert("Bosnian Bill", 457000);
/// assert_eq!(*youtubers.get("Bosnian Bill").unwrap(), 457000);
/// ```
pub fn get<Q>(&'a self, key: &Q) -> Option<Ref<'a, K, V, S>>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self._get(key)
}
/// Removes an entry from the map, returning the key and value if they existed in the map.
///
/// **Locking behaviour:** May deadlock if called when holding any sort of reference into the map.
///
/// # Examples
///
/// ```
/// use dashmap::DashMap;
///
/// let soccer_team = DashMap::new();
/// soccer_team.insert("Jack", "Goalie");
/// assert_eq!(soccer_team.remove("Jack").unwrap().1, "Goalie");
/// ```
pub fn remove<Q>(&self, key: &Q) -> Option<(K, V)>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self._remove(key)
}
The fix? Try to get a clone of the client when doing a get, which is probably not the best solution but it works!
if clients.contains_key(client_name) {
let client = clients.get(client_name).map(|c| c.clone()).unwrap();
// if the env vars haven't changed, return the cached client
if !client.has_env_vars_changed(ctx.env_vars()) {
return Ok(client.provider.clone());
}else{
// if the env vars have changed, remove the client from the cache, and create a new one.
println!("removing client {}", client_name);
clients.remove(client_name); // Works!
println!("removed client {}", client_name);
}
}