Search code examples
rustactix-web

How to use actix-redis session in async-graphql resolvers


I am trying to use session object with Redis as the storage in a distributed system in the signin, signup and signout resolvers to set and delete session for userid but having issues with that because actix' Session does not implement Send and cannot be used across threads. It has type: Rc<RefCell<actix_session::SessionInner>>

Question

  • What's the idiomatic way to handle such in async-graphql? I would like to do something like below:
#[Object]
impl User {
    async fn signin(&self, ctx: &Context<'_>) -> anyhow::Result<Vec<User>> {
        let session = ctx.data_unchecked::<Session>();
        session.insert("user_id", id);
        session.get::<UserId>("user_id");
        ...
    }
}

If I try the above, I get:

`Rc<RefCell<actix_session::SessionInner>>` cannot be shared between threads safely
within `actix_session::Session`, the trait `Sync` is not implemented for `Rc<RefCell<actix_session::SessionInner>>`
  • Also, where is the right place to create session in async-graphql context? I am trying this but would face the same issue:
#[post("/graphql")]
pub async fn index(
    schema: web::Data<MyGraphQLSchema>,
    req: HttpRequest,
    gql_request: GraphQLRequest,
) -> GraphQLResponse {
    let mut request = gql_request.into_inner();
    let session = req.get_session();
    request = request.data(session);

    schema.execute(request).await.into()
}

My app:

    let redis_key = Key::from(hmac_secret_from_env_var.expose_secret().as_bytes());
        App::new()
            .wrap(cors)
            .wrap(
                RedisSession::new(redis.get_url(), redis_key.master())
                    .cookie_http_only(true)
                    // allow the cookie only from the current domain
                    .cookie_same_site(cookie::SameSite::Lax),
            )
            .wrap(Logger::default())

Solution

  • I resolved this using a temporary hack. If you check session definition, you will notice that it wraps a RefCell as below and does not implement send

    pub struct Session(Rc<RefCell<SessionInner>>);
    

    Ideally, Session should implement sync, so that we can use it in a multi-threaded context.

    The approach I am using presently is to wrap in a SendWrapper, which isn't ideal as it is meant for when you're sure the wrapped item will be used in a single-threaded fashion.

    use std::ops::Deref;
    use actix_session::Session;
    use actix_web::Error;
    use send_wrapper::SendWrapper;
    use uuid::Uuid;
    
    #[derive(Clone, Debug)]
    struct Shared<T>(pub Option<SendWrapper<T>>);
    
    impl<T> Shared<T> {
        pub fn new(v: T) -> Self {
            Self(Some(SendWrapper::new(v)))
        }
    }
    
    impl<T> Deref for Shared<T> {
        type Target = T;
    
        fn deref(&self) -> &Self::Target {
            &*self.0.as_deref().clone().unwrap()
        }
    }
    
    type SessionShared = Shared<actix_session::Session>;
    
    
    pub struct TypedSession(SessionShared);
    
    impl TypedSession {
        const USER_ID_KEY: &'static str = "user_id";
    
        pub fn new(session: Session) -> Self {
            Self(Shared::new(session))
        }
    
        pub fn renew(&self) {
            self.0.renew();
        }
    
        pub fn insert_user_uuid(&self, user_id: Uuid) -> Result<(), Error> {
            self.0.insert(Self::USER_ID_KEY, user_id)
        }
    
        pub fn get_user_uuid(&self) -> Result<Option<Uuid>, Error> {
            self.0.get::<Uuid>(Self::USER_ID_KEY)
        }
    
        pub fn clear(&self) {
            self.0.clear()
        }
    }