Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sharing mutable state between clients using async (tokio) rust-websocket

I am writing a websocket server in Rust using rust-websocket and its Tokio-based async system. I can serve clients just fine, however, I can not figure out how to share mutable state between the clients. Here is some (partial) code demonstrating this issue:

let mut core = Core::new().unwrap();
let handle = core.handle();
let server = Server::bind("localhost:62831", &handle).unwrap();

let mut state = State{
    ...
};

let f = server.incoming()
    .map_err(|InvalidConnection {error, ..}| error)
    .for_each(|upgrade, _)| {
        let f = upgrade.accept()
            .and_then(|s, _| {
                let ctx = ClientContext{
                    // some other per-client values
                    state: &mut state,
                }
                ...
                return s.send(Message::binary(data).into())
                    .and_then(move |s| Ok(s, ctx)); // this could be the complete wrong way to insert context into the stream
            }).and_then(|s, ctx| {
                // client handling code here
            });

            handle.spawn(f
                .map_err(...)
                .map(...)
            );
            return Ok(())
    });

core.run(f).unwrap();

This code errors with this:

error[E0373]: closure may outlive the current function, but it borrows `**state`, which is owned by the current function
   --> src/main.rs:111:27
    |
111 |                 .and_then(|(s, _)| {
    |                           ^^^^^^^^ may outlive borrowed value `**state`
...
114 |                         state: &mut state,
    |                                     ----- `**state` is borrowed here
    |
help: to force the closure to take ownership of `**state` (and any other referenced variables), use the `move` keyword, as shown:
    |                 .and_then(move |(s, _)| {

When trying the compiler's suggestion, I get this:

error[E0507]: cannot move out of captured outer variable in an `FnMut` closure
   --> src/main.rs:111:27
    |
111 |                 .and_then(move |(s, _)| {
    |                           ^^^^^^^^^^^^^ cannot move out of captured outer variable in an `FnMut` closure

error: `state` does not live long enough
   --> src/main.rs:114:37
    |
114 |                         state: &mut state,
    |                                     ^^^^^ does not live long enough
...
122 |                 })
    |                 - borrowed value only lives until here
    |
    = note: borrowed value must be valid for the static lifetime...

I also tried wrapping the state in a RefCell (creating the RefCell right after the state itself), however, the compiler gives a similar move error since it tries to move the RefCell into the closure that creates the client context.

like image 513
rytone Avatar asked Jul 03 '17 02:07

rytone


1 Answers

You're pretty close with the RefCell. What you need now is an Rc to wrap that RefCell so you can clone the Rc and not capture the RefCell itself.

let shared_state = Rc::new(RefCell::new(State::new())));
incoming().for_each(move |s, _| {
    let shared_state = shared_state.clone();  // Left uncaptured
    shared_state.borrow_mut().do_mutable_state_stuff(); // Could panic
});

Note that since you're using Rc's and RefCell's now, you'll likely need to go ahead and convert your ClientContext struct to storing an Rc> instead of a &mut State. It may be possible to keep using &mut State's for some things, but your &mut State's will be tied to the lifetime of the RefMut, and if you keep it alive until the next closure runs, the borrows will panic (or fail if you use the try_ variants).

Also keep in mind if you decide you want to have multiple threads in your reactor, you will just need to change Rc to Arc, and RefCell to Mutex, which is a very natural conversion when it's needed.

like image 163
SpamapS Avatar answered Nov 17 '22 21:11

SpamapS