-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Support non-blocking cache invalidation #22683
Conversation
cc @rsvoboda |
66c53b9
to
219cd1c
Compare
@mkouba would you mind reviewing this one? I think you're the best person for it given Stuart is not around. |
Just FYI, played with the |
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ;-)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
aUni.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]; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :-)
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great!
219cd1c
to
0bc0c69
Compare
0bc0c69
to
0a153c8
Compare
@cescoffier / @mkouba does this PR have green light from you now? |
There was a problem hiding this 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.
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 aUni
. It means that the following code will work:On the other hand, this one will keep blocking the event loop thread and should not be used with resteasy-reactive:
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.