Keep global hashmap values in sync with struct hashmap values

Viewed 204

I'm using a globally accessible HashMap to store homogoneous entities, Roster, and Person.

Now my Roster structs also have a HashMap of entities, Roster.names, which is a subset of the entities inside the global HashMap, MY_ITEMS, so Roster.names⊂MY_ITEMS,

Naturally I want to get mutable references to MY_ITEMS so that they can "interact with" and mutate each other. The problem is that in the course of mutating some specific entities via a function, other entities might need to be updated as well as a side effect.

But within the context of match MY_ITEMS.lock().unwrap().get_pair_mut("roster 1", "roster 2"){...} we can no longer borrow or mutate anything from MY_ITEMS. We only have mutable access to the two entities we requested via get_pair_mut(). So any attempts to update some third value in MY_ITEMS within this scope will result in a WouldBlock.

Here's a generalization of the problem:

SET has A,B, and C
B.C = SET.C.clone()
get &mut A and &mut B from SET{
        A.change(B){
            updates(B.C)
            updates(B)
        }
}
SET.B.C !== SET.C

Here's some trivial code showing this design problem:

multi_mut = "0.1.3"
lazy_static = "1.4.0"
use lazy_static::lazy_static;
use std::sync::Mutex;
use std::collections::HashMap;
use multi_mut::{HashMapMultiMut};

//globally accessible Hashmap
lazy_static! {
    static ref MY_ITEMS: Mutex<HashMap<String, Entity>> = Mutex::new(HashMap::new());
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Person{
    pub roster_id: Option<String>,
    name: String,
    id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Roster{
  pub id: String,
  pub names: HashMap<String, Entity>
}
//wrapper enum
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Entity{
    Roster(Roster),
    Person(Person),
}

impl Roster{
    pub fn new(id: String, start_names: Vec<(String, Person)>)->Self{
        let mut map = HashMap::new();
        map.extend(start_names.clone().into_iter().map(|n|{
            let mut altered = n.1.clone();
            altered.roster_id = Some(id.clone());
            //on adding a Person to a Roster, we also add them to the global map
            MY_ITEMS.lock().unwrap().insert(n.0.clone(), Entity::Person(altered.clone()));
            return (n.0, Entity::Person(altered))
        }));
        Roster{
            id: id, 
            names: map
        }
    }
    pub fn take_person(&mut self, from_roster: &mut Roster, to_take_id: String){
        //take a person from the other roster
        let person_entry = from_roster.names.remove_entry(&to_take_id).unwrap();
        //alter their information to reflect a change in which roster the person belongs too
        if let Entity::Person(mut p) = person_entry.1.clone(){
            p.roster_id = Some(self.id.clone());
            //update the roster with the changed persons value
            self.names.insert(person_entry.0.clone(), Entity::Person(p.clone()));
            //trying to insert the updated value into the global hashmap will cause a lock to fail
            //so value of person inside of this roster and inside of the global map becomes out of sync
            // MY_ITEMS.try_lock().unwrap().insert(person_entry.0.clone(), Entity::Person(p.clone()));
        }
    }
}

fn main(){
    //starting people for roster 1
    let group_1 = vec![
    (String::from("Person 1"), Person{roster_id: None, name: String::from("Josie"), id: String::from("Person 1")}),
    (String::from("Person 2"), Person{roster_id: None, name: String::from("Joon"), id: String::from("Person 2")}),
    (String::from("Person 3"), Person{roster_id: None, name: String::from("Glooby"), id: String::from("Person 3")})];
    
    let roster_1 = Roster::new(String::from("roster 1"), group_1);

    //starting people for roster 2
    let group_2 = vec![
        (String::from("Person 4"), Person{roster_id: None, name: String::from("Borg"), id: String::from("Person 4")}),
        (String::from("Person 5"), Person{roster_id: None, name: String::from("Snog"), id: String::from("Person 5")}),
        (String::from("Person 6"), Person{roster_id: None, name: String::from("Cloob"), id: String::from("Person 6")})];
    
    let roster_2 = Roster::new(String::from("roster 2"), group_2);

    //add rosters to the global map
    MY_ITEMS.lock().unwrap().insert(roster_1.id.clone(), Entity::Roster(roster_1.clone()));
    MY_ITEMS.lock().unwrap().insert(roster_2.id.clone(), Entity::Roster(roster_2.clone()));
    
    //remove Person 4, Borg, from roster 2 and insert them into roster 1
    match MY_ITEMS.lock().unwrap().get_pair_mut("roster 1", "roster 2"){
        //becuase of get_pair_mut, we can no longer lock MY_ITEMS in this block scope
        Some((r1_wrapper, r2_wrapper))=>{
            match (r1_wrapper, r2_wrapper){
                (Entity::Roster(r1), Entity::Roster(r2))=>{
                    r1.take_person(r2, String::from("Person 4"));
                }
                _=>()
            }
        }
        _=>()
    };


    if let Entity::Roster(r1) = MY_ITEMS.lock().unwrap().get("roster 1").unwrap(){
        //the roster_id field for Person 4 is updated in our roster_2 local variable
        dbg!(r1.names.get("Person 4"));//->Some(Person)
        //but it is not updated in our global map
        dbg!(roster_1.clone().names.get("Person 4"));//->None
        assert_eq!(r1.names.get("Person 4"), roster_1.names.get("Person 4"), "Roster value in local does not equal roster value in global");
    }
}

So what could we do to keep the values we're using in our structs in sync with the values in our global map?

A couple of thoughts:

  • It seems that we'd either have to know before mutating any values in MY_ITEMS, that we'd need to know all the items we'd like mutate.
  • multi_mut does have a function for iterating over an arbitrary number of HashMap values, iter_multi_mut(keys, buffer), but this doesn't seem like avery flexible design.
  • We could also potentially have match MY_ITEMS.lock().unwrap().get_pair_mut() return some kind of FnMut(Mutex<HashMap<String, Entitity>>) that would perform the side effects for us.
  • We could also send an event over a channel with the update information.

As a side note I think its important to keep all of People structs and all of our Roster structs in the same globally accessible variable. But maybe there is some other container struct besides a HashMap that would better accommodate the dependencies between items.

Another sidenote, because rust's HashMap has no convenient way of mutating multiple values in a HashMap, I'm using the multi_mut crate's get_pair_mut() method to get my mutable references to two entities in MY_ITEMS.

Maybe there is a different pattern or design which would be better suited to this?

  • Edit: Here's a playground example of using Arc<Mutex>, but I think that wrappers are not a scalable solution. I think a better solution would involve segmenting all of the Entities that we'd like to mutate, then iterating over them.

Also I know see that this issue is a pretty common one to rust. Perhaps this and this article on shared mutability in Rust could point to a better design.


0 Answers
Related