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

Infinispan - Support caching annotations #25300

Merged
merged 1 commit into from
Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/src/main/asciidoc/infinispan-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,43 @@ for Kubernetes deployments, Infinispan Console,
https://infinispan.org/docs/stable/titles/rest/rest.html#rest_v2_protobuf_schemas[REST API] or the
https://infinispan.org/docs/stable/titles/encoding/encoding.html#registering-sci-remote-caches_marshalling[Hot Rod Java Client].

[#infinispan-annotations-api]
== Caching using annotations

The Infinispan Client extension offers a set of annotations that can be used in a CDI managed bean to enable caching abilities with Infinispan.

[WARNING]
====
Caching annotations are not allowed on private methods.
They will work fine with any other access modifier including package-private (no explicit modifier).
====

=== @CacheResult

Loads a method result from the cache without executing the method body whenever possible.

When a method annotated with `@CacheResult` is invoked, Quarkus will use the method argument as the cache key and check in the cache whether the method has been already invoked.
Methods with multiple parameters are not allowed. For composite keys, define a Protobuf schema that will hold multiple values.
If a value is found in the cache, it is returned and the annotated method is never actually executed.
If no value is found, the annotated method is invoked and the returned value is stored in the cache using the computed key.
This annotation cannot be used on a method returning `void`.
karesti marked this conversation as resolved.
Show resolved Hide resolved

[NOTE]
====
Infinispan Client extension is not able yet to cache `null` values unlike the Quarkus-Cache extension.
====

=== @CacheInvalidate

Removes an entry from the cache.

When a method annotated with `@CacheInvalidate` is invoked, Infinispan will use the method argument as a cache key to try to remove an existing entry from the cache.
If the key does not identify any cache entry, nothing will happen.

=== @CacheInvalidateAll

When a method annotated with `@CacheInvalidateAll` is invoked, Infinispan will remove all entries from the cache.


== Querying

Expand Down
4 changes: 4 additions & 0 deletions extensions/infinispan-client/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonp-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mutiny-deployment</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
import io.quarkus.infinispan.client.runtime.InfinispanClientBuildTimeConfig;
import io.quarkus.infinispan.client.runtime.InfinispanClientProducer;
import io.quarkus.infinispan.client.runtime.InfinispanRecorder;
import io.quarkus.infinispan.client.runtime.cache.CacheInvalidateAllInterceptor;
import io.quarkus.infinispan.client.runtime.cache.CacheInvalidateInterceptor;
import io.quarkus.infinispan.client.runtime.cache.CacheResultInterceptor;
import io.quarkus.infinispan.client.runtime.cache.SynchronousInfinispanGet;
import io.quarkus.infinispan.client.runtime.graal.DisableLoggingFeature;
import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem;

Expand Down Expand Up @@ -93,6 +97,10 @@ InfinispanPropertiesBuildItem setup(ApplicationArchivesBuildItem applicationArch

feature.produce(new FeatureBuildItem(Feature.INFINISPAN_CLIENT));
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(InfinispanClientProducer.class));
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CacheInvalidateAllInterceptor.class));
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CacheResultInterceptor.class));
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CacheInvalidateInterceptor.class));
karesti marked this conversation as resolved.
Show resolved Hide resolved
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(SynchronousInfinispanGet.class));
systemProperties.produce(new SystemPropertyBuildItem("io.netty.noUnsafe", "true"));
hotDeployment.produce(new HotDeploymentWatchedFileBuildItem(META_INF + File.separator + HOTROD_CLIENT_PROPERTIES));

Expand Down
4 changes: 4 additions & 0 deletions extensions/infinispan-client/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-caffeine</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mutiny</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-netty</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.infinispan.client;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.enterprise.util.Nonbinding;
import javax.interceptor.InterceptorBinding;

import io.quarkus.infinispan.client.CacheInvalidate.List;

/**
* When a method annotated with {@link CacheInvalidate} is invoked, Quarkus will use the method argument as key to try to
* remove an existing entry from the Infinispan cache. If the key does not identify any cache entry, nothing will happen.
* <p>
* This annotation can be combined with {@link CacheResult} annotation on a single method. Caching operations will always
* be executed in the same order: {@link CacheInvalidateAll} first, then {@link CacheInvalidate} and finally
karesti marked this conversation as resolved.
Show resolved Hide resolved
* {@link CacheResult}.
*/
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
public @interface CacheInvalidate {

/**
* The name of the cache.
*/
@Nonbinding
String cacheName();

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@interface List {
CacheInvalidate[] value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.infinispan.client;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.enterprise.util.Nonbinding;
import javax.interceptor.InterceptorBinding;

import io.quarkus.infinispan.client.CacheInvalidateAll.List;

/**
* When a method annotated with {@link CacheInvalidateAll} is invoked, Quarkus will remove all entries from the Infinispan
* cache.
* <p>
* This annotation can be combined with {@link CacheResult} annotation on a single method. Caching operations will always
* be executed in the same order: {@link CacheInvalidateAll} first, then {@link CacheInvalidate} and finally
* {@link CacheResult}.
*/
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
public @interface CacheInvalidateAll {

/**
* The name of the cache.
*/
@Nonbinding
String cacheName();

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@interface List {
CacheInvalidateAll[] value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.quarkus.infinispan.client;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.enterprise.util.Nonbinding;
import javax.interceptor.InterceptorBinding;

/**
* When a method annotated with {@link CacheResult} is invoked, Quarkus will use the method argument as key and use it to check
* in the
* Infinispan cache if the method has been already invoked. If a value is found in the cache, it is returned and the
* annotated method is never actually
* executed. If no value is found, the annotated method is invoked and the returned value is stored in the cache using the
* computed key.
* <p>
* A method annotated with {@link CacheResult} is protected by a lock on cache miss mechanism. If several concurrent
* invocations try to retrieve a cache value from the same missing key, the method will only be invoked once. The first
* concurrent invocation will trigger the method invocation while the subsequent concurrent invocations will wait for the end
* of the method invocation to get the cached result. The {@code lockTimeout} parameter can be used to interrupt the lock after
* a given delay. The lock timeout is disabled by default, meaning the lock is never interrupted. See the parameter Javadoc for
* more details.
* <p>
* This annotation cannot be used on a method returning {@code void}. It can be combined with {@link CacheInvalidate} and
* {@link CacheInvalidateAll}
* annotations on a single method. Caching operations will always be executed in the same order: {@link CacheInvalidateAll}
* first, then {@link CacheInvalidate} and finally {@link CacheResult}.
* <p>
*/
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheResult {

/**
* The name of the cache.
*/
@Nonbinding
String cacheName();

/**
* Delay in milliseconds before the lock on cache miss is interrupted. If such interruption happens, the cached method will
* be invoked and its result will be returned without being cached. A value of {@code 0} (which is the default one) means
* that the lock timeout is disabled.
*/
@Nonbinding
long lockTimeout() default 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.infinispan.client.runtime.cache;

import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class CacheInterceptionContext<T extends Annotation> {

private final List<T> interceptorBindings;

public CacheInterceptionContext(List<T> interceptorBindings) {
Objects.requireNonNull(interceptorBindings);
this.interceptorBindings = Collections.unmodifiableList(interceptorBindings);
}

public List<T> getInterceptorBindings() {
return interceptorBindings;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package io.quarkus.infinispan.client.runtime.cache;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.function.Supplier;

import javax.inject.Inject;
import javax.interceptor.Interceptor.Priority;
import javax.interceptor.InvocationContext;

import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.commons.CacheException;
import org.jboss.logging.Logger;

import io.quarkus.arc.runtime.InterceptorBindings;
import io.smallrye.mutiny.Uni;

public abstract class CacheInterceptor {
karesti marked this conversation as resolved.
Show resolved Hide resolved

public static final int BASE_PRIORITY = Priority.PLATFORM_BEFORE;
protected static final String UNHANDLED_ASYNC_RETURN_TYPE_MSG = "Unhandled async return type";

private static final Logger LOGGER = Logger.getLogger(CacheInterceptor.class);

@Inject
RemoteCacheManager cacheManager;

/*
* The interception is almost always managed by Arc in a Quarkus application. In such a case, we want to retrieve the
* interceptor bindings stored by Arc in the invocation context data (very good performance-wise). But sometimes the
* interception is managed by another CDI interceptors implementation. It can happen for example while using caching
* annotations on a MicroProfile REST Client method. In that case, we have no other choice but to rely on reflection (with
* underlying synchronized blocks which are bad for performances) to retrieve the interceptor bindings.
*/
protected <T extends Annotation> CacheInterceptionContext<T> getInterceptionContext(InvocationContext invocationContext,
Copy link
Member

Choose a reason for hiding this comment

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

CacheInterceptionContext makes sense in quarkus-cache because it contains two fields (interceptor bindings and key params positions). Here, it could be replaced with a simple list of interceptor bindings.

Class<T> interceptorBindingClass) {
return getArcCacheInterceptionContext(invocationContext, interceptorBindingClass)
.orElseGet(new Supplier<CacheInterceptionContext<T>>() {
@Override
public CacheInterceptionContext<T> get() {
return getNonArcCacheInterceptionContext(invocationContext, interceptorBindingClass);
}
});
}

private <T extends Annotation> Optional<CacheInterceptionContext<T>> getArcCacheInterceptionContext(
InvocationContext invocationContext, Class<T> interceptorBindingClass) {
Set<Annotation> bindings = InterceptorBindings.getInterceptorBindings(invocationContext);
if (bindings == null) {
LOGGER.trace("Interceptor bindings not found in ArC");
// This should only happen when the interception is not managed by Arc.
return Optional.empty();
}
List<T> interceptorBindings = new ArrayList<>();
for (Annotation binding : bindings) {
if (interceptorBindingClass.isInstance(binding)) {
interceptorBindings.add((T) binding);
}
}
return Optional.of(new CacheInterceptionContext<>(interceptorBindings));
}

private <T extends Annotation> CacheInterceptionContext<T> getNonArcCacheInterceptionContext(
InvocationContext invocationContext, Class<T> interceptorBindingClass) {
LOGGER.trace("Retrieving interceptor bindings using reflection");
List<T> interceptorBindings = new ArrayList<>();
for (Annotation annotation : invocationContext.getMethod().getAnnotations()) {
if (interceptorBindingClass.isInstance(annotation)) {
interceptorBindings.add((T) annotation);
}
}
return new CacheInterceptionContext<>(interceptorBindings);
karesti marked this conversation as resolved.
Show resolved Hide resolved
}

protected Object getCacheKey(Object[] methodParameterValues) {
if (methodParameterValues == null || methodParameterValues.length == 0) {
// If the intercepted method doesn't have any parameter, raise an exception.
throw new CacheException("Unable to cache without a key");
} else if (methodParameterValues.length == 1) {
// If the intercepted method has exactly one parameter, then this parameter will be used as the cache key.
return methodParameterValues[0];
} else {
// Protobuf type must be used
return new RuntimeException("A single parameter is needed. Create a Protobuf schema to create a Composite Key.");
}
}

protected static ReturnType determineReturnType(Class<?> returnType) {
if (Uni.class.isAssignableFrom(returnType)) {
return ReturnType.Uni;
}
if (CompletionStage.class.isAssignableFrom(returnType)) {
return ReturnType.CompletionStage;
}
return ReturnType.NonAsync;
}

protected Uni<?> asyncInvocationResultToUni(Object invocationResult, ReturnType returnType) {
if (returnType == ReturnType.Uni) {
return (Uni<?>) invocationResult;
} else if (returnType == ReturnType.CompletionStage) {
return Uni.createFrom().completionStage(new Supplier<>() {
@Override
public CompletionStage<?> get() {
return (CompletionStage<?>) invocationResult;
}
});
} else {
throw new CacheException(new IllegalStateException(UNHANDLED_ASYNC_RETURN_TYPE_MSG));
}
}

protected Object createAsyncResult(Uni<Object> cacheValue, ReturnType returnType) {
if (returnType == ReturnType.Uni) {
return cacheValue;
}
if (returnType == ReturnType.CompletionStage) {
return cacheValue.subscribeAsCompletionStage();
}
throw new CacheException(new IllegalStateException(UNHANDLED_ASYNC_RETURN_TYPE_MSG));
}

protected enum ReturnType {
NonAsync,
Uni,
CompletionStage
}
}
Loading