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

Support non-blocking cache invalidation #22683

Merged

Conversation

gwenneg
Copy link
Member

@gwenneg gwenneg commented Jan 6, 2022

Fixes #22431.

Based on @stuartwdouglas' suggestion in the Quarkus mailing-list.

The cache invalidation is now possible with resteasy-reactive as long as the method annotated with @CacheInvalidate or @CacheInvalidateAll returns a Uni. It means that the following code will work:

@Path("/value")
public class ReactiveResource {
    @GET
    @CacheInvalidate(cacheName = "mycache")
    public Uni<Void> getValue() {
        return Uni.createFrom().nullItem();
    }
}

On the other hand, this one will keep blocking the event loop thread and should not be used with resteasy-reactive:

@NonBlocking
@Path("/value")
public class ReactiveResource {
    @GET
    @CacheInvalidate(cacheName = "mycache")
    public void getValue() {
    }
}

⚠️ I am preparing an update for the quarkus-cache doc related to the integration between the extension and Mutiny. It will mention this as well as other important things. I'll create a separate PR for it if that's OK.

@gwenneg
Copy link
Member Author

gwenneg commented Jan 6, 2022

cc @rsvoboda

@gwenneg gwenneg force-pushed the issue-22431-cache-invalidate-non-blocking branch from 66c53b9 to 219cd1c Compare January 6, 2022 10:17
@gsmet
Copy link
Member

gsmet commented Jan 7, 2022

@mkouba would you mind reviewing this one? I think you're the best person for it given Stuart is not around.

@rsvoboda
Copy link
Member

rsvoboda commented Jan 7, 2022

Just FYI, played with the revert.50d90aa branch of quarkus-test-suite and using Uni<Void> as return type for reactive endpoints makes the tests pass when executed against patched main.

void testCacheResult() {
// STEP 1
// Action: a method annotated with @CacheResult and returning a Uni is called.
// Expected effect: the method is not invoked, as Uni is lazy.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that I understand the reasoning but I find this rather counter-intuitive. As the method is annotated with @CacheResult I would expect the method to be executed but the result of the invocation to be cached. But I'm no reactive expert so it might be perfectly OK.

@cescoffier @jponge WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

Everything (invocation and caching of the result) happens at subscription time. This PR doesn't change that part of the extension BTW. I refactored CacheResultInterceptor a bit to get rid of runtime lambdas, nothing more.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, I'm not saying that it's a change or anything but this is the first time I see it... and it's not documented anyway so ;-)

Copy link
Member Author

Choose a reason for hiding this comment

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

It was changed recently though with #22271.

Copy link
Member Author

Choose a reason for hiding this comment

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

and it's not documented anyway so ;-)

It will be shortly, I'm on it 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

I would naively expect that the method is invoked, the result is cached and a next invocation would return a new Uni for the cached result. But like I said - I'm no expert in this area at all.

Copy link
Member

Choose a reason for hiding this comment

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

That always puzzled me.
With:

@CahcedResult
Uni<Person> getPerson() { ... }

What is cached? The Uni? The emitted Person?

If you cache the Uni, resubscribing will re-execute the logic, so probably not what we want to achieve.
Caching the person makes more sense to me, but I'm happy to discuss it.

Copy link
Member Author

@gwenneg gwenneg Jan 7, 2022

Choose a reason for hiding this comment

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

The original Uni is never cached by the extension. What is cached is an UnresolvedUniValue at first which is then replaced with the actual item emitted by the original Uni once it's been resolved. After that, when the cached value is accessed, a Uni.createFrom().item(theResolvedValue) is returned.

This is another part of the doc update I'm working on 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

In other words, the method body is not called until someone subscribes to the Uni which is returned by the interceptor, right? That probably makes sense.

My naive expectation was that the method is called and once someone subscribes to the returned Uni a Uni.createFrom().item(theResolvedValue) is returned. That's very similar but I assume the current approach makes more sense from caching POV.

Copy link
Member Author

@gwenneg gwenneg Jan 10, 2022

Choose a reason for hiding this comment

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

In other words, the method body is not called until someone subscribes to the Uni which is returned by the interceptor, right? That probably makes sense.

Exactly.

My naive expectation was that the method is called and once someone subscribes to the returned Uni a Uni.createFrom().item(theResolvedValue) is returned. That's very similar but I assume the current approach makes more sense from caching POV.

The method was invoked before the subscription until recently but it caused a bug with resteasy-reactive (blocked thread) which was fixed by @stuartwdouglas.

private Object invalidateNonBlocking(InvocationContext invocationContext,
CacheInterceptionContext<CacheInvalidate> interceptionContext) {
// Because of this array, the key will only be built once and reused by each iteration of the following Multi.
Object[] key = new Object[1];
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you be more specific? Why the array? Is it just a holder of a reference that can be only set on the same thread?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes it's a holder reference to "hack" the lambda restriction about final variables. The idea is to avoid recalculating the same key each time an invalidation is performed because the key value will always be the same.

I'm not entirely sure, but the interceptor execution may involve several threads as Merge implies concurrency in Mutiny. But the key value will always be the same and if it's not available to a thread (null) then it will be calculated.

Copy link
Contributor

Choose a reason for hiding this comment

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

If so I'd say that AtomicReference is a little bit more expensive because of the volatile field... but also a bit less "hacky" IMO :-)

Copy link
Member Author

Choose a reason for hiding this comment

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

After re-reading the extension code, I think there's an issue (unrelated with the holder reference) with reusing the same key...

Copy link
Member Author

@gwenneg gwenneg Jan 10, 2022

Choose a reason for hiding this comment

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

I just fixed a bug with the reusable cache key during an invalidation.

When a method annotated with @CacheInvalidate does not have any arguments, the cache name is used to build a default key. Reusing the same key means that the following code won't work as one of the caches won't be invalidated. It's not a common scenario as most cached methods have arguments, but still a bug (old one) 😃

@CacheInvalidate(cacheName = "foo")
@CacheInvalidate(cacheName = "bar")
public void invalidate() {
}

Thanks for questioning the holder reference, it made me realize this 😄 The hack is gone now.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll add a test for this case in a few minutes.

Copy link
Member Author

Choose a reason for hiding this comment

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

Test added.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great!

@gwenneg gwenneg force-pushed the issue-22431-cache-invalidate-non-blocking branch from 219cd1c to 0bc0c69 Compare January 10, 2022 14:35
@gwenneg gwenneg force-pushed the issue-22431-cache-invalidate-non-blocking branch from 0bc0c69 to 0a153c8 Compare January 10, 2022 14:53
@rsvoboda
Copy link
Member

@cescoffier / @mkouba does this PR have green light from you now?

Copy link
Member

@gsmet gsmet left a comment

Choose a reason for hiding this comment

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

Let's get this in. AFAICS, the remaining discussions are more general than related to this specific PR.

@gsmet gsmet merged commit 2e4a786 into quarkusio:main Jan 12, 2022
@quarkus-bot quarkus-bot bot added this to the 2.7 - main milestone Jan 12, 2022
@gwenneg gwenneg deleted the issue-22431-cache-invalidate-non-blocking branch January 12, 2022 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

IllegalStateException, reactive jaxrs - Don't block when returning void
5 participants