Is there a way to enforce that a borrow doesn't come from a tokio::sync::RwLockReadGuard?

Since tokio::sync::RwLockReadGuard<T> implements Deref<T>, you're able to pass it to a function whose signature is like this fn(val: &T). Assuming T is Clone + Send + Sync + 'static is there an elegant way to handle something like this?

For example, if you write something like this, it should compile however it will deadlock.

#[tokio::test]
async fn test() {
    let test = std::sync::Arc::new(tokio::sync::RwLock::new(10usize));

    async fn announce(val: &usize, test: std::sync::Arc<tokio::sync::RwLock<usize>>) {
        println!("{val}");
        // This will deadlock
        let _ = test.write().await;
    }

    let read = test.read().await;
    announce(&read, test.clone()).await;
}

The way to fix this would've been,

#[tokio::test]
async fn test() {
    let test = std::sync::Arc::new(tokio::sync::RwLock::new(10usize));

    async fn announce(val: &usize, test: std::sync::Arc<tokio::sync::RwLock<usize>>) {
        println!("{val}");
        let _ = test.write().await;
    }

    let read = test.read().await;
    let _read = read.to_owned();
    drop(read);
    announce(&_read, test.clone()).await;
}

I was wondering if there was a clever way to handle this.

I'm not aware of any way to do this. Even if it was possible, what you're asking for looks like at most a partial solution to me, too, anyways: in your own code example, without the drop(read), it would still dead-lock, even though the &_read argument does not borrow from an RwLockReadGuard.

2 Likes

You can newtype something that doesn't Deref (but still have to remember to use it).

You could add more newtypes I suppose, depending on how much you control.

1 Like

Hmm it doesn't deadlock when I run it,

Edit
Oh I think I misunderstood. Yes, I know that the drop is required, and I guess I'm looking for a language semantic that somehow ends up doing a ref-to-ref conversion. If it's impossible that's fine too.

I built on top of your idea and came up with this so that you don't have to call .to_owned(),

The key was here,

struct AutoGuardReadGuard<T>
where
    T: ToOwned + Default + Send + Sync + 'static,
{
    guard: std::cell::RefCell<Option<tokio::sync::OwnedRwLockReadGuard<T>>>,
    val:  std::cell::OnceCell<T>,
}

impl<T> std::ops::Deref for AutoGuardReadGuard<T>
where
    T: ToOwned<Owned = T> + Default + Send + Sync + 'static,
{
    type Target = T;

    fn deref(&self) -> &Self::Target {
        self.val.get_or_init(|| {
            self.guard
                .replace(None)
                .map(|v| v.to_owned())
                .unwrap_or_default()
        })
    }
}

Seems to work.

I've made the realization that what I'm effectively doing is more like this,

#[tokio::test]
async fn test_ref_to_ref() {
    let test = std::sync::Arc::new(tokio::sync::RwLock::new(10usize));
    async fn announce(val: &usize, test: std::sync::Arc<tokio::sync::RwLock<usize>>) {
        println!("{val}");
        let mut _val = test.write().await;
        *_val = *val + 1;
    }

    let read = test.latest().await;
    announce(&read, test.clone()).await;

    let read = test.latest().await;
    announce(&read, test.clone()).await;
    println!("exit");
}

/// Returns the latest value from a reference,
/// 
#[async_trait]
pub trait Latest<T>
where
    T: ToOwned<Owned = T> + Send + Sync + 'static, 
{
    /// Returns the latest value,
    /// 
    async fn latest(&self) -> T;
}

#[async_trait]
impl<T> Latest<T> for tokio::sync::RwLock<T> 
where
    T: ToOwned<Owned = T> + Send + Sync + 'static,  
{
    async fn latest(&self) -> T {
        self.read().await.to_owned()
    }
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.