Cursor of HashMap records with RwLockGuard in Rust
Asked Answered
M

1

7

I am new to Rust, and I am trying to implement a simple, thread-safe memory key-value store, using a HashMap protected within a RwLock. My code looks like this:

use std::sync::{ Arc, RwLock, RwLockReadGuard };
use std::collections::HashMap;
use std::collections::hash_map::Iter;

type SimpleCollection = HashMap<String, String>;

struct Store(Arc<RwLock<SimpleCollection>>);

impl Store {
    fn new() -> Store { return Store(Arc::new(RwLock::new(SimpleCollection::new()))) }

    fn get(&self, key: &str) -> Option<String> {
        let map = self.0.read().unwrap();
        return map.get(&key.to_string()).map(|s| s.clone());
    }

    fn set(&self, key: &str, value: &str) {
        let mut map = self.0.write().unwrap();
        map.insert(key.to_string(), value.to_string());
    }
}

So far, this code works OK. The problem is that I am trying to implement a scan() function, which returns a Cursor object that can be used to iterate over all the records. I want the Cursor object to hold a RwLockGuard, which is not released until the cursor itself is released (basically I don't want to allow modifications while a Cursor is alive).

I tried this:

use ...

type SimpleCollection = HashMap<String, String>;

struct Store(Arc<RwLock<SimpleCollection>>);

impl Store {
    ...

    fn scan(&self) -> Cursor {
        let guard = self.0.read().unwrap();
        let iter = guard.iter();
        return Cursor { guard, iter };
    }
}

struct Cursor<'l> {
    guard: RwLockReadGuard<'l, SimpleCollection>,
    iter: Iter<'l, String, String>
}

impl<'l> Cursor<'l> {
    fn next(&mut self) -> Option<(String, String)> {
        return self.iter.next().map(|r| (r.0.clone(), r.1.clone()));
    }
}

But that did not work, as I got this compilation error:

error[E0597]: `guard` does not live long enough
  --> src/main.rs:24:20
   |
24 |         let iter = guard.iter();
   |                    ^^^^^ borrowed value does not live long enough
25 |         return Cursor { guard, iter };
26 |     }
   |     - borrowed value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the method body at 22:5...
  --> src/main.rs:22:5
   |
22 | /     fn scan(&self) -> Cursor {
23 | |         let guard = self.0.read().unwrap();
24 | |         let iter = guard.iter();
25 | |         return Cursor { guard, iter };
26 | |     }
   | |_____^

Any ideas?

Misplay answered 23/11, 2018 at 1:40 Comment(12)
While Cursor is alive, you want nobody else to modify your HashMap? Is that the purpose of all that?Avert
@Avert correct.Misplay
This could help?Landlord
Could you put your code into the playground so that we can more easily play around with it?Banquette
Btw. it is very unidiotmatic to use return as a last statement. Instead just remove return and the ; , e.g. fn foo() -> u32 { 3 }Avert
@SebastianRedl Here you go: play.rust-lang.org/…Misplay
@Avert Even some of the core developers prefer to always use return for consistency, so I wouldn't call this "unidiomatic"; it's just a question of personal style.Noisy
@SvenMarnach #27962379 doc.rust-lang.org/book/first-edition/…: "Using a return as the last line of a function works, but is considered poor style:"Avert
Is it necessary for you have the iterator inside the Cursor ? If not, this is another way (may not be ideal)Landlord
@AymanMadkour You are essentially trying to create a self-referential struct, which is not easily possible in Rust (see this question for more information).Noisy
@Avert I know. There's also this question and a Clippy lint. I personally don't use the return in the last statement, but don't feel strongly about what other people should do.Noisy
Possible duplicate of Why can't I store a value and a reference to that value in the same struct?Bordelaise
N
6

As mentioned in the comments, the problem is that structs generally can't be self-referential in Rust. The Cursor struct you are trying to construct contains both the MutexGuard and the iterator borrowing the MutexGuard, which is not possible (for good reasons – see the linked question).

The easiest fix in this case is to introduce a separate struct storing the MutexGuard, e.g.

struct StoreLock<'a> {
    guard: RwLockReadGuard<'a, SimpleCollection>,
}

On the Store, we can then introduce a method returning a StoreLock

fn lock(&self) -> StoreLock {
    StoreLock { guard: self.0.read().unwrap() }
}

and the StoreLock can expose the actual scan() method (and possibly others requiring a persistent lock):

impl<'a> StoreLock<'a> {
    fn scan(&self) -> Cursor {
        Cursor { iter: self.guard.iter() }
    }
}

The Cursor struct itself only contains the iterator:

struct Cursor<'a> {
    iter: Iter<'a, String, String>,
}

Client code first needs to obtain the lock, then get the cursor:

let lock = s.lock();
let cursor = lock.scan();

This ensures that the lock lives long enough to finish scanning.

Full code on the playground

Noisy answered 23/11, 2018 at 15:27 Comment(1)
Thanks a lot, @SvenMarnach. I guess I am still not used to the Rust mindset.Misplay

© 2022 - 2024 — McMap. All rights reserved.