How do I use `tower::Service` with Axum's `with_state`?

I am trying to write a toy web server that gets user info from an OAuth provider and displays their name on the index page.

I have a tower::Service impl that takes in a request; an auth response from oauth provider, and does a few things with a database backend via Diesel.

Service Impl

pub(crate) struct AppOAuthEntry {
    // Fairly nested 'inner'
    inner: GetAuth<...<...<...<...<...<...<...<GetSession>>>>>>>>,
}

impl Service<AuthResponse> for AppOAuthEntry {
    type Response = UserSession;
    ...
    ...

Each is a service with a corresponding tower::Layer

pub(crate) struct GetAuthLayer {
    auth_exists: AuthExists<StoreError>,
    auth_create: AuthCreate<StoreError>,
}

Backend Database Services

Each of those fields are BoxCloneService and look like this

type AuthExists<E> = BoxCloneService<Request, Response, E>

BoxCloneService because

  1. Layer requires the services to be Clone to create Service instance

    impl<S> Layer<S> for GetAuthLayer {
        type Service = GetAuth<S>;
    
        fn layer(&self, inner: S) -> Self::Service {
            GetAuth {
                inner,
                auth_exists: self.auth_exists.clone(),
                auth_create: self.auth_create.clone(),
            }
        }
    }
    
  2. So I could swap backend if needed, as long as that backend implements required Service trait.

    Here's an example of one such backend

    pub(crate) struct Postgres {
        pool: Pool<ConnectionManager<PgConnection>>,
    }
    

    where Service impl is basically this

    fn call(&mut self, Create(req): Create<Request>) -> Self::Future {
        ...
        ...
        let connection = self.get();
        ...
        ...
        async move {
            let resp = connection?.transaction(|conn| {
                let record = diesel::insert_into(auth::table)
                    .values(req)
                    .get_result(conn)?;
    
                Ok(record)
            })?;
    
            Ok(resp)
        }
        .boxed()
    }
    
    

Axum Server

Finally, this is how I want to use it with Axum

    let app = Router::new()
        .route("/", get(route::index))
        .nest(
            "/login",
            Router::new()
                .route(
                    "/google",
                    get(route::login::google).with_state(...),
                )
                ...
                ...
                ...
        )
        .with_state(AppOAuthEntry::new(...));
                     ^^^
                     ^^^

Errors

It was here that I found out none of my Service impls are Sync! And BoxCloneService is specifically !Sync

help: the trait `Sync` is not implemented for
`(dyn tower::util::boxed_clone::CloneService...

This refers to those struct fields that are BoxCloneService like AuthCreate.

Which leads to this error

35  | pub(crate) struct AppOAuthEntry {
    |                   ^^^^^^^^^^^^^
note: required by a bound in `Router::<S, B>::new`
    |
105 |     S: Clone + Send + Sync + 'static,
    |                       ^^^^ required by this bound in
          `Router::<S, B>::new`

Question(s)

So my question is, what should I do to fix this?

I haven't tried it yet, but perhaps wrapping those store services in Rc<RefCell<BoxCloneService<...>>> could work? Seems the easiest solution but I am not sure if it'd be safe.

Maybe using Mutex? Although I don't think it's worth it because none of those services are modifying internal state (they are just wrappers around db transaction).

BoxService does implement Sync but not Clone so maybe using Arc like Arc::clone(...) could work, although I won't be able to borrow the service mutably which is required by call method.

No, Rc and RefCell are single-threaded abstractions. All they can do is remove Send/Sync. They can't add thread safety.

Yes, Mutex<T>: Sync if T: Send.

No, it explicitly doesn't.

Yep that did it. At first I thought I had to wrap each field of the struct with Arc<Mutex<...>> but that got out of hand very quickly. Finally I realized this suffices

.with_state(Arc::new(Mutex::new(AppOAuthEntry::new(...))));

As for the BoxService implementing Sync, google result pointed me to this util: make `BoxService` impl `Sync` via `SyncWrapper` by davidpdrsn · Pull Request #702 · tower-rs/tower · GitHub but that's conditional Sync impl which seems doesn't apply to this situation.

Thanks for the help!

It seems like BoxService could be made sync without issues.

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.