How to create a lock guard which wraps multiple lock guards?

I'm working on a project which involves building a file system. This "file system" just wraps standard File structs and implements encryption and authentication for the data stored in them. The file system struct uses three tiers of locks to synchronize access to files:

  • There's a top-level reader-writer lock on a table indexed by inode.
  • For each inode, the table entry is protected by another reader-writer lock.
  • Finally, inside each table entry there is another table of handles. Each value of this table
    (each file handle) is protected by a mutex.

I've been struggling to make the reading of file data zero-copy and able to be done in async code. My last approach was to create an RAII guard type which contains the guards for the three locks described above, and which can be dereferenced as a slice.

struct FirstGuard<'a> {
    handle_guard: MutexGuard<'a, Vec<u8>>,
    inode_guard: RwLockReadGuard<'a, InodeTableEntry>,
    table_guard: RwLockReadGuard<'a, InodeTable>,
}

impl<'a> FirstGuard<'a> {
    async fn new(table: &'a RwLock<InodeTable>, inode: Inode, handle: Handle) -> FirstGuard<'a> {
        let table_guard = table.read().await;
        let inode_guard = table_guard.get(&inode).unwrap().read().await;
        let handle_guard = inode_guard.handles.get(&handle).unwrap().lock().await;
        Self {
            table_guard,
            inode_guard,
            handle_guard,
        }
    }
}

(I'm using Vec<u8> to stand-in for files.)

This fails with an error because table_guard and inode_guard cannot be moved while they are borrowed, which makes sense. So I thought I could get around this by having the caller pass in a mutable reference to the guard. Then I could initialize this reference inside my FS struct after which the caller could dereference it. The full example is on the playground. This does not work due to what appears to be a limitation in the borrow checker's support for Generic Associated Types.

Does anyone have any suggestions on how I can work-around this error? Or, taking a step back, is there a better way to solve this problem?

Before trying this I was using a callback-based approach, where the callback was executed while the locks were held so the caller didn't need to own any guards. This approach worked for synchronous callbacks, but failed for ones which returned futures, which I posted about here.

  • If you are using tokio::sync locks, you can use their owned guards (e.g. tokio::sync::RwLock::read_owned()) which do not borrow anything — but you have to put each of the locks in Arc.

  • A general solution to borrows of data in the same struct is ouroboros, but this has some overhead and risk of turning out unsafe because of its trickery, so don't use it if you have good alternatives.

Since I am using tokio::sync locks, the owned lock approach seems like it's the cleaner and perhaps even the lower overhead solution (ouroboros requires futures to be boxed when fields need to be initialized with async code). I managed to get zero-copy reading working today using this approach. Thanks for the help!

I updated the code I linked to previously with owned lock guards: link to The Playground.

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.