diff --git a/bom/pom.xml b/bom/pom.xml index 919647b523..d422a7869d 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -430,11 +430,21 @@ quarkus.vertx.max-event-loop-execute-time=${max.event-loop.execute-time:20000} hono-service-command-router ${project.version} + + org.eclipse.hono + client-device-connection + ${project.version} + org.eclipse.hono client-device-connection-infinispan ${project.version} + + org.eclipse.hono + client-device-connection-redis + ${project.version} + org.eclipse.hono hono-client-application diff --git a/client-device-connection-infinispan/pom.xml b/client-device-connection-infinispan/pom.xml index b24f28db0b..c951d639ef 100644 --- a/client-device-connection-infinispan/pom.xml +++ b/client-device-connection-infinispan/pom.xml @@ -34,6 +34,7 @@ org.slf4j slf4j-api + org.infinispan infinispan-core-jakarta @@ -59,18 +60,35 @@ + + + org.eclipse.hono + hono-core + + + org.eclipse.hono + client-device-connection + org.eclipse.hono hono-client-common + io.vertx - vertx-web + vertx-core + com.google.guava guava + + + io.smallrye.config + smallrye-config-core + + io.quarkus quarkus-core diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/BasicCache.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/BasicCache.java index 9373aae86f..3fdaa5ce77 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/BasicCache.java +++ b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/BasicCache.java @@ -24,6 +24,7 @@ import java.util.function.Function; import org.eclipse.hono.client.ServerErrorException; +import org.eclipse.hono.deviceconnection.common.Cache; import org.eclipse.hono.util.Futures; import org.eclipse.hono.util.Lifecycle; import org.infinispan.commons.api.BasicCacheContainer; diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/HotrodCache.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/HotrodCache.java index 4e3b3ea9b1..693a5e7904 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/HotrodCache.java +++ b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/HotrodCache.java @@ -18,6 +18,7 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.hono.deviceconnection.common.CommonCacheConfig; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.RemoteCacheContainer; import org.infinispan.client.hotrod.RemoteCacheManager; @@ -174,7 +175,6 @@ protected void postCacheAccess(final AsyncResult cacheOperationResult) { */ @Override public Future checkForCacheAvailability() { - if (isStarted()) { final ConnectionCheckResult lastResult = lastConnectionCheckResult; if (lastResult != null && !lastResult.isOlderThan(CACHED_CONNECTION_CHECK_RESULT_MAX_AGE)) { diff --git a/client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/QuarkusPropertyBindingTest.java b/client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/RemoteCacheQuarkusPropertyBindingTest.java similarity index 85% rename from client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/QuarkusPropertyBindingTest.java rename to client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/RemoteCacheQuarkusPropertyBindingTest.java index 53fe74cf6a..fe52d57ff5 100644 --- a/client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/QuarkusPropertyBindingTest.java +++ b/client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/RemoteCacheQuarkusPropertyBindingTest.java @@ -25,24 +25,10 @@ import org.junit.jupiter.api.Test; /** - * Tests verifying binding of configuration properties to {@link CommonCacheConfig} and - * {@link InfinispanRemoteConfigurationProperties}. + * Tests verifying binding of configuration properties to {@link InfinispanRemoteConfigurationProperties}. * */ -public class QuarkusPropertyBindingTest { - - @Test - void testCommonCacheConfigurationPropertiesArePickedUp() { - - final var commonCacheConfig = new CommonCacheConfig( - ConfigMappingSupport.getConfigMapping( - CommonCacheOptions.class, - this.getClass().getResource("/common-cache-options.yaml"))); - - assertThat(commonCacheConfig.getCacheName()).isEqualTo("the-cache"); - assertThat(commonCacheConfig.getCheckKey()).isEqualTo("the-key"); - assertThat(commonCacheConfig.getCheckValue()).isEqualTo("the-value"); - } +public class RemoteCacheQuarkusPropertyBindingTest { @SuppressWarnings("deprecation") @Test diff --git a/client-device-connection-redis/pom.xml b/client-device-connection-redis/pom.xml new file mode 100644 index 0000000000..25a09d01ad --- /dev/null +++ b/client-device-connection-redis/pom.xml @@ -0,0 +1,125 @@ + + + + 4.0.0 + + org.eclipse.hono + hono-bom + 2.6.0-SNAPSHOT + ../bom + + client-device-connection-redis + + Redis Device Connection client + A Redis based client for accessing device connection information in a Redis cluster. + + + + org.eclipse.hono + client-device-connection + + + + io.quarkus + quarkus-vertx + + + + org.eclipse.hono + hono-legal + + + org.eclipse.hono + hono-core + + + + org.slf4j + slf4j-api + + + + io.smallrye.config + smallrye-config + + + + io.vertx + vertx-core + + + io.vertx + vertx-redis-client + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + com.google.truth + truth + test + + + ch.qos.logback + logback-classic + test + + + org.mockito + mockito-core + test + + + io.vertx + vertx-junit5 + test + + + org.eclipse.hono + core-test-utils + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.jboss.jandex + jandex-maven-plugin + + + org.jacoco + jacoco-maven-plugin + + + + diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCacheVertx.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCacheVertx.java new file mode 100644 index 0000000000..9777fdb181 --- /dev/null +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCacheVertx.java @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.hono.deviceconnection.redis.client; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.eclipse.hono.deviceconnection.common.Cache; +import org.eclipse.hono.util.Lifecycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import io.vertx.redis.client.RedisAPI; +import io.vertx.redis.client.Response; + +/** + * TODO. + */ +public class RedisCacheVertx implements Cache, Lifecycle { + + private static final Logger LOG = LoggerFactory.getLogger(RedisCacheVertx.class); + + private final RedisAPI api; + + /** + * TODO. + * + * @param api TODO. + */ + private RedisCacheVertx(final RedisAPI api) { + Objects.requireNonNull(api); + this.api = api; + } + + /** + * TODO. + * + * @param api TODO. + * @return TODO. + */ + public static RedisCacheVertx from(final RedisAPI api) { + Objects.requireNonNull(api); + return new RedisCacheVertx(api); + } + + @Override + public Future start() { + LOG.info("VREDIS: start()"); + return checkForCacheAvailability().mapEmpty(); + } + + @Override + public Future stop() { + LOG.info("VREDIS: stop()"); + api.close(); + return Future.succeededFuture(); + } + + @Override + public Future checkForCacheAvailability() { + LOG.info("VREDIS: checkForCacheAvailability()"); + Objects.requireNonNull(api); + + return api.ping(List.of()) + .map(new JsonObject()); + } + + @Override + public Future put(final String key, final String value) { + LOG.info("VREDIS: put {}={}", key, value); + Objects.requireNonNull(api); + + return api.set(List.of(String.valueOf(key), String.valueOf(value))) + .mapEmpty(); + } + + @Override + public Future put(final String key, final String value, final long lifespan, final TimeUnit lifespanUnit) { + LOG.info("VREDIS: put {}={} ({} {})", key, value, lifespan, lifespanUnit); + Objects.requireNonNull(api); + + final List params = new ArrayList<>(List.of(key, value)); + final long millis = lifespanUnit.toMillis(lifespan); + if (millis > 0) { + params.addAll(List.of("PX", String.valueOf(millis))); + } + return api.set(params) + .mapEmpty(); + } + + @Override + public Future putAll(final Map data) { + LOG.info("VREDIS: putAll ({})", data.size()); + Objects.requireNonNull(api); + + final List keyValues = new ArrayList<>(data.size() * 2); + data.forEach((k, v) -> { + keyValues.add(k); + keyValues.add(v); + }); + return api.mset(keyValues) + .mapEmpty(); + } + + @Override + public Future putAll(final Map data, final long lifespan, + final TimeUnit lifespanUnit) { + LOG.info("VREDIS: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); + Objects.requireNonNull(api); + + final long millis = lifespanUnit.toMillis(lifespan); + return api.multi() + .compose(ignored -> { + final List> futures = new ArrayList<>(data.size()); + data.forEach((k, v) -> { + final List params = new ArrayList<>(List.of(String.valueOf(k), String.valueOf(v))); + if (millis > 0) { + params.addAll(List.of("PX", String.valueOf(millis))); + } + futures.add(api.set(params)); + }); + return Future.all(Collections.unmodifiableList(futures)); + }) + .compose(ignored -> api.exec()) + // null reply means transaction aborted + .map(Objects::nonNull) + .mapEmpty(); + } + + @Override + public Future get(final String key) { + LOG.info("VREDIS: get {}", key); + Objects.requireNonNull(api); + + return api.get(String.valueOf(key)) + .compose(value -> Future.succeededFuture(String.valueOf(value))); + } + + @Override + public Future remove(final String key, final String value) { + LOG.info("VREDIS: remove {}={}", key, value); + Objects.requireNonNull(api); + + return api.watch(List.of(String.valueOf(key))) + .compose(ignored -> api.get(String.valueOf(key))) + .compose(response -> { + if (response == null) { + // key does not exist + return Future.succeededFuture(false); + } + if (String.valueOf(response).equals(value)) { + return api.multi() + .compose(ignored -> api.del(List.of(String.valueOf(key)))) + .compose(ignored -> api.exec()) + // null reply means transaction aborted + .map(Objects::nonNull); + } else { + return Future.succeededFuture(false); + } + }); + } + + @Override + public Future> getAll(final Set keys) { + LOG.info("VREDIS: getAll ({})", keys.size()); + Objects.requireNonNull(api); + + final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); + final Map result = new HashMap<>(keyList.size()); + return api.mget(keyList) + .compose(values -> { + values.forEach(i -> { + try { + if (i != null) { // TODO: this is kinda strange but some results are null and the BasicCache does not include those in the returned result. Ask about/investigate. + result.put(keyList.removeFirst(), i.toString()); + } else { + keyList.removeFirst(); + } + } catch (Exception e) { + LOG.info(" - got exception {}", e.getMessage()); + } + }); + return Future.succeededFuture(result); + }); + } +} diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/NetConfig.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/NetConfig.java new file mode 100644 index 0000000000..9c5d89402f --- /dev/null +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/NetConfig.java @@ -0,0 +1,135 @@ +/** + * TODO. + */ +package org.eclipse.hono.deviceconnection.redis.client.config; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; + + +/** + * TODO. + */ +@SuppressWarnings("checkstyle:JavadocMethod") +public interface NetConfig { + + /** + * Set the ALPN usage. + */ + Optional alpn(); + + /** + * Sets the list of application-layer protocols to provide to the server during the + * {@code Application-Layer Protocol Negotiation}. + */ + Optional> applicationLayerProtocols(); + + /** + * Sets the list of enabled SSL/TLS protocols. + */ + Optional> secureTransportProtocols(); + + /** + * Set the idle timeout. + */ + Optional idleTimeout(); + + /** + * Set the connect timeout. + */ + Optional connectionTimeout(); + + /** + * Set a list of remote hosts that are not proxied when the client is configured to use a proxy. + */ + Optional> nonProxyHosts(); + + /** + * Set proxy options for connections via CONNECT proxy. + */ + ProxyConfig proxyOptions(); + + /** + * Set the read idle timeout. + */ + Optional readIdleTimeout(); + + /** + * Set the TCP receive buffer size. + */ + OptionalInt receiveBufferSize(); + + /** + * Set the value of reconnect attempts. + */ + OptionalInt reconnectAttempts(); + + /** + * Set the reconnect interval. + */ + Optional reconnectInterval(); + + /** + * Whether to reuse the address. + */ + Optional reuseAddress(); + + /** + * Whether to reuse the port. + */ + Optional reusePort(); + + /** + * Set the TCP send buffer size. + */ + OptionalInt sendBufferSize(); + + /** + * Set the {@code SO_linger} keep alive duration. + */ + Optional soLinger(); + + /** + * Enable the {@code TCP_CORK} option - only with linux native transport. + */ + Optional cork(); + + /** + * Enable the {@code TCP_FASTOPEN} option - only with linux native transport. + */ + Optional fastOpen(); + + /** + * Set whether keep alive is enabled. + */ + Optional keepAlive(); + + /** + * Set whether no delay is enabled. + */ + Optional noDelay(); + + /** + * Enable the {@code TCP_QUICKACK} option - only with linux native transport. + */ + Optional quickAck(); + + /** + * Set the value of traffic class. + */ + OptionalInt trafficClass(); + + /** + * Set the write idle timeout. + */ + Optional writeIdleTimeout(); + + /** + * Set the local interface to bind for network connections. + * When the local address is null, it will pick any local address, the default local address is null. + */ + Optional localAddress(); +} diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/ProxyConfig.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/ProxyConfig.java new file mode 100644 index 0000000000..970d4ba628 --- /dev/null +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/ProxyConfig.java @@ -0,0 +1,47 @@ +/** + * TODO. + */ +package org.eclipse.hono.deviceconnection.redis.client.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; +import io.vertx.core.net.ProxyType; + +/** + * TODO. + */ +@SuppressWarnings("checkstyle:JavadocMethod") +@ConfigGroup +public interface ProxyConfig { + + /** + * Set proxy username. + */ + Optional username(); + + /** + * Set proxy password. + */ + Optional password(); + + /** + * Set proxy port. Defaults to 3128. + */ + @WithDefault("3128") + int port(); + + /** + * Set proxy host. + */ + Optional host(); + + /** + * Set proxy type. + * Accepted values are: {@code HTTP} (default), {@code SOCKS4} and {@code SOCKS5}. + */ + @WithDefault("http") + ProxyType type(); + +} diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/RedisConfig.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/RedisConfig.java new file mode 100644 index 0000000000..1af2377a34 --- /dev/null +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/RedisConfig.java @@ -0,0 +1,197 @@ +/** + * TODO. + */ +package org.eclipse.hono.deviceconnection.redis.client.config; + +import java.net.URI; +import java.time.Duration; +import java.util.Optional; +import java.util.Set; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.vertx.redis.client.RedisClientType; +import io.vertx.redis.client.RedisReplicas; +import io.vertx.redis.client.RedisRole; + +/** + * TODO. + */ +@SuppressWarnings("checkstyle:JavadocMethod") +@ConfigMapping(prefix = "hono.cache.redis") //, namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) +public interface RedisConfig { + + /** + * The redis hosts to use while connecting to the redis server. Only the cluster and sentinel modes will consider more than + * 1 element. + *

+ * The URI provided uses the following schema `redis://[username:password@][host][:port][/database]` + * Use `quarkus.redis.hosts-provider-name` to provide the hosts programmatically. + *

+ * @see Redis scheme on www.iana.org + */ + Optional> hosts(); + + /** + * The hosts provider bean name. + *

+ * It is the {@code @Named} value of the hosts provider bean. It is used to discriminate if multiple + * `io.quarkus.redis.client.RedisHostsProvider` beans are available. + * + *

+ * Used when `quarkus.redis.hosts` is not set. + */ + Optional hostsProviderName(); + + /** + * The maximum delay to wait before a blocking command to redis server times out. + */ + @WithDefault("10s") + Duration timeout(); + + /** + * The redis client type. + * Accepted values are: {@code STANDALONE} (default), {@code CLUSTER}, {@code REPLICATION}, {@code SENTINEL}. + */ + @WithDefault("standalone") + RedisClientType clientType(); + + /** + * The master name (only considered in HA mode). + */ + Optional masterName(); + + /** + * The role name (only considered in Sentinel / HA mode). + * Accepted values are: {@code MASTER}, {@code REPLICA}, {@code SENTINEL}. + */ + Optional role(); + + /** + * Whether to use replicas nodes (only considered in Cluster mode). + * Accepted values are: {@code ALWAYS}, {@code NEVER}, {@code SHARE}. + */ + Optional replicas(); + + /** + * The default password for cluster/sentinel connections. + *

+ * If not set it will try to extract the value from the current default {@code #hosts}. + */ + Optional password(); + + /** + * The maximum size of the connection pool. When working with cluster or sentinel. + *

+ * This value should be at least the total number of cluster member (or number of sentinels + 1) + */ + @WithDefault("6") + int maxPoolSize(); + + /** + * The maximum waiting requests for a connection from the pool. + */ + @WithDefault("24") + int maxPoolWaiting(); + + /** + * The duration indicating how often should the connection pool cleaner executes. + */ + Optional poolCleanerInterval(); + + /** + * The timeout for a connection recycling. + */ + @WithDefault("15") + Duration poolRecycleTimeout(); + + /** + * Sets how many handlers is the client willing to queue. + *

+ * The client will always work on pipeline mode, this means that messages can start queueing. + * Using this configuration option, you can control how much backlog you're willing to accept. + */ + @WithDefault("2048") + int maxWaitingHandlers(); + + /** + * Tune how much nested arrays are allowed on a redis response. This affects the parser performance. + */ + @WithDefault("32") + int maxNestedArrays(); + + /** + * The number of reconnection attempts when a pooled connection cannot be established on first try. + */ + @WithDefault("0") + int reconnectAttempts(); + + /** + * The interval between reconnection attempts when a pooled connection cannot be established on first try. + */ + @WithDefault("1") + Duration reconnectInterval(); + + /** + * Should the client perform {@code RESP} protocol negotiation during the connection handshake. + */ + @WithDefault("true") + boolean protocolNegotiation(); + + /* + * The preferred protocol version to be used during protocol negotiation. When not set, + * defaults to RESP 3. When protocol negotiation is disabled, this setting has no effect. + */ + //Optional preferredProtocolVersion(); + + /** + * The TTL of the hash slot cache. A hash slot cache is used by the clustered Redis client + * to prevent constantly sending {@code CLUSTER SLOTS} commands to the first statically + * configured cluster node. + *

+ * This setting is only meaningful in case of a clustered Redis client and has no effect + * otherwise. + */ + @WithDefault("1s") + Duration hashSlotCacheTtl(); + + /** + * TCP config. + */ + NetConfig tcp(); + + /** + * SSL/TLS config. + */ + TlsConfig tls(); + + /** + * TODO. + * @return TODO. + */ + default String toDebugString() { + return "RedisClientConfig{" + + "hosts=" + hosts() + + ", hostsProviderName=" + hostsProviderName() + + ", timeout=" + timeout() + + ", clientType=" + clientType() + + ", masterName=" + masterName() + + ", role=" + role() + + ", replicas=" + replicas() + + ", password=" + password() + + ", maxPoolSize=" + maxPoolSize() + + ", maxPoolWaiting=" + maxPoolWaiting() + + ", poolCleanerInterval=" + poolCleanerInterval() + + ", poolRecycleTimeout=" + poolRecycleTimeout() + + ", maxWaitingHandlers=" + maxWaitingHandlers() + + ", maxNestedArrays=" + maxNestedArrays() + + ", reconnectAttempts=" + reconnectAttempts() + + ", reconnectInterval=" + reconnectInterval() + + ", protocolNegotiation=" + protocolNegotiation() + + //", preferredProtocolVersion=" + preferredProtocolVersion() + + ", hashSlotCacheTtl=" + hashSlotCacheTtl() + + ", tcp=" + tcp() + + ", tls=" + tls() + + '}'; + } +} diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/TlsConfig.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/TlsConfig.java new file mode 100644 index 0000000000..bbbc652510 --- /dev/null +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/TlsConfig.java @@ -0,0 +1,214 @@ +/** + * TODO. + */ +package org.eclipse.hono.deviceconnection.redis.client.config; + +import java.util.List; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.vertx.core.runtime.config.JksConfiguration; +import io.quarkus.vertx.core.runtime.config.PemKeyCertConfiguration; +import io.quarkus.vertx.core.runtime.config.PemTrustCertConfiguration; +import io.quarkus.vertx.core.runtime.config.PfxConfiguration; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithParentName; + +/** + * TODO. + */ +@SuppressWarnings("checkstyle:JavadocMethod") +@ConfigGroup +public interface TlsConfig { + + /** + * Whether SSL/TLS is enabled. + */ + @WithDefault("false") + boolean enabled(); + + /** + * Enable trusting all certificates. Disabled by default. + */ + @WithDefault("false") + boolean trustAll(); + + /** + * TODO. + */ + interface PemTrustCertificate { + /** + * TODO. + */ + @WithParentName + @WithDefault("false") + boolean enabled(); + + /** + * TODO. + */ + Optional> certs(); + + /** + * TODO. + */ + default PemTrustCertConfiguration convert() { + final PemTrustCertConfiguration trustCertificatePem = new PemTrustCertConfiguration(); + trustCertificatePem.enabled = enabled(); + trustCertificatePem.certs = certs(); + return trustCertificatePem; + } + } + + /** + * Trust configuration in the PEM format. + *

+ * When enabled, {@code #trust-certificate-jks} and {@code #trust-certificate-pfx} must be disabled. + */ + PemTrustCertificate trustCertificatePem(); + + /** + * TODO. + */ + interface Jks { + /** + * TODO. + */ + @WithParentName + @WithDefault("false") + boolean enabled(); + + /** + * TODO. + */ + Optional path(); + + /** + * TODO. + */ + Optional password(); + + /** + * TODO. + */ + default JksConfiguration convert() { + final JksConfiguration jks = new JksConfiguration(); + jks.enabled = enabled(); + jks.path = path(); + jks.password = password(); + return jks; + } + } + + /** + * Trust configuration in the JKS format. + *

+ * When enabled, {@code #trust-certificate-pem} and {@code #trust-certificate-pfx} must be disabled. + */ + Jks trustCertificateJks(); + + /** + * TODO. + */ + interface Pfx { + /** + * TODO. + */ + @WithParentName + @WithDefault("false") + boolean enabled(); + + /** + * TODO. + */ + Optional path(); + + /** + * TODO. + */ + Optional password(); + + /** + * TODO. + */ + default PfxConfiguration convert() { + final PfxConfiguration jks = new PfxConfiguration(); + jks.enabled = enabled(); + jks.path = path(); + jks.password = password(); + return jks; + } + } + + /** + * Trust configuration in the PFX format. + *

+ * When enabled, {@code #trust-certificate-jks} and {@code #trust-certificate-pem} must be disabled. + */ + Pfx trustCertificatePfx(); + + /** + * TODO. + */ + interface PemKeyCert { + /** + * TODO. + */ + @WithParentName + @WithDefault("false") + boolean enabled(); + + /** + * TODO. + */ + Optional> keys(); + + /** + * TODO. + */ + Optional> certs(); + + /** + * TODO. + */ + default PemKeyCertConfiguration convert() { + final PemKeyCertConfiguration pemKeyCert = new PemKeyCertConfiguration(); + pemKeyCert.enabled = enabled(); + pemKeyCert.keys = keys(); + pemKeyCert.certs = certs(); + return pemKeyCert; + } + } + + /** + * Key/cert configuration in the PEM format. + *

+ * When enabled, {@code key-certificate-jks} and {@code #key-certificate-pfx} must be disabled. + */ + PemKeyCert keyCertificatePem(); + + /** + * Key/cert configuration in the JKS format. + *

+ * When enabled, {@code #key-certificate-pem} and {@code #key-certificate-pfx} must be disabled. + */ + Jks keyCertificateJks(); + + /** + * Key/cert configuration in the PFX format. + *

+ * When enabled, {@code key-certificate-jks} and {@code #key-certificate-pem} must be disabled. + */ + Pfx keyCertificatePfx(); + + /** + * The hostname verification algorithm to use in case the server's identity should be checked. + * Should be {@code HTTPS}, {@code LDAPS} or an {@code NONE} (default). + *

+ * If set to {@code NONE}, it does not verify the hostname. + *

+ */ + @WithDefault("NONE") + String hostnameVerificationAlgorithm(); + +} diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/vertx/VertxRedisClientFactory.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/vertx/VertxRedisClientFactory.java new file mode 100644 index 0000000000..31866c4c66 --- /dev/null +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/vertx/VertxRedisClientFactory.java @@ -0,0 +1,153 @@ +/** + * TODO. + */ +package org.eclipse.hono.deviceconnection.redis.client.vertx; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hono.deviceconnection.redis.client.config.NetConfig; +import org.eclipse.hono.deviceconnection.redis.client.config.RedisConfig; +import org.eclipse.hono.deviceconnection.redis.client.config.TlsConfig; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.vertx.core.runtime.SSLConfigHelper; +import io.vertx.core.Vertx; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.ProxyOptions; +import io.vertx.redis.client.Redis; +import io.vertx.redis.client.RedisClientType; +import io.vertx.redis.client.RedisOptions; + +/** + * Creates Vert.x Redis client for a given {@link RedisConfig}. + */ +public class VertxRedisClientFactory { + + public static final String CLIENT_NAME = "hono-deviceconnection"; + + private VertxRedisClientFactory() { + // Avoid direct instantiation. + } + + /** + * TODO. + * @param vertx TODO. + * @param config TODO. + * @return TODO. + * @throws ConfigurationException TODO. + */ + public static Redis create(final Vertx vertx, final RedisConfig config) throws ConfigurationException { + final RedisOptions options = new RedisOptions(); + + final List hosts; + if (config.hosts().isPresent()) { + hosts = new ArrayList<>(config.hosts().get()); + for (URI uri : config.hosts().get()) { + options.addConnectionString(uri.toString().trim()); + } + } else { + throw new ConfigurationException("Redis host not configured"); + } + + if (RedisClientType.STANDALONE == config.clientType()) { + if (hosts.size() > 1) { + throw new ConfigurationException("Multiple Redis hosts supplied for non-clustered configuration"); + } + } + + config.masterName().ifPresent(options::setMasterName); + options.setMaxNestedArrays(config.maxNestedArrays()); + options.setMaxPoolSize(config.maxPoolSize()); + options.setMaxPoolWaiting(config.maxPoolWaiting()); + options.setMaxWaitingHandlers(config.maxWaitingHandlers()); + + options.setProtocolNegotiation(config.protocolNegotiation()); + //config.preferredProtocolVersion().ifPresent(options::setPreferredProtocolVersion); + options.setPassword(config.password().orElse(null)); + config.poolCleanerInterval().ifPresent(d -> options.setPoolCleanerInterval((int) d.toMillis())); + options.setPoolRecycleTimeout((int) config.poolRecycleTimeout().toMillis()); + options.setHashSlotCacheTTL(config.hashSlotCacheTtl().toMillis()); + + config.role().ifPresent(options::setRole); + options.setType(config.clientType()); + config.replicas().ifPresent(options::setUseReplicas); + + options.setNetClientOptions(toNetClientOptions(config)); + + options.setPoolName(CLIENT_NAME); + // Use the convention defined by Quarkus Micrometer Vert.x metrics to create metrics prefixed with redis. + // and the client_name as tag. + // See io.quarkus.micrometer.runtime.binder.vertx.VertxMeterBinderAdapter.extractPrefix and + // io.quarkus.micrometer.runtime.binder.vertx.VertxMeterBinderAdapter.extractClientName + options.getNetClientOptions().setMetricsName("redis|" + CLIENT_NAME); + + return Redis.createClient(vertx, options); + } + + private static NetClientOptions toNetClientOptions(final RedisConfig config) { + final NetConfig tcp = config.tcp(); + final TlsConfig tls = config.tls(); + final NetClientOptions net = new NetClientOptions(); + + tcp.alpn().ifPresent(net::setUseAlpn); + tcp.applicationLayerProtocols().ifPresent(net::setApplicationLayerProtocols); + tcp.connectionTimeout().ifPresent(d -> net.setConnectTimeout((int) d.toMillis())); + + final String verificationAlgorithm = tls.hostnameVerificationAlgorithm(); + if ("NONE".equalsIgnoreCase(verificationAlgorithm)) { + net.setHostnameVerificationAlgorithm(""); + } else { + net.setHostnameVerificationAlgorithm(verificationAlgorithm); + } + + tcp.idleTimeout().ifPresent(d -> net.setIdleTimeout((int) d.toSeconds())); + + tcp.keepAlive().ifPresent(b -> net.setTcpKeepAlive(true)); + tcp.noDelay().ifPresent(b -> net.setTcpNoDelay(true)); + + net.setSsl(tls.enabled()).setTrustAll(tls.trustAll()); + + SSLConfigHelper.configurePemTrustOptions(net, tls.trustCertificatePem().convert()); + SSLConfigHelper.configureJksTrustOptions(net, tls.trustCertificateJks().convert()); + SSLConfigHelper.configurePfxTrustOptions(net, tls.trustCertificatePfx().convert()); + + SSLConfigHelper.configurePemKeyCertOptions(net, tls.keyCertificatePem().convert()); + SSLConfigHelper.configureJksKeyCertOptions(net, tls.keyCertificateJks().convert()); + SSLConfigHelper.configurePfxKeyCertOptions(net, tls.keyCertificatePfx().convert()); + + net.setReconnectAttempts(config.reconnectAttempts()); + net.setReconnectInterval(config.reconnectInterval().toMillis()); + + tcp.localAddress().ifPresent(net::setLocalAddress); + tcp.nonProxyHosts().ifPresent(net::setNonProxyHosts); + if (tcp.proxyOptions().host().isPresent()) { + final ProxyOptions po = new ProxyOptions(); + po.setHost(tcp.proxyOptions().host().get()); + po.setType(tcp.proxyOptions().type()); + po.setPort(tcp.proxyOptions().port()); + tcp.proxyOptions().username().ifPresent(po::setUsername); + tcp.proxyOptions().password().ifPresent(po::setPassword); + net.setProxyOptions(po); + } + tcp.readIdleTimeout().ifPresent(d -> net.setReadIdleTimeout((int) d.toSeconds())); + tcp.reconnectAttempts().ifPresent(net::setReconnectAttempts); + tcp.reconnectInterval().ifPresent(v -> net.setReconnectInterval(v.toMillis())); + tcp.reuseAddress().ifPresent(net::setReuseAddress); + tcp.reusePort().ifPresent(net::setReusePort); + tcp.receiveBufferSize().ifPresent(net::setReceiveBufferSize); + tcp.sendBufferSize().ifPresent(net::setSendBufferSize); + tcp.soLinger().ifPresent(d -> net.setSoLinger((int) d.toMillis())); + tcp.secureTransportProtocols().ifPresent(net::setEnabledSecureTransportProtocols); + tcp.trafficClass().ifPresent(net::setTrafficClass); + tcp.noDelay().ifPresent(net::setTcpNoDelay); + tcp.cork().ifPresent(net::setTcpCork); + tcp.keepAlive().ifPresent(net::setTcpKeepAlive); + tcp.fastOpen().ifPresent(net::setTcpFastOpen); + tcp.quickAck().ifPresent(net::setTcpQuickAck); + tcp.writeIdleTimeout().ifPresent(d -> net.setWriteIdleTimeout((int) d.toSeconds())); + + return net; + } +} diff --git a/client-device-connection-redis/src/main/resources/META-INF/native-image/org.eclipse.hono/client-device-connection-infinispan/native-image.properties b/client-device-connection-redis/src/main/resources/META-INF/native-image/org.eclipse.hono/client-device-connection-infinispan/native-image.properties new file mode 100644 index 0000000000..2879f33e15 --- /dev/null +++ b/client-device-connection-redis/src/main/resources/META-INF/native-image/org.eclipse.hono/client-device-connection-infinispan/native-image.properties @@ -0,0 +1,15 @@ +# Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0 +# +# SPDX-License-Identifier: EPL-2.0 +Args = -H:ResourceConfigurationResources=${.}/resources-config.json \ + -H:AdditionalSecurityProviders=org.wildfly.security.sasl.digest.WildFlyElytronSaslDigestProvider \ + -H:AdditionalSecurityProviders=org.wildfly.security.sasl.external.WildFlyElytronSaslExternalProvider \ + -H:AdditionalSecurityProviders=org.wildfly.security.sasl.plain.WildFlyElytronSaslPlainProvider \ + -H:AdditionalSecurityProviders=org.wildfly.security.sasl.scram.WildFlyElytronSaslScramProvider diff --git a/client-device-connection-redis/src/main/resources/META-INF/native-image/org.eclipse.hono/client-device-connection-infinispan/resources-config.json b/client-device-connection-redis/src/main/resources/META-INF/native-image/org.eclipse.hono/client-device-connection-infinispan/resources-config.json new file mode 100644 index 0000000000..ef9f0db4ab --- /dev/null +++ b/client-device-connection-redis/src/main/resources/META-INF/native-image/org.eclipse.hono/client-device-connection-infinispan/resources-config.json @@ -0,0 +1,37 @@ +{ + "resources": { + "includes": [ + { + "pattern": ".*\\.properties$" + }, + { + "pattern": ".*\\.proto$" + }, + { + "pattern": "default-configs\\/.*$" + }, + { + "pattern": "META-INF\\/services\\/java\\.security\\.Provider" + }, + { + "pattern": "META-INF\\/services\\/javax\\.security\\.sasl\\.SaslClientFactory" + }, + { + "pattern": "META-INF\\/services\\/org\\.infinispan\\.configuration\\.parsing\\.ConfigurationParser" + }, + { + "pattern": "META-INF\\/services\\/org\\.infinispan\\.factories\\.impl\\..*$" + }, + { + "pattern": "META-INF\\/services\\/org\\.infinispan\\.protostream\\..*$" + } + ], + "excludes": [ + { + "pattern": "META-INF\\/maven\\/.*$" + },{ + "pattern": "META-INF\\/native-image\\/.*$" + } + ] + } +} diff --git a/client-device-connection-redis/src/main/resources/application.properties b/client-device-connection-redis/src/main/resources/application.properties new file mode 100644 index 0000000000..7fce6fc1be --- /dev/null +++ b/client-device-connection-redis/src/main/resources/application.properties @@ -0,0 +1,11 @@ +# Create a Jandex index of beans contained in Google Guava +# This prevents warnings when building downstream modules that use for example +# the com.google.common.base.MoreObjects$ToStringHelper method. +quarkus.index-dependency.guava.group-id=com.google.guava +quarkus.index-dependency.guava.artifact-id=guava +# Create a Jandex index of beans contained in the Infinispan Hotrod client +# This is necessary in order to be able to configure the Hotrod client by +# means of the org.eclipse.hono.deviceconnection.infinispan.client.InfinispanRemoteConfigurationOptions +# class. +quarkus.index-dependency.infinispan.group-id=org.infinispan +quarkus.index-dependency.infinispan.artifact-id=infinispan-client-hotrod diff --git a/client-device-connection-redis/src/test/resources/logback-test.xml b/client-device-connection-redis/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..fb0c54d42d --- /dev/null +++ b/client-device-connection-redis/src/test/resources/logback-test.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + diff --git a/client-device-connection-redis/src/test/resources/remote-cache-options.yaml b/client-device-connection-redis/src/test/resources/remote-cache-options.yaml new file mode 100644 index 0000000000..19b061cf42 --- /dev/null +++ b/client-device-connection-redis/src/test/resources/remote-cache-options.yaml @@ -0,0 +1,34 @@ +hono: + cache: + infinispan: + serverList: "data-grid:11222" + authServerName: "data-grid" + authUsername: "user" + authPassword: "secret" + authRealm: "ApplicationRealm" + cluster: + siteA: "hostA1:11222; hostA2:11223" + siteB: "hostB1:11222; hostB2:11223" + connectionPool: + minIdle: 10 + maxActive: 10 + maxPendingRequests: 400 + maxWait: 500 + defaultExecutorFactory: + poolSize: 200 + saslMechanism: "DIGEST-MD5" + saslProperties: + "javax.security.sasl.qop": "auth" + socketTimeout: 5000 + connectTimeout: 5000 + keyStoreFileName: "/etc/hono/key-store.p12" + keyStoreType: "PKCS12" + keyStorePassword: "key-store-secret" + keyAlias: "infinispan" + keyStoreCertificatePassword: "cert-secret" + trustStorePath: "/etc/hono/trust-store.p12" + trustStoreFileName: "/etc/hono/trust-store-file.p12" + trustStoreType: "PKCS12" + trustStorePassword: "trust-store-secret" + useSsl: true + sslCiphers: "TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256" diff --git a/client-device-connection/pom.xml b/client-device-connection/pom.xml new file mode 100644 index 0000000000..6a3848e74f --- /dev/null +++ b/client-device-connection/pom.xml @@ -0,0 +1,164 @@ + + + + 4.0.0 + + org.eclipse.hono + hono-bom + 2.6.0-SNAPSHOT + ../bom + + client-device-connection + + Device Connection client + Base classes for client for accessing device connection information in a remote cache / data grid. + + + + org.eclipse.hono + hono-legal + + + org.eclipse.hono + hono-core + + + org.eclipse.hono + hono-client-common + + + + org.slf4j + slf4j-api + + + io.opentracing + opentracing-api + + + + io.smallrye.config + smallrye-config-core + + + + io.vertx + vertx-core + + + + io.vertx + vertx-health-check + + + + com.google.guava + guava + + + io.quarkus + quarkus-core + true + + + org.jboss.logmanager + jboss-logmanager-embedded + + + org.jboss.logging + jboss-logging-annotations + + + io.quarkus + quarkus-development-mode-spi + + + io.quarkus + quarkus-bootstrap-runner + + + org.jboss.slf4j + slf4j-jboss-logmanager + + + org.graalvm.sdk + graal-sdk + + + io.quarkus + quarkus-fs-util + + + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + com.google.truth + truth + test + + + ch.qos.logback + logback-classic + test + + + org.mockito + mockito-core + test + + + io.vertx + vertx-junit5 + test + + + org.eclipse.hono + core-test-utils + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.jboss.jandex + jandex-maven-plugin + + + org.jacoco + jacoco-maven-plugin + + + + diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/AdapterInstanceStatusProvider.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/AdapterInstanceStatusProvider.java similarity index 97% rename from client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/AdapterInstanceStatusProvider.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/AdapterInstanceStatusProvider.java index 2c98c8a2c0..64a18cf0f6 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/AdapterInstanceStatusProvider.java +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/AdapterInstanceStatusProvider.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 *******************************************************************************/ -package org.eclipse.hono.deviceconnection.infinispan.client; +package org.eclipse.hono.deviceconnection.common; import java.util.Collection; import java.util.Set; diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/Cache.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/Cache.java similarity index 98% rename from client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/Cache.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/Cache.java index eec68f8f7c..42d28a015c 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/Cache.java +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/Cache.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.hono.deviceconnection.infinispan.client; +package org.eclipse.hono.deviceconnection.common; import java.util.Map; import java.util.Set; diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfo.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CacheBasedDeviceConnectionInfo.java similarity index 99% rename from client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfo.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CacheBasedDeviceConnectionInfo.java index 21f3bd28a7..caa3e24172 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfo.java +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CacheBasedDeviceConnectionInfo.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.hono.deviceconnection.infinispan.client; +package org.eclipse.hono.deviceconnection.common; import java.net.HttpURLConnection; import java.time.Duration; diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheConfig.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheConfig.java similarity index 97% rename from client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheConfig.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheConfig.java index ec4109424c..c969ab3c49 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheConfig.java +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheConfig.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 *******************************************************************************/ -package org.eclipse.hono.deviceconnection.infinispan.client; +package org.eclipse.hono.deviceconnection.common; import com.google.common.base.MoreObjects; diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheOptions.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheOptions.java similarity index 95% rename from client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheOptions.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheOptions.java index 8118a198f5..bbd69c036d 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheOptions.java +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheOptions.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.hono.deviceconnection.infinispan.client; +package org.eclipse.hono.deviceconnection.common; import org.eclipse.hono.util.CommandRouterConstants; diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceConnectionInfo.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceConnectionInfo.java similarity index 99% rename from client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceConnectionInfo.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceConnectionInfo.java index 4b7e800298..f3c2d2a35c 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceConnectionInfo.java +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceConnectionInfo.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.hono.deviceconnection.infinispan.client; +package org.eclipse.hono.deviceconnection.common; import java.time.Duration; import java.util.Map; diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceToAdapterMappingErrorListener.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceToAdapterMappingErrorListener.java similarity index 95% rename from client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceToAdapterMappingErrorListener.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceToAdapterMappingErrorListener.java index bdb45e069b..0fa4d7fb95 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceToAdapterMappingErrorListener.java +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceToAdapterMappingErrorListener.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.hono.deviceconnection.infinispan.client; +package org.eclipse.hono.deviceconnection.common; import io.opentracing.Span; import io.vertx.core.Future; diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/UnknownStatusProvider.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/UnknownStatusProvider.java similarity index 94% rename from client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/UnknownStatusProvider.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/UnknownStatusProvider.java index d52468ce44..861ecfda79 100644 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/UnknownStatusProvider.java +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/UnknownStatusProvider.java @@ -12,7 +12,7 @@ */ -package org.eclipse.hono.deviceconnection.infinispan.client; +package org.eclipse.hono.deviceconnection.common; import java.util.Collection; import java.util.Set; diff --git a/client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfoTest.java b/client-device-connection/src/test/java/org/eclipse/hono/deviceconnection/common/CacheBasedDeviceConnectionInfoTest.java similarity index 99% rename from client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfoTest.java rename to client-device-connection/src/test/java/org/eclipse/hono/deviceconnection/common/CacheBasedDeviceConnectionInfoTest.java index 744858a2be..a6efb9126d 100644 --- a/client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfoTest.java +++ b/client-device-connection/src/test/java/org/eclipse/hono/deviceconnection/common/CacheBasedDeviceConnectionInfoTest.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.hono.deviceconnection.infinispan.client; +package org.eclipse.hono.deviceconnection.common; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/client-device-connection/src/test/java/org/eclipse/hono/deviceconnection/common/CommonCacheQuarkusPropertyBindingTest.java b/client-device-connection/src/test/java/org/eclipse/hono/deviceconnection/common/CommonCacheQuarkusPropertyBindingTest.java new file mode 100644 index 0000000000..753711f531 --- /dev/null +++ b/client-device-connection/src/test/java/org/eclipse/hono/deviceconnection/common/CommonCacheQuarkusPropertyBindingTest.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021, 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.hono.deviceconnection.common; + +import static com.google.common.truth.Truth.assertThat; + +import org.eclipse.hono.test.ConfigMappingSupport; +import org.junit.jupiter.api.Test; + +/** + * Tests verifying binding of configuration properties to {@link CommonCacheConfig}. + * + */ +public class CommonCacheQuarkusPropertyBindingTest { + + @Test + void testCommonCacheConfigurationPropertiesArePickedUp() { + final var commonCacheConfig = new CommonCacheConfig( + ConfigMappingSupport.getConfigMapping( + CommonCacheOptions.class, + this.getClass().getResource("/common-cache-options.yaml"))); + + assertThat(commonCacheConfig.getCacheName()).isEqualTo("the-cache"); + assertThat(commonCacheConfig.getCheckKey()).isEqualTo("the-key"); + assertThat(commonCacheConfig.getCheckValue()).isEqualTo("the-value"); + } +} diff --git a/client-device-connection-infinispan/src/test/resources/common-cache-options.yaml b/client-device-connection/src/test/resources/common-cache-options.yaml similarity index 100% rename from client-device-connection-infinispan/src/test/resources/common-cache-options.yaml rename to client-device-connection/src/test/resources/common-cache-options.yaml diff --git a/client-device-connection/src/test/resources/logback-test.xml b/client-device-connection/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..42747dae3b --- /dev/null +++ b/client-device-connection/src/test/resources/logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index fdcafbacd4..56facc63da 100644 --- a/pom.xml +++ b/pom.xml @@ -178,7 +178,9 @@ bom core cli + client-device-connection client-device-connection-infinispan + client-device-connection-redis clients demo-certs examples diff --git a/services/command-router/pom.xml b/services/command-router/pom.xml index aeaa5a4738..fb530d9aab 100644 --- a/services/command-router/pom.xml +++ b/services/command-router/pom.xml @@ -24,10 +24,19 @@ A Quarkus based implementation of Hono's Command Router API that is using Infinispan for storing data. + + org.eclipse.hono + client-device-connection + org.eclipse.hono client-device-connection-infinispan + + org.eclipse.hono + client-device-connection-redis + + org.eclipse.hono hono-client-command-amqp diff --git a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/AdapterInstanceStatusService.java b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/AdapterInstanceStatusService.java index bf3dec738a..16eb5653df 100644 --- a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/AdapterInstanceStatusService.java +++ b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/AdapterInstanceStatusService.java @@ -13,7 +13,7 @@ package org.eclipse.hono.commandrouter; -import org.eclipse.hono.deviceconnection.infinispan.client.AdapterInstanceStatusProvider; +import org.eclipse.hono.deviceconnection.common.AdapterInstanceStatusProvider; import org.eclipse.hono.util.Lifecycle; /** diff --git a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/CommandTargetMapper.java b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/CommandTargetMapper.java index d39fc6eb45..482efeb4da 100644 --- a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/CommandTargetMapper.java +++ b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/CommandTargetMapper.java @@ -15,7 +15,7 @@ import org.eclipse.hono.client.registry.DeviceRegistrationClient; import org.eclipse.hono.commandrouter.impl.CommandTargetMapperImpl; -import org.eclipse.hono.deviceconnection.infinispan.client.DeviceConnectionInfo; +import org.eclipse.hono.deviceconnection.common.DeviceConnectionInfo; import io.opentracing.SpanContext; import io.opentracing.Tracer; diff --git a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/app/Application.java b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/app/Application.java index 81c1309fb8..44d98f5da5 100644 --- a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/app/Application.java +++ b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/app/Application.java @@ -61,7 +61,7 @@ import org.eclipse.hono.commandrouter.impl.pubsub.PubSubBasedCommandConsumerFactoryImpl; import org.eclipse.hono.config.ServiceConfigProperties; import org.eclipse.hono.config.ServiceOptions; -import org.eclipse.hono.deviceconnection.infinispan.client.DeviceConnectionInfo; +import org.eclipse.hono.deviceconnection.common.DeviceConnectionInfo; import org.eclipse.hono.service.HealthCheckProvider; import org.eclipse.hono.service.NotificationSupportingServiceApplication; import org.eclipse.hono.service.amqp.AmqpEndpoint; diff --git a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/app/DeviceConnectionInfoProducer.java b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/app/DeviceConnectionInfoProducer.java index 07b1482bbb..971d013aea 100644 --- a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/app/DeviceConnectionInfoProducer.java +++ b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/app/DeviceConnectionInfoProducer.java @@ -14,6 +14,7 @@ package org.eclipse.hono.commandrouter.app; +import io.quarkus.runtime.configuration.ConfigurationException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -23,15 +24,18 @@ import org.eclipse.hono.commandrouter.CommandRouterServiceOptions; import org.eclipse.hono.commandrouter.impl.KubernetesBasedAdapterInstanceStatusService; import org.eclipse.hono.commandrouter.impl.UnknownStatusProvidingService; -import org.eclipse.hono.deviceconnection.infinispan.client.BasicCache; -import org.eclipse.hono.deviceconnection.infinispan.client.CacheBasedDeviceConnectionInfo; -import org.eclipse.hono.deviceconnection.infinispan.client.CommonCacheConfig; -import org.eclipse.hono.deviceconnection.infinispan.client.CommonCacheOptions; -import org.eclipse.hono.deviceconnection.infinispan.client.DeviceConnectionInfo; +import org.eclipse.hono.deviceconnection.common.Cache; +import org.eclipse.hono.deviceconnection.common.CacheBasedDeviceConnectionInfo; +import org.eclipse.hono.deviceconnection.common.CommonCacheConfig; +import org.eclipse.hono.deviceconnection.common.CommonCacheOptions; +import org.eclipse.hono.deviceconnection.common.DeviceConnectionInfo; import org.eclipse.hono.deviceconnection.infinispan.client.EmbeddedCache; import org.eclipse.hono.deviceconnection.infinispan.client.HotrodCache; import org.eclipse.hono.deviceconnection.infinispan.client.InfinispanRemoteConfigurationOptions; import org.eclipse.hono.deviceconnection.infinispan.client.InfinispanRemoteConfigurationProperties; +import org.eclipse.hono.deviceconnection.redis.client.RedisCacheVertx; +import org.eclipse.hono.deviceconnection.redis.client.config.RedisConfig; +import org.eclipse.hono.deviceconnection.redis.client.vertx.VertxRedisClientFactory; import org.eclipse.hono.util.Strings; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; @@ -44,6 +48,7 @@ import io.opentracing.Tracer; import io.smallrye.config.ConfigMapping; import io.vertx.core.Vertx; +import io.vertx.redis.client.RedisAPI; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import jakarta.inject.Singleton; @@ -64,35 +69,50 @@ public class DeviceConnectionInfoProducer { @Produces DeviceConnectionInfo deviceConnectionInfo( - final BasicCache cache, + final Cache cache, final Tracer tracer, final AdapterInstanceStatusService adapterInstanceStatusService) { return new CacheBasedDeviceConnectionInfo(cache, tracer, adapterInstanceStatusService); } @Produces - BasicCache cache( + Cache cache( final Vertx vertx, @ConfigMapping(prefix = "hono.commandRouter.cache.common") final CommonCacheOptions commonCacheOptions, @ConfigMapping(prefix = "hono.commandRouter.cache.remote") - final InfinispanRemoteConfigurationOptions remoteCacheConfigurationOptions) { + final InfinispanRemoteConfigurationOptions remoteCacheConfigurationOptions, + @ConfigMapping(prefix = "hono.commandRouter.cache.redis") + final RedisConfig redisCacheConfiguration + ) { final var commonCacheConfig = new CommonCacheConfig(commonCacheOptions); final var infinispanCacheConfig = new InfinispanRemoteConfigurationProperties(remoteCacheConfigurationOptions); - if (Strings.isNullOrEmpty(infinispanCacheConfig.getServerList())) { - LOG.info("configuring embedded cache"); - return new EmbeddedCache<>( - vertx, - embeddedCacheManager(commonCacheConfig), - commonCacheConfig.getCacheName()); - } else { + + if (!Strings.isNullOrEmpty(infinispanCacheConfig.getServerList()) && + redisCacheConfiguration.hosts().isPresent()) { + throw new ConfigurationException( + "Both hotrod (remote) and redis cache configuration exists. Only one should be provided."); + } + + if (!Strings.isNullOrEmpty(infinispanCacheConfig.getServerList())) { LOG.info("configuring remote cache"); return HotrodCache.from( vertx, infinispanCacheConfig, commonCacheConfig); } + + if (redisCacheConfiguration.hosts().isPresent()) { + LOG.info("configuring redis cache"); + return RedisCacheVertx.from(RedisAPI.api(VertxRedisClientFactory.create(vertx, redisCacheConfiguration))); + } + + LOG.info("configuring embedded cache"); + return new EmbeddedCache<>( + vertx, + embeddedCacheManager(commonCacheConfig), + commonCacheConfig.getCacheName()); } private EmbeddedCacheManager embeddedCacheManager(final CommonCacheConfig cacheConfig) { diff --git a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/impl/CommandRouterServiceImpl.java b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/impl/CommandRouterServiceImpl.java index 6d53adac9f..713ff373d6 100644 --- a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/impl/CommandRouterServiceImpl.java +++ b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/impl/CommandRouterServiceImpl.java @@ -38,7 +38,7 @@ import org.eclipse.hono.commandrouter.CommandRouterResult; import org.eclipse.hono.commandrouter.CommandRouterService; import org.eclipse.hono.config.ServiceConfigProperties; -import org.eclipse.hono.deviceconnection.infinispan.client.DeviceConnectionInfo; +import org.eclipse.hono.deviceconnection.common.DeviceConnectionInfo; import org.eclipse.hono.service.HealthCheckProvider; import org.eclipse.hono.tracing.TracingHelper; import org.eclipse.hono.util.CommandConstants; diff --git a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/impl/CommandTargetMapperImpl.java b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/impl/CommandTargetMapperImpl.java index 17e60de2a5..1f4a863fc6 100644 --- a/services/command-router/src/main/java/org/eclipse/hono/commandrouter/impl/CommandTargetMapperImpl.java +++ b/services/command-router/src/main/java/org/eclipse/hono/commandrouter/impl/CommandTargetMapperImpl.java @@ -23,7 +23,7 @@ import org.eclipse.hono.client.registry.DeviceDisabledOrNotRegisteredException; import org.eclipse.hono.client.registry.DeviceRegistrationClient; import org.eclipse.hono.commandrouter.CommandTargetMapper; -import org.eclipse.hono.deviceconnection.infinispan.client.DeviceConnectionInfo; +import org.eclipse.hono.deviceconnection.common.DeviceConnectionInfo; import org.eclipse.hono.tracing.TracingHelper; import org.eclipse.hono.util.DeviceConnectionConstants; import org.eclipse.hono.util.MessageHelper; diff --git a/services/command-router/src/test/java/org/eclipse/hono/commandrouter/impl/CommandRouterServiceImplTest.java b/services/command-router/src/test/java/org/eclipse/hono/commandrouter/impl/CommandRouterServiceImplTest.java index 929d2bf701..5fa82469be 100644 --- a/services/command-router/src/test/java/org/eclipse/hono/commandrouter/impl/CommandRouterServiceImplTest.java +++ b/services/command-router/src/test/java/org/eclipse/hono/commandrouter/impl/CommandRouterServiceImplTest.java @@ -41,7 +41,7 @@ import org.eclipse.hono.client.util.MessagingClientProvider; import org.eclipse.hono.commandrouter.CommandConsumerFactory; import org.eclipse.hono.config.ServiceConfigProperties; -import org.eclipse.hono.deviceconnection.infinispan.client.DeviceConnectionInfo; +import org.eclipse.hono.deviceconnection.common.DeviceConnectionInfo; import org.eclipse.hono.test.VertxMockSupport; import org.eclipse.hono.util.CommandConstants; import org.eclipse.hono.util.EventConstants; diff --git a/tests/pom.xml b/tests/pom.xml index b7466c33bc..c1b956fa05 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -466,6 +466,22 @@ 350000000 + + + redis_cache + + + hono.commandrouting.cache + redis + + + + true + redis-cache + 350000000 + + + amqp @@ -764,6 +780,7 @@ 27017 9094 + 6379 15671 15672 @@ -1262,6 +1279,35 @@ + + + bitnami/redis + hono-redis-test + + false + + 6379:6379 + + + custom + ${custom.network.name} + redis + + 1500000000 + 1500000000 + + Redis + ${log.color.extra-services} + + + + .*(Ready to accept connections).* + + + yes + + + ${docker.repository}/hono-mongodb-test:${project.version} @@ -2227,7 +2273,7 @@ true **/hono-*-test:* - hono-*-test-*,all-in-one-*,cp-*,postgres-* + hono-*-test-*,all-in-one-*,cp-*,postgres-*,redis-* diff --git a/tests/src/test/resources/commandrouter/redis-cache/application.yml b/tests/src/test/resources/commandrouter/redis-cache/application.yml new file mode 100644 index 0000000000..7c5eada006 --- /dev/null +++ b/tests/src/test/resources/commandrouter/redis-cache/application.yml @@ -0,0 +1,92 @@ +hono: + app: + maxInstances: 1 + amqpMessagingDisabled: ${hono.amqp-messaging.disabled} + kafkaMessagingDisabled: ${hono.kafka-messaging.disabled} + auth: + host: "${hono.auth.host}" + port: 5671 + name: "command-router" + trustStorePath: "/opt/hono/config/certs/trusted-certs.pem" + jwksPollingInterval: "PT20S" + commandRouter: + amqp: + insecurePortEnabled: true + insecurePortBindAddress: "0.0.0.0" + cache: + redis: + hosts: "redis://redis:6379" + messaging: + name: 'Hono Command Router' + host: "${hono.amqp-network.host}" + port: 5673 + amqpHostname: "hono-internal" + keyPath: "/opt/hono/config/certs/command-router-key.pem" + certPath: "/opt/hono/config/certs/command-router-cert.pem" + trustStorePath: "/opt/hono/config/certs/trusted-certs.pem" + linkEstablishmentTimeout: ${link.establishment.timeout} + flowLatency: ${flow.latency} + requestTimeout: ${request.timeout} + registration: + name: 'Hono Command Router' + host: "${hono.registration.host}" + port: 5672 + username: "command-router@HONO" + password: "cmd-router-secret" + linkEstablishmentTimeout: ${link.establishment.timeout} + flowLatency: ${flow.latency} + requestTimeout: ${request.timeout} + tenant: + name: 'Hono Command Router' + host: "${hono.registration.host}" + port: 5672 + username: "command-router@HONO" + password: "cmd-router-secret" + linkEstablishmentTimeout: ${link.establishment.timeout} + flowLatency: ${flow.latency} + requestTimeout: ${request.timeout} + command: + name: 'Hono Command Router' + host: "${hono.amqp-network.host}" + port: 5673 + amqpHostname: "hono-internal" + keyPath: "/opt/hono/config/certs/command-router-key.pem" + certPath: "/opt/hono/config/certs/command-router-cert.pem" + trustStorePath: "/opt/hono/config/certs/trusted-certs.pem" + linkEstablishmentTimeout: ${link.establishment.timeout} + flowLatency: ${flow.latency} + requestTimeout: ${request.timeout} + kafka: + commonClientConfig: + bootstrap.servers: "${hono.kafka.bootstrap.servers}" + commandInternal: + producerConfig: + max.block.ms: ${kafka-client.producer.max-block-ms} + request.timeout.ms: ${kafka-client.producer.request-timeout-ms} + delivery.timeout.ms: ${kafka-client.producer.delivery-timeout-ms} + commandResponse: + producerConfig: + max.block.ms: ${kafka-client.producer.max-block-ms} + request.timeout.ms: ${kafka-client.producer.request-timeout-ms} + delivery.timeout.ms: ${kafka-client.producer.delivery-timeout-ms} + event: + producerConfig: + max.block.ms: ${kafka-client.producer.max-block-ms} + request.timeout.ms: ${kafka-client.producer.request-timeout-ms} + delivery.timeout.ms: ${kafka-client.producer.delivery-timeout-ms} + +quarkus: + otel: + exporter: + otlp: + endpoint: "${otel-collector.endpoint}" + console: + color: true + log: + level: INFO + min-level: TRACE + category: + "io.quarkus.vertx.core.runtime": + level: DEBUG + vertx: + max-event-loop-execute-time: ${max.event-loop.execute-time}