Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request-Local State Cache #654

Closed
vhakulinen opened this issue Jun 6, 2018 · 2 comments
Closed

Request-Local State Cache #654

vhakulinen opened this issue Jun 6, 2018 · 2 comments
Labels
docs Improvements or additions to documentation enhancement A minor feature request
Milestone

Comments

@vhakulinen
Copy link
Contributor

vhakulinen commented Jun 6, 2018

Rocket doesn't currently support "per request cache". Usually this is done with middlewares (#55) on other frameworks (and Go does this with Context). Such functionality is usually used to store (as an example) loaded User object to the context of current request. This way, if the user object is needed in multiple places in the request handling path, the same object can be used.

Here's an simple example of such use case: https://gist.github.com/vhakulinen/5f72f380d5959b7f00435fc473b2c1e3. In the cargo run file, you can see that the user object is being loaded multiple times, and this cannot currently be avoided in any non-hacky way.

That said, I'm interested in implementing this feature, as I said earlier in Matrix. I don't yet have any plan yet other that to extend the Request or RequestState to have context property which would act as a cache. This is so that both fairings and request guards have access to the per request cache.

Example usage (for request guard) would be following:

struct User {
    uid: usize
}

struct PermissionViewContent{}

impl<'a, 'r> FromRequest<'a, 'r> for PermissionViewContent {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> request::Outcome<PermissionViewContent, ()> {
        let u = request.guard::<User>()?;
        if u.uid != 1 {
            return Outcome::Forward(())
        }

        Outcome::Success(PermissionViewContent{})
    }
}

impl<'a, 'r> FromRequest<'a, 'r> for User {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> request::Outcome<User, ()> {
        if let Some(user) = request.context.get("my-unique-user-key") {
            return user;
        }

        let u = ...database access and so on... 

        request.context.set("my-unique-user-key", u);

        return u;
    }
}

I'm quite new to rust, so there probably is more 'rust' way to do this. I was wondering if something like the request.guard::<User>() could be implemented for the per request cache, but that would be limiting in some ways: for example, you'd be limited to have one cached User object per request.

@SergioBenitez SergioBenitez changed the title Per request cache Request-Local State Cache Jun 6, 2018
@SergioBenitez SergioBenitez added the enhancement A minor feature request label Jun 6, 2018
@SergioBenitez
Copy link
Member

SergioBenitez commented Jun 6, 2018

Here's the document I wrote up for this feature:

Request-Local State Cache

Motivation

When we have routes that forward to each-other with guards that access the same data, it's unfortunate that the data has to be retrieved for each guard and each route during request processing. For instance, if there are two routes, the first with an AdminUser guard and the second with a User guard, both will need to query the database; this may be quite expensive. It would be nice if Rocket had a facility to cache the retrieved data for the lifetime of the request. This can also help with being consistent about decisions through the request. We don't want to see changes to the data source while a single request is being processed.

Design

The request-local state cache will be accessible anywhere an &Request is available. Any state stored in the request-local state cache is dropped along with the Request. Access to the cache is typed in a similar vain to managed state. Unlike managed state, however, accessing request-local cached state is infallible: accesses cannot fail. This design decision makes it significantly more difficult to misuse request-local cached state. Curtailing errors here is particularly important as accesses to request-local cached state are likely to be in the hot-path of request processing, and thus failures here result in user-visible errors.

Accessing (setting and retrieving) request-local cached state is accomplished via a single method on Request of the following form:

impl<'r> Request<'r> {
    /// Retrieves the cached value for type `T` from the request-local cached
    /// state of `self`. If no such value has previously been cached for
    /// this request, `f` is called to produce the value which is subsequently
    /// returned.
    fn local_cache<T, F>(&self, f: F) -> &T
        where T: Send + 'r,
              F: FnOnce(&Request) -> T;
}

Note that there are no failure modes for this method.

The method places a Send restriction on the stored typed T; this is required so that a Request can be transferred to another thread to be handled there. While this doesn't happen at the moment, it will happen with certainty in the future.

Implementation

The implementation is straight-forward:

  1. Add a new cache: state::Container field to the RequestState struct in core/lib/src/request.rs and initialize it as empty in Request::new. The Container type is from the state crate.

  2. Implement the local_cache method by:

    a. Calling cache.try_get and returning the value if it exists.

    b. Calling cache.set(f()) if the value doesn't exist followed by cache.get().

The state::Container type of state requires that the type of any values stored implement Send + Sync + 'static, but the bounds for T in local_cache aren't as strict. As a result, either the state crate should be extended to allow values with more lenient bounds or the local_cache method must enforce the stricter bounds.

To start, the latter will suffice, but the former should be implemented before public release.

@SergioBenitez
Copy link
Member

Going to re-open this to track guide documentation (in the state guide) for this feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Improvements or additions to documentation enhancement A minor feature request
Projects
None yet
Development

No branches or pull requests

2 participants