Requiring a Sync Future in a tokio app

Hey, I have implemented an async cache and the cache requires the user to create it with a loader function. The loader function should return a Sync Future:

impl<K, V, E> LoadingCache<K, V, E>
where
    K: Eq + Hash + Send + Sync + 'static,
    V: Send + Sync + 'static,
{
    pub fn new<FUser, Fut, Q>(loader: FUser, expire_after_write: Duration) -> Self
    where
        K: Borrow<Q>,
        FUser: Fn(&Q) -> Fut + Send + Sync + 'static,
        Fut: Future<Output = Result<V, E>> + Send + Sync + 'static, // <- Here
    {
        Self::with_capacity(loader, expire_after_write, 0)
    }

    pub fn with_capacity<FUser, Fut, Q>(
        loader: FUser,
        expire_after_write: Duration,
        capacity: usize,
    ) -> Self
    where
        K: Borrow<Q>,
        FUser: Fn(&Q) -> Fut + Send + Sync + 'static,
        Fut: Future<Output = Result<V, E>> + Send + Sync + 'static,
    {
        LoadingCache {
            map: Arc::new(DashMap::with_capacity(capacity)),
            loader: Arc::new(move |k| Box::pin((loader)(k.as_ref().borrow()))),
            expire_after_write,
            stats: Arc::new(Default::default()),
        }
    }
}

When trying to use it with redis-rs, I am creating the loader as such:

    let loader = |_| get_custom_domains(&mut conn);
    Ok(LoadingCache::new(loader, Duration::from_secs(10 * 60)))

where:

pub async fn get_custom_domains(redis_conn: &mut Connection) -> Result<HashSet<String>> {
    let redis_resp = redis_conn.smembers("custom-domains").await;
    match redis_resp {
        Ok(data) => Ok(data),
        Err(e) => Err(anyhow!("failed to get custom domains fomr redis: {}", e)),
    }
}

But this fails to compile:

error: future cannot be shared between threads safely
  --> src/cache.rs:19:8
   |
19 |       Ok(LoadingCache::new(loader, Duration::from_secs(10 * 60)))
   |          ^^^^^^^^^^^^^^^^^ future returned by `get_custom_domains` is not `Sync`
   | 
  ::: src/ttl_cache.rs:45:5
   |
45 | /     pub fn new<FUser, Fut, Q>(loader: FUser, expire_after_write: Duration) -> Self
46 | |     where
47 | |         K: Borrow<Q>,
48 | |         FUser: Fn(&Q) -> Fut + Send + Sync + 'static,
49 | |         Fut: Future<Output = Result<V, E>> + Send + Sync + 'static,
   | |___________________________________________________________________- required by `LoadingCache::<K, V, E>::new`
   |
   = help: the trait `Sync` is not implemented for `dyn Future<Output = Result<HashSet<std::string::String>, RedisError>> + Send`
note: future is not `Sync` as it awaits another future which is not `Sync`
  --> src/redis.rs:35:22
   |
35 |     let redis_resp = redis_conn.smembers("custom-domains").await;
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here on type `Pin<Box<dyn Future<Output = Result<HashSet<std::string::String>, RedisError>> + Send>>`, which is not `Sync`
  • Is this because inherently the redis-rs API returns a !Sync future? Maybe this is related to the API taking a &mut Connection? Or is it something else causing this?
  • Can this be worked around by creating a proper loader closure?

Are you sure that you need the future to be Sync in the first place? Usually it's enough for futures to be Send.

At this point, I am not. But, I think that refactoring out the Sync of the current implementation is hard enough. IIUC the cache can spawn a task on its own invoking the loader, and several different threads can poll this future, but it might not be enough to require Sync.

If what you claim is correct, then my API is certainly not usable enough. But if we assume at this point that Sync is needed for the cache. Is there a workaround?

The truth is that Sync is never required for futures because they have no &self methods. It will probably work if you take out the + Sync from you box. For the same reason, it would be safe to wrap the future in something that has an unsafe impl Sync on it. There's a crate for it, but I forget which one.

https://docs.rs/sync_wrapper/0.1.0/sync_wrapper/struct.SyncWrapper.html

great! Actually removing Sync from the API wasn't so hard. I was sure it wasn't going to compile.

very good and useful
thank you

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.