Looking for appropriate data type to safely share configuration when using Tokio runtime


#1

I’m looking for a datatype to use (std lib or a crate that works on stable Rust) for the following:

I have Tokio event loop with many futures/threads reading from a single struct like this:

pub struct Token {
    pub token_type: String,
    pub access_token: String,
    pub expires_at: Instant,
}

Sharing this across my program with Arc works fine for this; except:

  • Once every now and then the struct needs to be safely updated in its entirety. These updates are very infrequent. There may be multiple writes at the same time, but it’s ok if they need to wait on each other. Reads may be on outdated data, I don’t really care, as long as the write will eventually propagate to all readers. The struct is small, it could be copied if necessary…?
  • The data structure needs to be future aware I imagine?
  • Bonus if reads are fast.

I tried all sorts of crates and just can’t get this to work. Can anyone point me in the right direction what I should look for?

The closest I came to a working solution is ArcSwap: https://docs.rs/arc-swap/0.3.0/arc_swap/struct.ArcSwapAny.html, but it seems this sometimes hangs in a loop waiting for a write lock (it does in my test suite). It probably does not play nicely with the event loop from Tokio? I’m not sure what I’m looking for except to describe it as “a struct that can be read from concurrently at all times, and is sometimes concurrently replaced with a new version”.


#2

Just now I tried https://docs.rs/chashmap/2.2.0/chashmap/struct.CHashMap.html with a single key (1) to see how that would work, and I run into the exact same problem: occasional hanging test processes with 100% CPU usage.

My theory is that the futures do not advance because Tokio is unaware what to do to make the locks acquirable. Is that correct? If I can’t use “regular” concurrent data structures with Tokio, then what do I do instead?


#3

If the performance requirements and general architecture of the program allow, you could confine all futures to a single thread and use Tokio’s current_thread reactor/executor. If the task for updating the struct is executing on that thread’s event loop, it’s guaranteed not to clash with any other task while doing so.


#4

That does seem to work. Is there a way around it if it turns out I need more threads in the future? Does such a thing simply not exist yet?


#5

Not a solution, but related: https://github.com/crossbeam-rs/crossbeam/issues/160


#6

This is very easy to accomplish in a GC’d language and difficult otherwise :slight_smile:.

Another idea would be to dedicate a thread that receives update requests (over a mpsc channel) and maintains the “master” copy of the data. After it updates the value, broadcast out the new value to receivers (via a separate channel). The receivers then poll this channel and update their local copy.

If the updates are indeed rare then reads stay cheap (readers read a local copy, zero synchronization), and readers can choose when to poll the channel to see if there’s an updated value.

You waste some memory but the struct is small so likely ok. You can also consider sharing an Arc of the struct with the readers, but likely not warranted if the struct is indeed small. The setup is a bit involved but it’s basically a classic message passing scheme.

I’m not sure how you serialize or synchronize updates of the data, or if it’s even necessary, but that might be something that would need extra consideration.

That said, my first suggestion would’ve been the same as @inejge’s.


#7

Just a random thought: a small ring of static Tokens, and an atomic usize to indicate the active one?