Mutable sub-structures and references to them

Architectural dilemma. I have a big struct, let's call Simulation, with graph and some more data in it.

I need to run path algorithms, and for this I need calculator structs that borrow the graph. It worked fine with Arc, until I needed mutability of Graph.

struct Simulation {
    graph1: Graph
}

struct Graph { 
    costs: Vec<(usize, usize, usize)>,
    edges_geom: Vec<LineString>
}

struct PathCalculator {
    graph: Arc<Graph>,
    heap: BinaryHeap<(usize, usize)>,
    // etc.
}

How do I pack it?

Ways I see:

  1. Keep Arc and use .into() to unpack when mutability is needed? Requires moving data from heap to stack and back. (Fortunately, edits are very rare, like once in several seconds.)
  2. just a plain reference -- this requires 'static for multi-threaded code, which I hesitate to do (I suspect this may lead to memory leaks).
  3. Arc<RwLock<T>> turns just every line of code in endless pain. You mut get read() guard for every breath, and also handle an error, that requires re-packing (because Err contains a reference!).
  4. Arc<Mutex> will block any parallel access.
  5. Box<T>? No idea if it will work.

More pain to consider: Simulation is stored in Arc<RwLock<...>> inside a state struct of an async Axum server.

Any suggestions are welcome.

Keep Arc and use .into() to unpack when mutability is needed? Requires moving data from heap to stack and back. (Fortunately, edits are very rare, like once in several seconds.)

That's moving just two Vec values, i.e. 6 address-sized values, not any of the data owned by the Vecs. Trivial for your rate of usage.

just a plain reference -- this requires 'static for multi-threaded code

You can use std::thread::scope or rayon to run parallel algorithms that borrow data. (But this doesn't help if you want to run them “in the background”, i.e. without having any &Graph for the duration.)

Box<T>? No idea if it will work.

Adding a Box wouldn't solve (or create) any sharing/mutability problems.

2 Likes

Thanks for explaining. I have another similar struct inside Simulation, that has its own calculator struct, and that one will need background calculations and I'll probably have to put it under RwLock, but the rest of the structs will be stored plainly.

As for Arc, I found out .to_owned() is fallible too, and checks if refs count is 1. This makes it not much easier to work with than Arc<RwLock<T>>.

There's one more requirement here: I do some calculations on the struct in background, but if an edit comes from an API, calculations should be cancelled. This will probably need a queue of tasks stored somewhere in the struct, and workers should get and soon release the read lock.

I do some calculations on the struct in background, but if an edit comes from an API, calculations should be cancelled. This will probably need a queue of tasks stored somewhere in the struct, and workers should get and soon release the read lock.

Note that there may be three parties here: the incoming edits, the calculations, and whoever's reading the results. It can be useful to consider them separately.

For example, instead of looking at it as a data structure that is shared between the three parties, you could have a task which loops running the background calculations, and each time through the loop checks if any edits are incoming (delivered to it through a channel) and also writes updated results to another channel or a RwLocked copy of the data. This way, the readers can read even when the calculations are in progress; they are only blocked when the copy is being written.

That may not be the best idea for your use case (which you haven't given much detail on) — I just mean to illustrate that you can consider more options than just "here is some data, which must be locked for mutation".

1 Like

I tried sketching such an app in Rust Explorer, and your vision looks correct. I tried making it simpler, but indeed I'll need those three parties that you write about. (In the sketch, I tried to simplify it, but it's obvious this won't work.)