diff --git a/docs/src/main/asciidoc/infinispan-client.adoc b/docs/src/main/asciidoc/infinispan-client.adoc index ca618d298605d..5416de29092d8 100644 --- a/docs/src/main/asciidoc/infinispan-client.adoc +++ b/docs/src/main/asciidoc/infinispan-client.adoc @@ -442,6 +442,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]. +[#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 compute a cache key and use it to 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`. + +[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 diff --git a/extensions/infinispan-client/deployment/pom.xml b/extensions/infinispan-client/deployment/pom.xml index ba865f639aaaf..a294e6da65c47 100644 --- a/extensions/infinispan-client/deployment/pom.xml +++ b/extensions/infinispan-client/deployment/pom.xml @@ -41,6 +41,10 @@ io.quarkus quarkus-jsonp-deployment + + io.quarkus + quarkus-mutiny-deployment + io.quarkus diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/CacheNamesBuildItem.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/CacheNamesBuildItem.java new file mode 100644 index 0000000000000..411082aa3dd5f --- /dev/null +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/CacheNamesBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.infinispan.client.deployment; + +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * This build item is used to pass the full list of cache names from the validation step to the recording step. + */ +public final class CacheNamesBuildItem extends SimpleBuildItem { + + private final Set names; + + public CacheNamesBuildItem(Set names) { + this.names = names; + } + + public Set getNames() { + return names; + } +} diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java index 3404733cc1ac4..732376caa7f5f 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java @@ -56,6 +56,9 @@ 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.smallrye.health.deployment.spi.HealthBuildItem; class InfinispanClientProcessor { @@ -85,6 +88,9 @@ 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)); systemProperties.produce(new SystemPropertyBuildItem("io.netty.noUnsafe", "true")); hotDeployment.produce(new HotDeploymentWatchedFileBuildItem(META_INF + File.separator + HOTROD_CLIENT_PROPERTIES)); @@ -235,7 +241,8 @@ BeanContainerListenerBuildItem build(InfinispanRecorder recorder, InfinispanProp @BuildStep UnremovableBeanBuildItem ensureBeanLookupAvailable() { return UnremovableBeanBuildItem.beanTypes(BaseMarshaller.class, EnumMarshaller.class, MessageMarshaller.class, - RawProtobufMarshaller.class, FileDescriptorSource.class); + RawProtobufMarshaller.class, FileDescriptorSource.class, CacheResultInterceptor.class, + CacheInvalidateAllInterceptor.class); } @BuildStep diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/CacheDeploymentConstants.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/CacheDeploymentConstants.java new file mode 100644 index 0000000000000..fa20a016e445a --- /dev/null +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/CacheDeploymentConstants.java @@ -0,0 +1,41 @@ +package io.quarkus.infinispan.client.deployment.cache; + +import java.util.Arrays; +import java.util.List; + +import org.jboss.jandex.DotName; + +import io.quarkus.infinispan.client.CacheInvalidate; +import io.quarkus.infinispan.client.CacheInvalidateAll; +import io.quarkus.infinispan.client.CacheResult; +import io.quarkus.infinispan.client.runtime.cache.CacheResultInterceptor; +import io.smallrye.mutiny.Multi; + +public class CacheDeploymentConstants { + + // API annotations names. + public static final DotName CACHE_INVALIDATE_ALL = dotName(CacheInvalidateAll.class); + public static final DotName CACHE_INVALIDATE_ALL_LIST = dotName(CacheInvalidateAll.List.class); + public static final DotName CACHE_INVALIDATE = dotName(CacheInvalidate.class); + public static final DotName CACHE_INVALIDATE_LIST = dotName(CacheInvalidate.List.class); + public static final DotName CACHE_RESULT = dotName(CacheResult.class); + public static final List INTERCEPTOR_BINDINGS = Arrays.asList(CACHE_RESULT, CACHE_INVALIDATE, + CACHE_INVALIDATE_ALL); + public static final List INTERCEPTOR_BINDING_CONTAINERS = Arrays.asList(CACHE_INVALIDATE_LIST, + CACHE_INVALIDATE_ALL_LIST); + public static final List INTERCEPTORS = Arrays.asList(dotName(CacheResultInterceptor.class)); + + // MicroProfile REST Client. + public static final DotName REGISTER_REST_CLIENT = DotName + .createSimple("org.eclipse.microprofile.rest.client.inject.RegisterRestClient"); + + // Mutiny. + public static final DotName MULTI = dotName(Multi.class); + + // Annotations parameters. + public static final String CACHE_NAME_PARAM = "cacheName"; + + private static DotName dotName(Class annotationClass) { + return DotName.createSimple(annotationClass.getName()); + } +} diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/ClassTargetException.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/ClassTargetException.java new file mode 100644 index 0000000000000..9c6f59e0bdb30 --- /dev/null +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/ClassTargetException.java @@ -0,0 +1,28 @@ +package io.quarkus.infinispan.client.deployment.cache.exception; + +import org.jboss.jandex.DotName; + +import io.quarkus.infinispan.client.CacheInvalidate; +import io.quarkus.infinispan.client.CacheInvalidateAll; +import io.quarkus.infinispan.client.CacheResult; + +/** + * This exception is thrown at build time during the validation phase if a class is annotated with + * {@link CacheInvalidate @CacheInvalidate}, {@link CacheInvalidateAll @CacheInvalidateAll} or + * {@link CacheResult @CacheResult}. These annotations are only allowed at type level for the caching + * interceptors from this extension. + */ +@SuppressWarnings("serial") +public class ClassTargetException extends RuntimeException { + + private final DotName className; + + public ClassTargetException(DotName className, DotName annotationName) { + super("Caching annotations are not allowed on a class [class=" + className + ", annotation=" + annotationName + "]"); + this.className = className; + } + + public DotName getClassName() { + return className; + } +} diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/PrivateMethodTargetException.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/PrivateMethodTargetException.java new file mode 100644 index 0000000000000..d04aa1677756d --- /dev/null +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/PrivateMethodTargetException.java @@ -0,0 +1,29 @@ +package io.quarkus.infinispan.client.deployment.cache.exception; + +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; + +import io.quarkus.infinispan.client.CacheInvalidate; +import io.quarkus.infinispan.client.CacheInvalidateAll; +import io.quarkus.infinispan.client.CacheResult; + +/** + * This exception is thrown at build time during the validation phase if a private method is annotated with + * {@link CacheInvalidate @CacheInvalidate}, {@link CacheInvalidateAll @CacheInvalidateAll} or + * {@link CacheResult @CacheResult}. + */ +@SuppressWarnings("serial") +public class PrivateMethodTargetException extends RuntimeException { + + private final MethodInfo methodInfo; + + public PrivateMethodTargetException(MethodInfo methodInfo, DotName annotationName) { + super("Caching annotations are not allowed on a private method [class=" + methodInfo.declaringClass().name() + + ", method=" + methodInfo.name() + ", annotation=" + annotationName + "]"); + this.methodInfo = methodInfo; + } + + public MethodInfo getMethodInfo() { + return methodInfo; + } +} diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/VoidReturnTypeTargetException.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/VoidReturnTypeTargetException.java new file mode 100644 index 0000000000000..0b92dc83d98ca --- /dev/null +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/VoidReturnTypeTargetException.java @@ -0,0 +1,25 @@ +package io.quarkus.infinispan.client.deployment.cache.exception; + +import org.jboss.jandex.MethodInfo; + +import io.quarkus.infinispan.client.CacheResult; + +/** + * This exception is thrown at build time during the validation phase if a method returning void is annotated with + * {@link CacheResult @CacheResult}. + */ +@SuppressWarnings("serial") +public class VoidReturnTypeTargetException extends RuntimeException { + + private final MethodInfo methodInfo; + + public VoidReturnTypeTargetException(MethodInfo methodInfo) { + super("@CacheResult is not allowed on a method returning void [class=" + methodInfo.declaringClass().name() + + ", method=" + methodInfo.name() + "]"); + this.methodInfo = methodInfo; + } + + public MethodInfo getMethodInfo() { + return methodInfo; + } +} diff --git a/extensions/infinispan-client/runtime/pom.xml b/extensions/infinispan-client/runtime/pom.xml index 4a8df04e0ce6b..3f75089e5463f 100644 --- a/extensions/infinispan-client/runtime/pom.xml +++ b/extensions/infinispan-client/runtime/pom.xml @@ -25,6 +25,10 @@ io.quarkus quarkus-caffeine + + io.quarkus + quarkus-mutiny + io.quarkus quarkus-netty diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java new file mode 100644 index 0000000000000..07eb705b596a7 --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java @@ -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. + *

+ * This annotation can be combined with multiple other caching 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}. + */ +@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(); + } +} diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java new file mode 100644 index 0000000000000..2bf19c2e4bcf3 --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java @@ -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. + *

+ * This annotation can be combined with multiple other caching 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}. + */ +@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(); + } +} diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java new file mode 100644 index 0000000000000..6ff5b1352569d --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java @@ -0,0 +1,49 @@ +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 whether 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. + *

+ * 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. + *

+ * This annotation cannot be used on a method returning {@code void}. It can be combined with multiple other caching + * 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}. + *

+ */ +@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; +} diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptionContext.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptionContext.java new file mode 100644 index 0000000000000..b61a7ca51af13 --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptionContext.java @@ -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 { + + private final List interceptorBindings; + + public CacheInterceptionContext(List interceptorBindings) { + Objects.requireNonNull(interceptorBindings); + this.interceptorBindings = Collections.unmodifiableList(interceptorBindings); + } + + public List getInterceptorBindings() { + return interceptorBindings; + } + +} diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptor.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptor.java new file mode 100644 index 0000000000000..9279f51b3ee37 --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptor.java @@ -0,0 +1,95 @@ +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.function.Supplier; + +import javax.inject.Inject; +import javax.interceptor.Interceptor.Priority; +import javax.interceptor.InvocationContext; + +import org.infinispan.client.hotrod.RemoteCache; +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 { + + public static final int BASE_PRIORITY = Priority.PLATFORM_BEFORE; + + private static final Logger LOGGER = Logger.getLogger(CacheInterceptor.class); + private static final String PERFORMANCE_WARN_MSG = "Cache key resolution based on reflection calls. Please create a GitHub issue in the Quarkus repository, the maintainers might be able to improve your application performance."; + + @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 CacheInterceptionContext getInterceptionContext(InvocationContext invocationContext, + Class interceptorBindingClass) { + return getArcCacheInterceptionContext(invocationContext, interceptorBindingClass) + .orElseGet(new Supplier>() { + @Override + public CacheInterceptionContext get() { + return getNonArcCacheInterceptionContext(invocationContext, interceptorBindingClass); + } + }); + } + + private Optional> getArcCacheInterceptionContext( + InvocationContext invocationContext, Class interceptorBindingClass) { + Set 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 interceptorBindings = new ArrayList<>(); + for (Annotation binding : bindings) { + if (interceptorBindingClass.isInstance(binding)) { + interceptorBindings.add(cast(binding, interceptorBindingClass)); + } + } + return Optional.of(new CacheInterceptionContext<>(interceptorBindings)); + } + + private CacheInterceptionContext getNonArcCacheInterceptionContext( + InvocationContext invocationContext, Class interceptorBindingClass) { + LOGGER.trace("Retrieving interceptor bindings using reflection"); + List interceptorBindings = new ArrayList<>(); + return new CacheInterceptionContext<>(interceptorBindings); + } + + @SuppressWarnings("unchecked") + private T cast(Annotation annotation, Class interceptorBindingClass) { + return (T) annotation; + } + + protected Object getCacheKey(RemoteCache cache, 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 boolean isUniReturnType(InvocationContext invocationContext) { + return Uni.class.isAssignableFrom(invocationContext.getMethod().getReturnType()); + } +} diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInvalidateAllInterceptor.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInvalidateAllInterceptor.java new file mode 100644 index 0000000000000..18dafbf25de7e --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInvalidateAllInterceptor.java @@ -0,0 +1,78 @@ +package io.quarkus.infinispan.client.runtime.cache; + +import java.util.function.Function; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.commons.CacheException; +import org.jboss.logging.Logger; + +import io.quarkus.infinispan.client.CacheInvalidateAll; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@CacheInvalidateAll(cacheName = "") // The `cacheName` attribute is @Nonbinding. +@Interceptor +@Priority(CacheInterceptor.BASE_PRIORITY) +public class CacheInvalidateAllInterceptor extends CacheInterceptor { + + private static final Logger LOGGER = Logger.getLogger(CacheInvalidateAllInterceptor.class); + private static final String INTERCEPTOR_BINDINGS_ERROR_MSG = "The Quarkus Infinispan Client extension is not working properly (CacheInvalidateAll interceptor bindings retrieval failed), please create a GitHub issue in the Quarkus repository to help the maintainers fix this bug"; + + @AroundInvoke + public Object intercept(InvocationContext invocationContext) throws Exception { + CacheInterceptionContext interceptionContext = getInterceptionContext(invocationContext, + CacheInvalidateAll.class); + if (interceptionContext.getInterceptorBindings().isEmpty()) { + // This should never happen. + LOGGER.warn(INTERCEPTOR_BINDINGS_ERROR_MSG); + return invocationContext.proceed(); + } else if (isUniReturnType(invocationContext)) { + return invalidateAllNonBlocking(invocationContext, interceptionContext); + } else { + return invalidateAllBlocking(invocationContext, interceptionContext); + } + } + + private Object invalidateAllNonBlocking(InvocationContext invocationContext, + CacheInterceptionContext interceptionContext) { + LOGGER.trace("Invalidating all cache entries in a non-blocking way"); + return Multi.createFrom().iterable(interceptionContext.getInterceptorBindings()) + .onItem().transformToUniAndMerge(new Function>() { + @Override + public Uni apply(CacheInvalidateAll binding) { + return invalidateAll(binding); + } + }) + .onItem().ignoreAsUni() + .onItem().transformToUni(new Function>() { + @Override + public Uni apply(Object ignored) { + try { + return (Uni) invocationContext.proceed(); + } catch (Exception e) { + throw new CacheException(e); + } + } + }); + } + + private Object invalidateAllBlocking(InvocationContext invocationContext, + CacheInterceptionContext interceptionContext) throws Exception { + LOGGER.trace("Invalidating all cache entries in a blocking way"); + for (CacheInvalidateAll binding : interceptionContext.getInterceptorBindings()) { + invalidateAll(binding).await().indefinitely(); + } + return invocationContext.proceed(); + } + + private Uni invalidateAll(CacheInvalidateAll binding) { + RemoteCache cache = cacheManager.getCache(binding.cacheName()); + LOGGER.debugf("Invalidating all entries from cache [%s]", binding.cacheName()); + return Uni.createFrom().completionStage(cache.clearAsync()); + } +} diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInvalidateInterceptor.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInvalidateInterceptor.java new file mode 100644 index 0000000000000..c57dbab0b8fda --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInvalidateInterceptor.java @@ -0,0 +1,81 @@ + +package io.quarkus.infinispan.client.runtime.cache; + +import java.util.function.Function; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.commons.CacheException; +import org.jboss.logging.Logger; + +import io.quarkus.infinispan.client.CacheInvalidate; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@CacheInvalidate(cacheName = "") // The `cacheName` attribute is @Nonbinding. +@Interceptor +@Priority(CacheInterceptor.BASE_PRIORITY + 1) +public class CacheInvalidateInterceptor extends CacheInterceptor { + + private static final Logger LOGGER = Logger.getLogger(CacheInvalidateInterceptor.class); + private static final String INTERCEPTOR_BINDINGS_ERROR_MSG = "The Quarkus Infinispan Client extension is not working properly (CacheInvalidate interceptor bindings retrieval failed), please create a GitHub issue in the Quarkus repository to help the maintainers fix this bug"; + + @AroundInvoke + public Object intercept(InvocationContext invocationContext) throws Exception { + CacheInterceptionContext interceptionContext = getInterceptionContext(invocationContext, + CacheInvalidate.class); + if (interceptionContext.getInterceptorBindings().isEmpty()) { + // This should never happen. + LOGGER.warn(INTERCEPTOR_BINDINGS_ERROR_MSG); + return invocationContext.proceed(); + } else if (isUniReturnType(invocationContext)) { + return invalidateNonBlocking(invocationContext, interceptionContext); + } else { + return invalidateBlocking(invocationContext, interceptionContext); + } + } + + private Object invalidateNonBlocking(InvocationContext invocationContext, + CacheInterceptionContext interceptionContext) { + LOGGER.trace("Invalidating cache entries in a non-blocking way"); + return Multi.createFrom().iterable(interceptionContext.getInterceptorBindings()) + .onItem().transformToUniAndMerge(new Function>() { + @Override + public Uni apply(CacheInvalidate binding) { + return invalidate(binding, invocationContext.getParameters()); + } + }) + .onItem().ignoreAsUni() + .onItem().transformToUni(new Function>() { + @Override + public Uni apply(Object ignored) { + try { + return (Uni) invocationContext.proceed(); + } catch (Exception e) { + throw new CacheException(e); + } + } + }); + } + + private Object invalidateBlocking(InvocationContext invocationContext, + CacheInterceptionContext interceptionContext) throws Exception { + LOGGER.trace("Invalidating cache entries in a blocking way"); + for (CacheInvalidate binding : interceptionContext.getInterceptorBindings()) { + invalidate(binding, invocationContext.getParameters()) + .await().indefinitely(); + } + return invocationContext.proceed(); + } + + private Uni invalidate(CacheInvalidate binding, Object[] parameters) { + RemoteCache cache = cacheManager.getCache(binding.cacheName()); + Object key = getCacheKey(cache, parameters); + LOGGER.debugf("Invalidating entry with key [%s] from cache [%s]", key, binding.cacheName()); + return Uni.createFrom().completionStage(cache.removeAsync(key)); + } +} diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheResultInterceptor.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheResultInterceptor.java new file mode 100644 index 0000000000000..a79d2a6799544 --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheResultInterceptor.java @@ -0,0 +1,143 @@ +package io.quarkus.infinispan.client.runtime.cache; + +import java.time.Duration; +import java.util.function.Function; +import java.util.function.Supplier; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.commons.CacheException; +import org.jboss.logging.Logger; + +import io.quarkus.infinispan.client.CacheResult; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.TimeoutException; +import io.smallrye.mutiny.Uni; + +@CacheResult(cacheName = "") // The `cacheName` attribute is @Nonbinding. +@Interceptor +@Priority(CacheInterceptor.BASE_PRIORITY + 2) +public class CacheResultInterceptor extends CacheInterceptor { + + private static final Logger LOGGER = Logger.getLogger(CacheResultInterceptor.class); + private static final String INTERCEPTOR_BINDING_ERROR_MSG = "The Quarkus Infinispan Client extension is not working properly (CacheResult interceptor binding retrieval failed), please create a GitHub issue in the Quarkus repository to help the maintainers fix this bug"; + + @AroundInvoke + public Object intercept(InvocationContext invocationContext) throws Throwable { + /* + * io.smallrye.mutiny.Multi values are never cached. + * There's already a WARN log entry at build time so we don't need to log anything at run time. + */ + if (Multi.class.isAssignableFrom(invocationContext.getMethod().getReturnType())) { + return invocationContext.proceed(); + } + + CacheInterceptionContext interceptionContext = getInterceptionContext(invocationContext, + CacheResult.class); + + if (interceptionContext.getInterceptorBindings().isEmpty()) { + // This should never happen. + LOGGER.debugf(INTERCEPTOR_BINDING_ERROR_MSG); + return invocationContext.proceed(); + } + + CacheResult binding = interceptionContext.getInterceptorBindings().get(0); + RemoteCache remoteCache = cacheManager.getCache(binding.cacheName()); + Object key = getCacheKey(remoteCache, invocationContext.getParameters()); + InfinispanGetWrapper cache = new InfinispanGetWrapper(remoteCache); + LOGGER.debugf("Loading entry with key [%s] from cache [%s]", key, binding.cacheName()); + + try { + if (isUniReturnType(invocationContext)) { + Uni cacheValue = cache.get(key, new Function() { + @Override + public Object apply(Object k) { + LOGGER.debugf("Adding %s entry with key [%s] into cache [%s]", + UnresolvedUniValue.class.getSimpleName(), key, binding.cacheName()); + return UnresolvedUniValue.INSTANCE; + } + }).onItem().transformToUni(new Function>() { + @Override + public Uni apply(Object value) { + if (value == UnresolvedUniValue.INSTANCE) { + try { + return ((Uni) invocationContext.proceed()) + .call(new Function>() { + @Override + public Uni apply(Object emittedValue) { + return Uni.createFrom() + .completionStage(remoteCache.replaceAsync(key, emittedValue)); + } + }); + } catch (CacheException e) { + throw e; + } catch (Exception e) { + throw new CacheException(e); + } + } else { + return Uni.createFrom().item(value); + } + } + }); + if (binding.lockTimeout() <= 0) { + return cacheValue; + } + return cacheValue.ifNoItem().after(Duration.ofMillis(binding.lockTimeout())) + .recoverWithUni(new Supplier>() { + @Override + public Uni get() { + try { + return (Uni) invocationContext.proceed(); + } catch (CacheException e) { + throw e; + } catch (Exception e) { + throw new CacheException(e); + } + } + }); + + } else { + Uni cacheValue = cache.get(key, new Function() { + @Override + public Object apply(Object k) { + try { + return invocationContext.proceed(); + } catch (CacheException e) { + throw e; + } catch (Throwable e) { + throw new CacheException(e); + } + } + }); + Object value; + if (binding.lockTimeout() <= 0) { + value = cacheValue.await().indefinitely(); + } else { + try { + /* + * If the current thread started the cache value computation, then the computation is already finished + * since + * it was done synchronously and the following call will never time out. + */ + value = cacheValue.await().atMost(Duration.ofMillis(binding.lockTimeout())); + } catch (TimeoutException e) { + // TODO: Add statistics here to monitor the timeout. + return invocationContext.proceed(); + } + } + return value; + } + + } catch (CacheException e) { + if (e.getCause() != null) { + throw e.getCause(); + } else { + throw e; + } + } + } +} diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/InfinispanGetWrapper.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/InfinispanGetWrapper.java new file mode 100644 index 0000000000000..c8912dd70e0f3 --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/InfinispanGetWrapper.java @@ -0,0 +1,80 @@ +package io.quarkus.infinispan.client.runtime.cache; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.commons.CacheException; +import org.jboss.logging.Logger; + +import io.smallrye.mutiny.Uni; + +public class InfinispanGetWrapper { + + private static final Logger LOGGER = Logger.getLogger(InfinispanGetWrapper.class); + + final RemoteCache cache; + private final Map synchronousGetLocks = new ConcurrentHashMap<>(); + + public InfinispanGetWrapper(RemoteCache cache) { + this.cache = cache; + } + + public Uni get(K key, Function valueLoader) { + return Uni.createFrom().completionStage( + /* + * Even if CompletionStage is eager, the Supplier used below guarantees that the cache value computation will be + * delayed until subscription time. In other words, the cache value computation is done lazily. + */ + new Supplier>() { + @Override + public CompletionStage get() { + CompletionStage infinispanValue = getFromInfinispan(key, valueLoader); + return cast(infinispanValue); + } + }); + } + + private CompletableFuture getFromInfinispan(K key, Function valueLoader) { + ReentrantLock lock; + Object value = cache.get(key); + if (value == null) { + lock = synchronousGetLocks.computeIfAbsent(key, k -> new ReentrantLock()); + lock.lock(); + try { + if ((value = cache.get(key)) == null) { + try { + Object newValue = valueLoader.apply(key); + // we can't use computeIfAbsent here since in distributed embedded scenario we would + // send a lambda to other nodes. This is the behavior we want to avoid. + value = cache.putIfAbsent(key, newValue); + if (value == null) { + value = newValue; + } + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } + } finally { + lock.unlock(); + synchronousGetLocks.remove(key); + } + } + return CompletableFuture.completedFuture(value); + + } + + private T cast(Object value) { + try { + return (T) value; + } catch (ClassCastException e) { + throw new CacheException( + "An existing cached value type does not match the type returned by the value loading function", e); + } + } +} diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/UnresolvedUniValue.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/UnresolvedUniValue.java new file mode 100644 index 0000000000000..8eb98aea540e4 --- /dev/null +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/UnresolvedUniValue.java @@ -0,0 +1,13 @@ +package io.quarkus.infinispan.client.runtime.cache; + +/** + * This value acts as a placeholder in the cache. It will be eventually replaced by the item emitted by the + * {@link io.smallrye.mutiny.Uni Uni} when it has been resolved. + */ +public class UnresolvedUniValue { + + public static final UnresolvedUniValue INSTANCE = new UnresolvedUniValue(); + + private UnresolvedUniValue() { + } +} diff --git a/integration-tests/infinispan-client/pom.xml b/integration-tests/infinispan-client/pom.xml index c56fa803edfa2..3c45974f6100e 100644 --- a/integration-tests/infinispan-client/pom.xml +++ b/integration-tests/infinispan-client/pom.xml @@ -28,6 +28,10 @@ io.quarkus quarkus-resteasy + + io.quarkus + quarkus-resteasy-jackson + @@ -154,6 +158,19 @@ + + io.quarkus + quarkus-resteasy-jackson-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-smallrye-health-deployment diff --git a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/BookStoreService.java b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/BookStoreService.java new file mode 100644 index 0000000000000..80098f910b337 --- /dev/null +++ b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/BookStoreService.java @@ -0,0 +1,52 @@ +package io.quarkus.it.infinispan.client; + +import java.math.BigDecimal; +import java.util.Collections; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.infinispan.client.CacheInvalidate; +import io.quarkus.infinispan.client.CacheInvalidateAll; +import io.quarkus.infinispan.client.CacheResult; + +@Path("/books") +public class BookStoreService { + + @GET + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + @CacheResult(cacheName = "books") + public Book book(@PathParam("id") String id) { + return new Book("computed book", "desc", 2022, + Collections.singleton(new Author("Wiwi", "B")), Type.FANTASY, new BigDecimal("100.99")); + + } + + @GET + @Path("{id}/{extra}") + @Produces(MediaType.APPLICATION_JSON) + @CacheResult(cacheName = "books") + public Book bookWithTwoParams(@PathParam("id") String id, @PathParam("extra") String extra) { + return null; + } + + @DELETE + @Path("{id}") + @Produces(MediaType.TEXT_PLAIN) + @CacheInvalidate(cacheName = "books") + public String remove(@PathParam("id") String id) { + return "Nothing to invalidate"; + } + + @DELETE + @Produces(MediaType.TEXT_PLAIN) + @CacheInvalidateAll(cacheName = "books") + public String removeAll() { + return "Invalidate all not needed"; + } +} diff --git a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java index bec47ec793c33..25d5e5e30c8c2 100644 --- a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java +++ b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java @@ -65,6 +65,10 @@ public class TestServlet { @Remote("magazine") RemoteCache magazineCache; + @Inject + @Remote("books") + RemoteCache books; + @Inject CounterManager counterManager; @@ -120,6 +124,14 @@ public void resultUpdated(String key, Book value) { magazineCache.put("popular-time", new Magazine("TIME", YearMonth.of(1997, 4), Arrays.asList("Yep, I'm gay", "Backlash against HMOS", "False Hope on Breast Cancer?"))); + books.put("hp-1", new Book("Harry Potter and the Philosopher's Stone", "After murdering Harry's parents...", 1997, + Collections.singleton(new Author("J.K", "Rowling")), Type.FANTASY, new BigDecimal("20.99"))); + books.put("hp-2", new Book("Harry Potter and the Chamber of Secrets", "After a summer spent with the Dursleys...", 1998, + Collections.singleton(new Author("J.K", "Rowling")), Type.FANTASY, new BigDecimal("20.99"))); + books.put("hp-3", + new Book("Harry Potter and the Prisoner of Azkaban", "Harry ends another insufferable summer...", 1999, + Collections.singleton(new Author("J.K", "Rowling")), Type.FANTASY, new BigDecimal("20.99"))); + log.info("Inserted values"); waitUntilStarted.countDown(); diff --git a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java index fd9e69c2b4c46..2d8ab0526b13a 100644 --- a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java +++ b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java @@ -1,5 +1,6 @@ package io.quarkus.it.infinispan.client; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -18,13 +19,11 @@ public class InfinispanClientFunctionalityTest { @Test public void testGetAllKeys() { - System.out.println("Running getAllKeys test"); RestAssured.when().get("/test").then().body(is("[book1, book2]")); } @Test public void testQuery() { - System.out.println("Running query test"); RestAssured.when().get("/test/query/So").then().body(is("[Son Martin]")); RestAssured.when().get("/test/query/org").then().body(is("[George Martin]")); RestAssured.when().get("/test/query/o").then().body(is("[George Martin,Son Martin]")); @@ -32,7 +31,6 @@ public void testQuery() { @Test public void testIckleQuery() { - System.out.println("Running ickleQuery test"); RestAssured.when().get("/test/icklequery/So").then().body(is("[Son Martin]")); RestAssured.when().get("/test/icklequery/org").then().body(is("[George Martin]")); RestAssured.when().get("/test/icklequery/o").then().body(is("[George Martin,Son Martin]")); @@ -40,7 +38,6 @@ public void testIckleQuery() { @Test public void testCounterIncrement() { - System.out.println("Running counterIncrement test"); String initialValue = RestAssured.when().get("test/incr/somevalue").body().print(); String nextValue = RestAssured.when().get("test/incr/somevalue").body().print(); assertEquals(Integer.parseInt(initialValue) + 1, Integer.parseInt(nextValue)); @@ -48,19 +45,59 @@ public void testCounterIncrement() { @Test public void testCQ() { - System.out.println("Running CQ test"); RestAssured.when().get("/test/cq").then().body(is("2023")); } @Test public void testNearCacheInvalidation() { - System.out.println("Running nearCacheInvalidation test"); RestAssured.when().get("/test/nearcache").then().body(is("worked")); } @Test public void testQueryWithCustomMarshaller() { - System.out.println("Running query with custom marshaller test"); RestAssured.when().get("/test/magazinequery/IM").then().body(is("[TIME:1923-03,TIME:1997-04]")); } + + @Test + public void testCacheAnnotations() { + RestAssured.when().get("/books/hp-1") + .then() + .body(containsString("Philosopher's Stone")); + + RestAssured.when().get("/books/hp-2") + .then() + .body(containsString("Chamber of Secrets")); + + RestAssured.when().get("/books/hp-3") + .then() + .body(containsString("Prisoner of Azkaban")); + + RestAssured.when().get("/books/hp-4") + .then() + .body(containsString("computed book")); + + RestAssured.when().get("/books/hp-3/extra-params") + .then().statusCode(500); + + RestAssured.when().delete("/books/hp-1") + .then() + .statusCode(200); + + RestAssured.when().get("/books/hp-1") + .then() + .body(containsString("computed book")); + + RestAssured.when().delete("/books") + .then() + .statusCode(200); + + RestAssured.when().get("/books/hp-2") + .then() + .body(containsString("computed book")); + + RestAssured.when().get("/books/hp-3") + .then() + .body(containsString("computed book")); + + } } diff --git a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanServerTestResource.java b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanServerTestResource.java index f88b26a329f3f..7bd425752abfe 100644 --- a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanServerTestResource.java +++ b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanServerTestResource.java @@ -32,7 +32,7 @@ public Map start() { configurationBuilder); ecm.defineConfiguration("magazine", configurationBuilder.build()); - + ecm.createCache("books", configurationBuilder.build()); // Client connects to a non default port final HotRodServerConfigurationBuilder hotRodServerConfigurationBuilder = new HotRodServerConfigurationBuilder(); hotRodServerConfigurationBuilder.adminOperationsHandler(new EmbeddedServerAdminOperationHandler());