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

Implement request-state local cache #655

Closed
wants to merge 6 commits into from

Conversation

vhakulinen
Copy link
Contributor

Opening this PR for feedback. Based completely on @SergioBenitez's design.

I guess the next step is to extend the state crate to allow more relaxed bounds for T in the local_cache?

The cache needs to be wrapped in Rc so it gets Clone, but it was mentioned that in future Request might be send to another thread to be processed, so should we already use Arc instead?

Simple example usage https://gist.github.com/vhakulinen/fb84746fa102761c0683f694523a47cd.

Closes #654.

@SergioBenitez
Copy link
Member

Please add some doc-tests, unit tests, and integration tests.

@SergioBenitez SergioBenitez mentioned this pull request Jun 12, 2018
@vhakulinen vhakulinen closed this Jun 16, 2018
@vhakulinen vhakulinen reopened this Jun 16, 2018
@vhakulinen
Copy link
Contributor Author

Sorry, didn't mean to close this PR.

@SergioBenitez
Copy link
Member

Depending on "wall time" for the test is error prone. Instead, lets use something like two request guards each which access the same pair of AtomicUsizes. One of the atomics should be incremented in the closure that initializes the cached value, and the other should be incremented outside of the closure. Thus, after calling a handler that makes use of both request guards, the atomic accessed inside the initialization closure should have a value of one while the other should have a value of two.

In pseudocode, this is:

#[get("/")]
fn handler(Guard1, Guard2) { .. }

impl FromRequest for {Guard1, Guard2} {
    req.guard::<State<Atomics>>()?.first.increment();
    req.local_cache(|req| req.guard::<State<Atomics>>()?.second.increment());
}

fn main() {
    rocket::ignite().manage(Atomics);
}

#[test]
fn test() {
    client.get("/").dispatch();
    assert_eq!(client.rocket().state::<Atomics>().unwrap().first.value(), 2);
    assert_eq!(client.rocket().state::<Atomics>().unwrap().second.value(), 1);
}

type Error = ();

fn from_request(req: &'a Request<'r>) -> request::Outcome<Self, ()> {
req.guard::<State<Atomics>>()?.first.fetch_add(1, Ordering::Relaxed);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just call the Guard1 request guard here.

second: AtomicUsize,
}

struct Guard1();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for () here or when it's constructed.

let client = Client::new(rocket()).unwrap();
client.get("/").dispatch();

assert!(client.rocket().state::<Atomics>().unwrap().first.load(Ordering::Relaxed) == 2, "First is two");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Store the reference to Atomics in a variable to clean these assert!s up:

let atomics = client.rocket().state::<Atomics>().unwrap();
assert_eq!(atomics.uncached.load(Ordering::Relaxed), 2);
assert_eq!(atomics.cached.load(Ordering::Relaxed), 2);

#[cfg(test)] mod tests;

struct Atomics {
first: AtomicUsize,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename these cached and uncached.


fn rocket() -> rocket::Rocket {
rocket::ignite()
.manage(Atomics{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, derive Default for Atomics and call Atomics::default() here.

/// ```
pub fn local_cache<T, F>(&self, f: F) -> &T
where T: Send + Sync + 'static,
F: FnOnce(&Request) -> T {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought: why are we passing self to F? The caller already has a handle to request, so they can use it if they need to. I think this should probably be F: FnOnce() -> T.



#[get("/")]
fn index(_g1: Guard1, _g2: Guard2) {

This comment was marked as resolved.

type Error = ();

fn from_request(req: &'a Request<'r>) -> request::Outcome<Self, ()> {
req.guard::<State<Atomics>>()?.first.fetch_add(1, Ordering::Relaxed);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull out the atomics state loading:

let atomics = req.guard::<State<Atomics>>()?;

atomics.uncached.fetch_add(1, Ordering::Relaxed);
req.local_cache(|| atomics.cached.fetch_add(1, Ordering::Relaxed));

Guard1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change wasn't made.


fn from_request(req: &'a Request<'r>) -> request::Outcome<Self, ()> {
req.guard::<State<Atomics>>()?.first.fetch_add(1, Ordering::Relaxed);
req.local_cache(|req| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is saving a value of type () in the cache. I suppose that's fine, but let's at least leave a comment indicating this.

@vhakulinen
Copy link
Contributor Author

Updated the PR based on feedback.

I've been wondering whether or not the use of trait would be better for local_cache instead of FnOnce. I've yet to come up with compelling example, where such situation would be favorable - I guess it would only make it a bit more explicit that the cache is per type.

client.get("/").dispatch();

let atomics = &client.rocket().state::<Atomics>().unwrap();
assert!(atomics.uncached.load(Ordering::Relaxed) == 2, "First is two");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the string comment. Also, use assert_eq. Same for the one below.

let client = Client::new(rocket()).unwrap();
client.get("/").dispatch();

let atomics = &client.rocket().state::<Atomics>().unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the & there.

Copy link
Contributor Author

@vhakulinen vhakulinen Jun 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmh, when I tried without &, I had to de-reference ((*atomics).cached...) the value when I wanted to access it, for some reason. Seems to work without it now.

Success(Guard2)
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kill this extra empty line.

type Error = ();

fn from_request(req: &'a Request<'r>) -> request::Outcome<Self, ()> {
req.local_cache(|| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the change in the previous comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see I commited my experiments.

@SergioBenitez
Copy link
Member

Merged in 97c6b3a. Hoorah! Thanks so much for this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr: merged This pull request was merged manually.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants