Cache for either sync or async I/O

I'm working on a crate that is meant to handle sync or async I/O, so I'd like to store the values as either Option<Value> or Future<Option<Value>>.

Then I have a fetch_file and fetch_file_sync where the sync will reject if the file is cached async, but async should also be able to return a cached sync.

I'm struggling to design the proper signatures and entries to store the values and return them.
I'm using Rc, but I could also use a reference or Arc if that works better:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5db5ca3ac7e5db7f030ae179e8be27d6

It's meant to be +/- port of https://github.com/zbraniecki/L10nRegistry/blob/master/lib/sources.js#L39-L56

Is such API possible in stable Rust at the moment?

Mixing the two will make things exponentially more complex. I suggest implementing them separately, or treating everything as async (sync I/O is a case of async spawn).

Futures have a single owner, which means only one thing can wait on them. But in a cache you will have potentially any number of callers waiting for the same future. For this, you'll need to make it Shared: https://docs.rs/futures/0.3.5/futures/future/struct.Shared.html

Arc is advised, since Rc is non-Send, and non-Send futures are much harder to work with.

2 Likes

Thank you @kornel!

With your advice I got it mostly to work!

I ran into an issue with RefCell on HashMap cache tho, which I'm not sure if its solvable using Rc - I need to make sure that my borrow is not extending to the async so that two fetch_file can borrow the same cache.

Here's the failing test - https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=3baa97111b97c33630cbb2243228e125

Am I using RefCell properly here?

In case of hashmaps the trick is to clone their content while you have the lock, and immediately drop the lock.

Make ResourceStatus cloneable, and then:

        let opt = cache.entry(full_path.to_owned()).or_insert_with(|| {
            let res = read_resource(full_path.clone()).boxed_local().shared();
            ResourceStatus::Async(res)
        }).clone();
        drop(cache);
1 Like

BTW, when an async function doesn't need arguments while awaiting (like in your case), it's efficient to change async fn to regular fn that returns impl Future.
async fn creates a future that holds a copy of its arguments, which is limiting for callers.

2 Likes

Thanks!

How should I wrap a non-feature in Feature for the return type impl Future in the Sync branch in such case?

Looking for a Rust equiv of JS'es return Promise.resolve(value);

async move {} is Promise.resolve(). You will have to wrap both together, because impl Trait wants one type only, and every future is a unique snowflake.

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.