From 700797574cc9398dd0e450c96dd98ffcd1ee1063 Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Thu, 24 Aug 2023 01:56:34 +0000 Subject: [PATCH 01/20] WIP: Refactor client-device-connection-infinispan module into 3 cache modules (common, infinispan and redis) and a client-device-connection module --- bom/pom.xml | 20 + caches/cache-common/pom.xml | 112 +++ .../java/org/eclipse/hono/cache/Cache.java | 119 +++ .../eclipse/hono/cache/CommonCacheConfig.java | 86 +++ .../hono/cache/CommonCacheOptions.java | 52 ++ caches/cache-infinispan/pom.xml | 96 +++ .../infinispan/BasicCache.java | 223 ++++++ .../infinispan/EmbeddedCache.java | 115 +++ .../infinispan/HotrodCache.java | 239 ++++++ .../InfinispanRemoteConfigurationOptions.java | 202 +++++ ...finispanRemoteConfigurationProperties.java | 189 +++++ caches/cache-redis/pom.xml | 50 ++ .../deviceconnection/redis/RedisCache.java | 144 ++++ caches/pom.xml | 95 +++ client-device-connection-infinispan/pom.xml | 5 + client-device-connection/pom.xml | 63 ++ .../AdapterInstanceStatusProvider.java | 51 ++ .../CacheBasedDeviceConnectionInfo.java | 694 ++++++++++++++++++ .../DeviceConnectionInfo.java | 186 +++++ .../DeviceToAdapterMappingErrorListener.java | 37 + .../UnknownStatusProvider.java | 41 ++ pom.xml | 4 +- services/command-router/pom.xml | 15 + .../AdapterInstanceStatusService.java | 2 +- .../commandrouter/CommandTargetMapper.java | 2 +- .../hono/commandrouter/app/Application.java | 2 +- .../app/DeviceConnectionInfoProducer.java | 26 +- .../impl/CommandRouterServiceImpl.java | 2 +- .../impl/CommandTargetMapperImpl.java | 2 +- .../impl/CommandRouterServiceImplTest.java | 2 +- 30 files changed, 2857 insertions(+), 19 deletions(-) create mode 100644 caches/cache-common/pom.xml create mode 100644 caches/cache-common/src/main/java/org/eclipse/hono/cache/Cache.java create mode 100644 caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheConfig.java create mode 100644 caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheOptions.java create mode 100644 caches/cache-infinispan/pom.xml create mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/BasicCache.java create mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/EmbeddedCache.java create mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/HotrodCache.java create mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationOptions.java create mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationProperties.java create mode 100644 caches/cache-redis/pom.xml create mode 100644 caches/cache-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/RedisCache.java create mode 100644 caches/pom.xml create mode 100644 client-device-connection/pom.xml create mode 100644 client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/AdapterInstanceStatusProvider.java create mode 100644 client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/CacheBasedDeviceConnectionInfo.java create mode 100644 client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceConnectionInfo.java create mode 100644 client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceToAdapterMappingErrorListener.java create mode 100644 client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/UnknownStatusProvider.java diff --git a/bom/pom.xml b/bom/pom.xml index abf27d3e75..0f055100da 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -326,6 +326,26 @@ quarkus.vertx.max-event-loop-execute-time=${max.event-loop.execute-time:20000} hono-client-core ${project.version} + + org.eclipse.hono + hono-cache-common + ${project.version} + + + org.eclipse.hono + hono-cache-infinispan + ${project.version} + + + org.eclipse.hono + hono-cache-redis + ${project.version} + + + org.eclipse.hono + hono-client-device-connection + ${project.version} + org.eclipse.hono hono-client-common diff --git a/caches/cache-common/pom.xml b/caches/cache-common/pom.xml new file mode 100644 index 0000000000..bc8282b651 --- /dev/null +++ b/caches/cache-common/pom.xml @@ -0,0 +1,112 @@ + + + + 4.0.0 + + org.eclipse.hono + hono-caches-parent + 2.5.0-SNAPSHOT + + hono-cache-common + + Hono Cache Common + Classes required for implementing Hono caches + + + + io.vertx + vertx-core + + + org.eclipse.hono + hono-client-common + + + 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 + + + + + + + + + diff --git a/caches/cache-common/src/main/java/org/eclipse/hono/cache/Cache.java b/caches/cache-common/src/main/java/org/eclipse/hono/cache/Cache.java new file mode 100644 index 0000000000..ff48131da7 --- /dev/null +++ b/caches/cache-common/src/main/java/org/eclipse/hono/cache/Cache.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2020, 2021 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.cache; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; + +/** + * A simple {@code Map} like interface to a data grid cache. + * + * @param The type of keys used for looking up data. + * @param The type of values stored in grid. + */ +public interface Cache { + + /** + * Checks if the cache is connected to the data grid. + *

+ * If a cache is found to be not connected here, this method may trigger a connection (re)establishment. + * + * @return A future that is completed with information about a successful check's result. + * Otherwise, the future will be failed with a + * {@link org.eclipse.hono.client.ServerErrorException}. + */ + Future checkForCacheAvailability(); + + /** + * Puts a value to the cache. + * + * @param key The key. + * @param value The value. + * @return A succeeded future if the value has been stored successfully. + * A failed future if the value could not be stored in the cache. + * @throws NullPointerException if any of the parameters is {@code null}. + */ + Future put(K key, V value); + + /** + * Puts a value to the cache. + * + * @param key The key. + * @param value The value. + * @param lifespan The lifespan of the entry. A negative value is interpreted as an unlimited lifespan. + * @param lifespanUnit The time unit for the lifespan. + * @return A succeeded future if the value has been stored successfully. + * A failed future if the value could not be stored in the cache. + * @throws NullPointerException if any of the parameters is {@code null}. + */ + Future put(K key, V value, long lifespan, TimeUnit lifespanUnit); + + /** + * Puts all values of the given map to the cache. + * + * @param data The map with the entries to add. + * @return A succeeded future if the operation succeeded. + * A failed future if there was an error storing the entries in the cache. + * @throws NullPointerException if data is {@code null}. + */ + Future putAll(Map data); + + /** + * Puts all values of the given map to the cache. + * + * @param data The map with the entries to add. + * @param lifespan The lifespan of the entries. A negative value is interpreted as an unlimited lifespan. + * @param lifespanUnit The time unit for the lifespan. + * @return A succeeded future if the operation succeeded. + * A failed future if there was an error storing the entries in the cache. + * @throws NullPointerException if any of the parameters is {@code null}. + */ + Future putAll(Map data, long lifespan, TimeUnit lifespanUnit); + + /** + * Gets a value from the cache. + * + * @param key The key. + * @return A succeeded future containing the value or {@code null} if the + * cache didn't contain the key yet. + * A failed future if the value could not be read from the cache. + * @throws NullPointerException if key is {@code null}. + */ + Future get(K key); + + /** + * Removes a key/value mapping from the cache. + * + * @param key The key. + * @param value The value. + * @return A succeeded future containing {@code true} if the key was + * mapped to the value, {@code false} otherwise. + * A failed future if the value could not be removed from the cache. + * @throws NullPointerException if any of the parameters is {@code null}. + */ + Future remove(K key, V value); + + /** + * Gets the values for the specified keys from the cache. + * + * @param keys The keys. + * @return A succeeded future containing a map with key/value pairs. + * @throws NullPointerException if keys is {@code null}. + */ + Future> getAll(Set keys); +} diff --git a/caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheConfig.java b/caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheConfig.java new file mode 100644 index 0000000000..573474bb12 --- /dev/null +++ b/caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheConfig.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2020, 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.cache; + +import com.google.common.base.MoreObjects; + +/** + * Common cache configuration options. + */ +public class CommonCacheConfig { + + /** + * The default name of the (remote) cache in the data grid that is used for + * storing device connection information. + */ + public static final String DEFAULT_CACHE_NAME = "device-connection"; + + private String cacheName = DEFAULT_CACHE_NAME; + + private String checkKey = "KEY_CONNECTION_CHECK"; + private String checkValue = "VALUE_CONNECTION_CHECK"; + + /** + * Creates properties for default values. + */ + public CommonCacheConfig() { + super(); + } + + /** + * Creates properties for existing options. + * + * @param options The options to copy. + */ + public CommonCacheConfig(final CommonCacheOptions options) { + super(); + this.cacheName = options.cacheName(); + this.checkKey = options.checkKey(); + this.checkValue = options.checkValue(); + } + + public void setCacheName(final String cacheName) { + this.cacheName = cacheName; + } + + public String getCacheName() { + return cacheName; + } + + public void setCheckKey(final String checkKey) { + this.checkKey = checkKey; + } + + public String getCheckKey() { + return checkKey; + } + + public void setCheckValue(final String checkValue) { + this.checkValue = checkValue; + } + + public String getCheckValue() { + return checkValue; + } + + @Override + public String toString() { + return MoreObjects + .toStringHelper(this) + .add("cacheName", this.cacheName) + .add("checkKey", this.checkKey) + .add("checkValue", this.checkValue) + .toString(); + } +} diff --git a/caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheOptions.java b/caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheOptions.java new file mode 100644 index 0000000000..1820c99a06 --- /dev/null +++ b/caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheOptions.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2021 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.cache; + +import org.eclipse.hono.util.CommandRouterConstants; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.ConfigMapping.NamingStrategy; +import io.smallrye.config.WithDefault; + +/** + * Common options for configuring a cache. + * + */ +@ConfigMapping(prefix = "hono.cache.common", namingStrategy = NamingStrategy.VERBATIM) +public interface CommonCacheOptions { + + /** + * Gets the name of the cache. + * + * @return The name. + */ + @WithDefault(CommandRouterConstants.DEFAULT_CACHE_NAME) + String cacheName(); + + /** + * Gets the key to use for checking the cache's availability. + * + * @return The key. + */ + @WithDefault("KEY_CONNECTION_CHECK") + String checkKey(); + + /** + * The value to use for checking the cache's availability. + * + * @return The value. + */ + @WithDefault("VALUE_CONNECTION_CHECK") + String checkValue(); +} diff --git a/caches/cache-infinispan/pom.xml b/caches/cache-infinispan/pom.xml new file mode 100644 index 0000000000..633a7f7376 --- /dev/null +++ b/caches/cache-infinispan/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + + org.eclipse.hono + hono-caches-parent + 2.5.0-SNAPSHOT + + hono-cache-infinispan + + Hono Client Device Connection Cache using Infinispan + Infinispan implementation of Hono's Client Device Connection cache + + + + org.eclipse.hono + hono-cache-common + + + org.infinispan + infinispan-client-hotrod + + + io.netty + netty-transport-native-epoll + + + + + org.infinispan + infinispan-query-dsl + + + org.infinispan + infinispan-core + + + 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 + + + + + + + + diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/BasicCache.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/BasicCache.java new file mode 100644 index 0000000000..47fe0603c7 --- /dev/null +++ b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/BasicCache.java @@ -0,0 +1,223 @@ +/** + * Copyright (c) 2020, 2021 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.infinispan; + +import java.net.HttpURLConnection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; + +import org.eclipse.hono.cache.Cache; +import org.eclipse.hono.client.ServerErrorException; +import org.eclipse.hono.util.Futures; +import org.eclipse.hono.util.Lifecycle; +import org.infinispan.commons.api.BasicCacheContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; + +/** + * An abstract base class for implementing caches based on an + * Infinispan {@link org.infinispan.commons.api.BasicCache}. + * + * @param The type of the key. + * @param The type of the value. + */ +public abstract class BasicCache implements Cache, Lifecycle { + + private static final Logger LOG = LoggerFactory.getLogger(BasicCache.class); + + protected final Vertx vertx; + private final BasicCacheContainer cacheManager; + private final AtomicBoolean stopCalled = new AtomicBoolean(); + + private org.infinispan.commons.api.BasicCache cache; + + /** + * Creates a new instance. + * + * @param vertx The vert.x instance to run on. + * @param cacheManager The cache manager. + */ + protected BasicCache(final Vertx vertx, final BasicCacheContainer cacheManager) { + this.vertx = Objects.requireNonNull(vertx); + this.cacheManager = Objects.requireNonNull(cacheManager); + } + + /** + * Called to trigger connecting the cache. + * + * @return A future tracking the progress, never returns {@code null}. + */ + protected abstract Future connectToCache(); + + /** + * Checks if the cache manager is started. + * + * @return {@code true} if the cache manager is started, {@code false} otherwise. + */ + protected abstract boolean isStarted(); + + @Override + public Future start() { + LOG.info("starting cache"); + return connectToCache(); + } + + @Override + public Future stop() { + if (!stopCalled.compareAndSet(false, true)) { + return Future.succeededFuture(); + } + LOG.info("stopping cache"); + setCache(null); + final Promise result = Promise.promise(); + vertx.executeBlocking(r -> { + try { + cacheManager.stop(); + r.complete(); + } catch (final Exception t) { + r.fail(t); + } + }, (AsyncResult stopAttempt) -> { + if (stopAttempt.succeeded()) { + LOG.info("connection(s) to cache stopped successfully"); + } else { + LOG.info("error trying to stop connection(s) to cache", stopAttempt.cause()); + } + result.handle(stopAttempt); + }); + return result.future(); + } + + protected void setCache(final org.infinispan.commons.api.BasicCache cache) { + this.cache = cache; + } + + protected org.infinispan.commons.api.BasicCache getCache() { + return this.cache; + } + + /** + * Performs a task with a connected cache. + *

+ * The method checks if the cache instance has been set. If that is the case, then the + * supplier will be invoked, providing a non-null cache instance. + *

+ * If the cache has not been set (yet) or it has been stopped, the supplier will not be + * called and a failed future will be returned, provided by {@link #noConnectionFailure()}. + * + * @param The type of the return value. + * @param futureSupplier The supplier, providing the operation which should be invoked. + * @return The future, tracking the result of the operation. + */ + protected final Future withCache( + final Function, CompletionStage> futureSupplier) { + + return Optional.ofNullable(cache) + .map(c -> Futures.create(() -> futureSupplier.apply(c))) + .orElseGet(BasicCache::noConnectionFailure) + .onComplete(this::postCacheAccess); + } + + /** + * Performs extra processing on the result of a cache operation returned by {@link #withCache(Function)}. + *

+ * Subclasses should override this method if needed. + *

+ * This default implementation does nothing. + * + * @param The type of the return value. + * @param cacheOperationResult The result of the cache operation. + */ + protected void postCacheAccess(final AsyncResult cacheOperationResult) { + // nothing done by default + } + + @Override + public Future put(final K key, final V value) { + Objects.requireNonNull(key); + Objects.requireNonNull(value); + + return withCache(aCache -> aCache.putAsync(key, value).thenApply(v -> (Void) null)); + } + + @Override + public Future put(final K key, final V value, final long lifespan, final TimeUnit lifespanUnit) { + Objects.requireNonNull(key); + Objects.requireNonNull(value); + Objects.requireNonNull(lifespanUnit); + + return withCache(aCache -> aCache.putAsync(key, value, lifespan, lifespanUnit).thenApply(v -> (Void) null)); + } + + @Override + public Future putAll(final Map data) { + Objects.requireNonNull(data); + + return withCache(aCache -> aCache.putAllAsync(data)); + } + + @Override + public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { + Objects.requireNonNull(data); + Objects.requireNonNull(lifespanUnit); + + return withCache(aCache -> aCache.putAllAsync(data, lifespan, lifespanUnit)); + } + + @Override + public Future remove(final K key, final V value) { + Objects.requireNonNull(key); + Objects.requireNonNull(value); + + return withCache(aCache -> aCache.removeAsync(key, value)); + } + + @Override + public Future get(final K key) { + Objects.requireNonNull(key); + + return withCache(aCache -> aCache.getAsync(key)); + } + + @Override + public Future> getAll(final Set keys) { + Objects.requireNonNull(keys); + + return withCache(aCache -> aCache.getAllAsync(keys)); + } + + /** + * Returns a failed future, reporting a missing connection to the cache. + * + * @param The value type of the returned future. + * @return A failed future, never returns {@code null}. + */ + protected static Future noConnectionFailure() { + + return Future.failedFuture(new ServerErrorException( + HttpURLConnection.HTTP_UNAVAILABLE, "no connection to data grid")); + } + +} diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/EmbeddedCache.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/EmbeddedCache.java new file mode 100644 index 0000000000..efbfeee309 --- /dev/null +++ b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/EmbeddedCache.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2020, 2021 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.infinispan; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.infinispan.manager.EmbeddedCacheManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; + +/** + * An embedded cache. + * + * @param The type of keys used by the cache. + * @param The type of values stored in the cache. + */ +public class EmbeddedCache extends BasicCache { + + private static final Logger LOG = LoggerFactory.getLogger(EmbeddedCache.class); + + private final AtomicBoolean connecting = new AtomicBoolean(false); + + private final EmbeddedCacheManager cacheManager; + private final String cacheName; + + /** + * Creates a new embedded cache instance. + * + * @param vertx The vert.x instance to run on. + * @param cacheManager The connection to the cache. + * @param cacheName The name of the cache. + */ + public EmbeddedCache( + final Vertx vertx, + final EmbeddedCacheManager cacheManager, + final String cacheName) { + super(vertx, cacheManager); + this.cacheManager = Objects.requireNonNull(cacheManager); + this.cacheName = Objects.requireNonNull(cacheName); + } + + @Override + protected boolean isStarted() { + return cacheManager.isRunning(cacheName) && getCache() != null; + } + + @Override + protected Future connectToCache() { + + final Promise result = Promise.promise(); + + if (connecting.compareAndSet(false, true)) { + + vertx.executeBlocking(r -> { + try { + LOG.debug("trying to start cache manager"); + cacheManager.start(); + LOG.info("started cache manager"); + LOG.debug("trying to get cache"); + setCache(cacheManager.getCache(cacheName)); + if (isStarted()) { + r.complete(getCache()); + } else { + r.fail(new IllegalStateException("cache [" + cacheName + "] is not configured")); + } + } catch (final Throwable t) { + r.fail(t); + } + }, attempt -> { + if (attempt.succeeded()) { + LOG.info("successfully connected to cache"); + result.complete(); + } else { + LOG.debug("failed to connect to cache: {}", attempt.cause().getMessage()); + result.fail(attempt.cause()); + } + connecting.set(false); + }); + } else { + LOG.info("already trying to establish connection to cache"); + result.fail("already trying to establish connection to cache"); + } + return result.future(); + } + + @Override + public Future checkForCacheAvailability() { + + if (isStarted()) { + return Future.succeededFuture(new JsonObject()); + } else { + // try to (re-)establish connection + connectToCache(); + return Future.failedFuture("not connected to cache"); + } + } + +} diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/HotrodCache.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/HotrodCache.java new file mode 100644 index 0000000000..6aebc33824 --- /dev/null +++ b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/HotrodCache.java @@ -0,0 +1,239 @@ +/** + * Copyright (c) 2020, 2021 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.infinispan; + +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.hono.cache.CommonCacheConfig; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.RemoteCacheContainer; +import org.infinispan.client.hotrod.RemoteCacheManager; +import org.infinispan.commons.marshall.ProtoStreamMarshaller; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; + +/** + * A remote cache that connects to a data grid using the Hotrod protocol. + * + * @param The type of keys used by the cache. + * @param The type of values stored in the cache. + */ +public final class HotrodCache extends BasicCache { + + private static final Logger LOG = LoggerFactory.getLogger(HotrodCache.class); + + /** + * Maximum age for a cached connection check result to be used in {@link #checkForCacheAvailability()}. + */ + private static final Duration CACHED_CONNECTION_CHECK_RESULT_MAX_AGE = Duration.ofSeconds(30); + + private final AtomicBoolean connecting = new AtomicBoolean(false); + private final RemoteCacheContainer cacheManager; + private final String cacheName; + + private final K connectionCheckKey; + private final V connectionCheckValue; + + private ConnectionCheckResult lastConnectionCheckResult; + + /** + * Creates a new HotrodCache instance. + * + * @param vertx The vert.x instance to run on. + * @param cacheManager The connection to the remote cache. + * @param cacheName The name of the (remote) cache. + * @param connectionCheckKey The key to use for checking the connection + * to the data grid. + * @param connectionCheckValue The value to use for checking the connection + * to the data grid. + * @throws NullPointerException if any of the parameters are {@code null}. + */ + HotrodCache( + final Vertx vertx, + final RemoteCacheContainer cacheManager, + final String cacheName, + final K connectionCheckKey, + final V connectionCheckValue) { + super(vertx, cacheManager); + this.cacheManager = Objects.requireNonNull(cacheManager); + this.cacheName = Objects.requireNonNull(cacheName); + this.connectionCheckKey = Objects.requireNonNull(connectionCheckKey); + this.connectionCheckValue = Objects.requireNonNull(connectionCheckValue); + } + + /** + * Creates a new remote cache. + * + * @param vertx The vert.x instance to run on. + * @param properties The remote cache configuration. + * @param commonCacheConfig The common cache configuration. + * @return The remote cache. + * @throws NullPointerException if any of the parameters are {@code null}. + */ + public static HotrodCache from( + final Vertx vertx, + final InfinispanRemoteConfigurationProperties properties, + final CommonCacheConfig commonCacheConfig) { + + Objects.requireNonNull(vertx); + Objects.requireNonNull(properties); + Objects.requireNonNull(commonCacheConfig); + + final var configBuilder = properties.getConfigurationBuilder(); + configBuilder.marshaller(new ProtoStreamMarshaller()); + final var configuration = configBuilder.build(); + if (LOG.isInfoEnabled()) { + LOG.info("creating HotrodCache using configuration: {}", configuration); + } + return new HotrodCache<>( + vertx, + new RemoteCacheManager(configuration, false), + commonCacheConfig.getCacheName(), + commonCacheConfig.getCheckKey(), + commonCacheConfig.getCheckValue()); + } + + @Override + protected Future connectToCache() { + + final Promise result = Promise.promise(); + + if (connecting.compareAndSet(false, true)) { + + vertx.executeBlocking(r -> { + try { + if (!cacheManager.isStarted()) { + LOG.debug("trying to start cache manager"); + cacheManager.start(); + LOG.info("started cache manager, now connecting to remote cache"); + } + LOG.debug("trying to connect to remote cache"); + @SuppressWarnings("unchecked") + final var cache = (RemoteCache) cacheManager.getCache(cacheName); + if (cache == null) { + r.fail(new IllegalStateException("remote cache [" + cacheName + "] does not exist")); + } else { + cache.start(); + setCache(cache); + r.complete(cache); + } + } catch (final Exception t) { + r.fail(t); + } + }, attempt -> { + if (attempt.succeeded()) { + LOG.info("successfully connected to remote cache"); + result.complete(); + } else { + LOG.debug("failed to connect to remote cache: {}", attempt.cause().getMessage()); + result.fail(attempt.cause()); + } + connecting.set(false); + }); + } else { + LOG.info("already trying to establish connection to data grid"); + result.fail("already trying to establish connection to data grid"); + } + return result.future(); + } + + @Override + protected boolean isStarted() { + return cacheManager.isStarted() && getCache() != null; + } + + @Override + protected void postCacheAccess(final AsyncResult cacheOperationResult) { + lastConnectionCheckResult = new ConnectionCheckResult(cacheOperationResult.cause()); + } + + /** + * Checks if the cache is connected. + * + * @return A future that is completed with information about a successful check's result. + * Otherwise, the future will be failed with a {@link org.eclipse.hono.client.ServerErrorException}. + */ + @Override + public Future checkForCacheAvailability() { + + if (isStarted()) { + final ConnectionCheckResult lastResult = lastConnectionCheckResult; + if (lastResult != null && !lastResult.isOlderThan(CACHED_CONNECTION_CHECK_RESULT_MAX_AGE)) { + return lastResult.asFuture(); + } else { + final Promise result = Promise.promise(); + put(connectionCheckKey, connectionCheckValue) + .onComplete(r -> { + if (r.succeeded()) { + result.complete(new JsonObject()); + } else { + LOG.debug("failed to put test value to cache", r.cause()); + result.fail(r.cause()); + } + }); + return result.future(); + } + } else { + // try to (re-)establish connection + connectToCache(); + return Future.failedFuture("not connected to data grid"); + } + } + + /** + * Keeps the result of a connection check. + */ + private static class ConnectionCheckResult { + private final Instant creationTimestamp = Instant.now(); + private final Throwable errorResult; + + /** + * Creates a new ConnectionCheckResult. + * + * @param errorResult The error in case the check failed; use {@code null} if the check succeeded. + */ + ConnectionCheckResult(final Throwable errorResult) { + this.errorResult = errorResult; + } + + /** + * Checks if the result is older than the given time span, determined from the current point in time. + * + * @param timespan The time span. + * @return {@code true} if the result is older. + */ + public boolean isOlderThan(final Duration timespan) { + return creationTimestamp.isBefore(Instant.now().minus(timespan)); + } + + /** + * Gets a future indicating the connection check outcome. + * + * @return A succeeded future if the check succeeded, otherwise a failed future. + */ + public Future asFuture() { + return errorResult != null ? Future.failedFuture(errorResult) : Future.succeededFuture(new JsonObject()); + } + } + +} diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationOptions.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationOptions.java new file mode 100644 index 0000000000..e47cdcf43d --- /dev/null +++ b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationOptions.java @@ -0,0 +1,202 @@ +/** + * 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.infinispan; + +import java.util.Map; +import java.util.Optional; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.ConfigMapping.NamingStrategy; +import io.smallrye.config.WithDefault; + +/** + * Options for configuring a Hotrod connection to a remote cache. + * + */ +@ConfigMapping(prefix = "hono.cache.infinispan", namingStrategy = NamingStrategy.VERBATIM) +public interface InfinispanRemoteConfigurationOptions { + + /** + * Gets the connection pool options. + * + * @return The options. + */ + Map connectionPool(); + + /** + * Gets the default executor factory options. + * + * @return The options. + */ + Map defaultExecutorFactory(); + + /** + * Gets the SASL properties. + * + * @return The properties. + */ + Map saslProperties(); + + /** + * Gets the cluster options. + * + * @return The options. + */ + Map cluster(); + + /** + * Gets the list of remote servers as a string of the form host1[:port][;host2[:port]]. + * + * @return The servers. + */ + Optional serverList(); + + /** + * Gets the auth server name. + * + * @return The server name. + */ + Optional authServerName(); + + /** + * Gets the user name to use for authentication. + * + * @return The user name. + */ + Optional authUsername(); + + /** + * Gets the password to use for authentication. + * + * @return The password. + */ + Optional authPassword(); + + /** + * Gets the auth realm (for DIGEST-MD5 authentication). + * + * @return The realm. + */ + Optional authRealm(); + + /** + * Gets the SASL mechanism to use for authentication. + * + * @return The mechanism. + */ + Optional saslMechanism(); + + /** + * Gets the socket timeout. + * + * @return The timeout. + */ + @WithDefault("60000") + int socketTimeout(); + + /** + * Gets the connect timeout. + * + * @return The timeout. + */ + @WithDefault("60000") + int connectTimeout(); + + /** + * Gets the path of the trust store. + * + * @return The path. + */ + Optional trustStorePath(); + + /** + * Gets the trust store file name. + * + * @return The file name. + */ + Optional trustStoreFileName(); + + /** + * Gets the type of the trust store (JKS, JCEKS, PCKS12 or PEM). + * + * @return The type. + */ + Optional trustStoreType(); + + /** + * Gets the password of the trust store. + * + * @return The password. + */ + Optional trustStorePassword(); + + /** + * Gets the file name of a keystore to use when using client certificate authentication. + * + * @return The file name. + */ + Optional keyStoreFileName(); + + /** + * Gets the keystore type. + * + * @return The type. + */ + Optional keyStoreType(); + + /** + * Gets the keystore password. + * + * @return The password. + */ + Optional keyStorePassword(); + + /** + * Gets the key alias. + * + * @return The alias. + */ + Optional keyAlias(); + + /** + * Gets the certificate password in the keystore. + * + * @return The password. + */ + Optional keyStoreCertificatePassword(); + + /** + * Checks whether TLS is enabled. + * + * @return {@code true} if TLS is enabled. + */ + @WithDefault("false") + boolean useSsl(); + + /** + * Gets the list of ciphers, separated with spaces and in order of preference, that are used during the TLS + * handshake. + * + * @return The ciphers. + */ + Optional sslCiphers(); + + /** + * Gets the TLS protocol to use (e.g. TLSv1.2). + * + * @return The protocol. + */ + Optional sslProtocol(); +} diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationProperties.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationProperties.java new file mode 100644 index 0000000000..0251615446 --- /dev/null +++ b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationProperties.java @@ -0,0 +1,189 @@ +/** + * Copyright (c) 2020, 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.infinispan; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; +import org.infinispan.client.hotrod.impl.ConfigurationProperties; + +import com.google.common.base.CaseFormat; +import com.google.common.base.MoreObjects; + + +/** + * Configuration properties for a Hotrod connection to a remote cache. + * + */ +public class InfinispanRemoteConfigurationProperties extends ConfigurationProperties { + + private static final String DEFAULT_EXECUTOR_FACTORY_PREFIX = "infinispan.client.hotrod.default_executor_factory"; + private static final String CONNECTION_POOL_PREFIX = "infinispan.client.hotrod.connection_pool"; + + /** + * Creates properties using default values. + */ + public InfinispanRemoteConfigurationProperties() { + super(); + } + + /** + * Creates properties from existing options. + * + * @param options The options to copy. + */ + @SuppressWarnings("deprecation") + public InfinispanRemoteConfigurationProperties(final InfinispanRemoteConfigurationOptions options) { + super(); + + options.authPassword().ifPresent(this::setAuthPassword); + options.authRealm().ifPresent(this::setAuthRealm); + options.authServerName().ifPresent(this::setAuthServerName); + options.authUsername().ifPresent(this::setAuthUsername); + + setCluster(options.cluster()); + setConnectionPool(options.connectionPool()); + setConnectTimeout(options.connectTimeout()); + + setDefaultExecutorFactory(options.defaultExecutorFactory()); + + options.keyAlias().ifPresent(this::setKeyAlias); + options.keyStoreCertificatePassword().ifPresent(this::setKeyStoreCertificatePassword); + options.keyStoreFileName().ifPresent(this::setKeyStoreFileName); + options.keyStorePassword().ifPresent(this::setKeyStorePassword); + options.keyStoreType().ifPresent(this::setKeyStoreType); + + options.saslMechanism().ifPresent(this::setSaslMechanism); + setSaslProperties(options.saslProperties()); + + options.serverList().ifPresent(this::setServerList); + setSocketTimeout(options.socketTimeout()); + + options.trustStoreFileName().ifPresent(this::setTrustStoreFileName); + options.trustStorePassword().ifPresent(this::setTrustStorePassword); + options.trustStorePath().ifPresent(this::setTrustStorePath); + options.trustStoreType().ifPresent(this::setTrustStoreType); + + setUseSSL(options.useSsl()); + + options.sslCiphers().ifPresent(this::setSSLCiphers); + options.sslProtocol().ifPresent(this::setSSLProtocol); + } + + /** + * Gets a builder for this configuration. + * + * @return A builder that can be used to create a cache. + */ + public final ConfigurationBuilder getConfigurationBuilder() { + return new ConfigurationBuilder().withProperties(getProperties()); + } + + /** + * Sets the properties related to the connection pool. + *

+ * Property keys may be in camel case or snake case. + * + * @param poolProperties The properties. + */ + public final void setConnectionPool(final Map poolProperties) { + setProperties(poolProperties, CONNECTION_POOL_PREFIX, this::toSnakeCase); + } + + /** + * Sets the properties related to the default executor factory. + *

+ * Property keys may be in camel case or snake case. + * + * @param factoryProperties The properties. + */ + public final void setDefaultExecutorFactory(final Map factoryProperties) { + setProperties(factoryProperties, DEFAULT_EXECUTOR_FACTORY_PREFIX, this::toSnakeCase); + } + + /** + * Sets the properties related to the SASL based authentication. + * + * @param saslProperties The properties. + */ + public final void setSaslProperties(final Map saslProperties) { + setProperties(saslProperties, SASL_PROPERTIES_PREFIX, null); + } + + /** + * Sets the properties related to cluster configuration. + * + * @param clusterProperties The properties. + */ + public final void setCluster(final Map clusterProperties) { + setProperties(clusterProperties, CLUSTER_PROPERTIES_PREFIX, null); + } + + private String toSnakeCase(final String key) { + return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, key); + } + + private void setProperties( + final Map properties, + final String keyPrefix, + final Function keyConverter) { + + properties.forEach((k, v) -> { + final String keySuffix = Optional.ofNullable(keyConverter).map(f -> f.apply(k)).orElse(k); + final String key = String.format("%s.%s", keyPrefix, keySuffix); + getProperties().setProperty(key, v); + }); + } + + // ------- Getters/setters missing in the parent ConfigurationProperties class ------- + + /** + * Gets the keystore certificate password. + * + * @return The password. + */ + public String getKeyStoreCertificatePassword() { + return getProperties().getProperty(KEY_STORE_CERTIFICATE_PASSWORD); + } + + /** + * Gets the SSL ciphers. + * + * @return The ciphers. + */ + public String getSSLCiphers() { + return getProperties().getProperty(SSL_CIPHERS); + } + + /** + * Sets the SSL ciphers. + * + * @param ciphers The ciphers. + */ + public void setSSLCiphers(final String ciphers) { + getProperties().put(SSL_CIPHERS, ciphers); + } + + @Override + public String toString() { + return MoreObjects + .toStringHelper(this) + .add("serverList", this.getServerList()) + .add("authUsername", this.getAuthUsername()) + .toString(); + } +} diff --git a/caches/cache-redis/pom.xml b/caches/cache-redis/pom.xml new file mode 100644 index 0000000000..7a6a5b04ca --- /dev/null +++ b/caches/cache-redis/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + org.eclipse.hono + hono-caches-parent + 2.5.0-SNAPSHOT + + hono-cache-redis + + Hono Client Device Connection Cache (Redis) + Redis implementation of Hono's Client Device Connection cache + + + + org.eclipse.hono + hono-cache-common + + + + redis.clients + jedis + 4.3.1 + + + + + + + diff --git a/caches/cache-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/RedisCache.java b/caches/cache-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/RedisCache.java new file mode 100644 index 0000000000..74809a3d0c --- /dev/null +++ b/caches/cache-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/RedisCache.java @@ -0,0 +1,144 @@ +/** + * 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; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.eclipse.hono.cache.Cache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +/** + * TODO. + * @param TODO + * @param TODO + */ +public class RedisCache implements Cache { + + private static final Logger LOG = LoggerFactory.getLogger(RedisCache.class); + + private JedisPool pool; + + /** + * TODO. + */ + public RedisCache() { + LOG.info("Initializing REDIS cache!"); + try { + pool = new JedisPool("redis", 6379); + try (Jedis jedis = pool.getResource()) { + final var response = jedis.ping(); + LOG.info("Got {} from redis server", response); + } + } catch (Exception e) { + LOG.error("something went wrong", e); + } + } + + @Override + public Future checkForCacheAvailability() { + LOG.info("REDIS: checking for cache availability"); + try (Jedis jedis = pool.getResource()) { + final var response = jedis.ping(); + LOG.info("Got {} from redis server", response); + return Future.succeededFuture(new JsonObject()); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + @Override + public Future put(final K key, final V value) { + LOG.info("REDIS: put {}={}", key, value); + try (Jedis jedis = pool.getResource()) { + jedis.set(key.toString(), value.toString()); + } + return Future.succeededFuture(); + } + + @Override + public Future put(final K key, final V value, final long lifespan, final TimeUnit lifespanUnit) { + LOG.info("REDIS: put {}={} ({} {})", key, value, lifespan, lifespanUnit); + try (Jedis jedis = pool.getResource()) { + jedis.psetex(key.toString(), lifespanUnit.toMillis(lifespan), value.toString()); + return Future.succeededFuture(); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + @Override + public Future putAll(final Map data) { + LOG.info("REDIS: putAll ({})", data.size()); + try (Jedis jedis = pool.getResource()) { + for (K k : data.keySet()) { + jedis.set(k.toString(), data.get(k).toString()); + } + return Future.succeededFuture(); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + @Override + public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { + LOG.info("REDIS: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); + try (Jedis jedis = pool.getResource()) { + for (K k : data.keySet()) { + jedis.psetex(k.toString(), lifespanUnit.toMillis(lifespan), data.get(k).toString()); + } + return Future.succeededFuture(); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + @Override + public Future get(final K key) { + LOG.info("REDIS: get {}", key); + try (Jedis jedis = pool.getResource()) { + return Future.succeededFuture((V) jedis.get(key.toString())); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + @Override + public Future remove(final K key, final V value) { + LOG.info("REDIS: remove {}={}", key, value); + try (Jedis jedis = pool.getResource()) { + jedis.del(key.toString()); + return Future.succeededFuture(true); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + @Override + public Future> getAll(final Set keys) { + LOG.warn("getAll() ({}) called but that has not been implemented!!!", keys.size()); + try (Jedis jedis = pool.getResource()) { + return Future.succeededFuture(null); + } catch (Exception e) { + return Future.failedFuture(e); + } + } +} diff --git a/caches/pom.xml b/caches/pom.xml new file mode 100644 index 0000000000..2b25b16dd9 --- /dev/null +++ b/caches/pom.xml @@ -0,0 +1,95 @@ + + + + 4.0.0 + + org.eclipse.hono + hono-bom + 2.5.0-SNAPSHOT + ../bom + + + hono-caches-parent + pom + + Hono Caches + Cache implementations used for Hono's client device connection (TODO: improve) + + + cache-common + cache-infinispan + cache-redis + + + + + org.eclipse.hono + hono-legal + + + + + org.mockito + mockito-core + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + io.vertx + vertx-junit5 + test + + + com.google.truth + truth + test + + + org.eclipse.hono + core-test-utils + test + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.jboss.jandex + jandex-maven-plugin + + + org.jacoco + jacoco-maven-plugin + + + + diff --git a/client-device-connection-infinispan/pom.xml b/client-device-connection-infinispan/pom.xml index 153c95c002..8af198fd9f 100644 --- a/client-device-connection-infinispan/pom.xml +++ b/client-device-connection-infinispan/pom.xml @@ -64,6 +64,11 @@ com.google.guava guava + + redis.clients + jedis + 4.3.1 + io.quarkus quarkus-core diff --git a/client-device-connection/pom.xml b/client-device-connection/pom.xml new file mode 100644 index 0000000000..ce4177a6a8 --- /dev/null +++ b/client-device-connection/pom.xml @@ -0,0 +1,63 @@ + + + + 4.0.0 + + org.eclipse.hono + hono-bom + 2.5.0-SNAPSHOT + ../bom + + hono-client-device-connection + + Hono Client Device Connection Cache + Common classes for Hono's Client Device Connection cache + + + + org.eclipse.hono + hono-client-common + + + org.eclipse.hono + hono-cache-common + + + + + + diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/AdapterInstanceStatusProvider.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/AdapterInstanceStatusProvider.java new file mode 100644 index 0000000000..7a55f28d6e --- /dev/null +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/AdapterInstanceStatusProvider.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * 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; + +import java.util.Collection; +import java.util.Set; + +import org.eclipse.hono.util.AdapterInstanceStatus; + +import io.vertx.core.Future; + +/** + * Provides the status of an adapter instance. + */ +public interface AdapterInstanceStatusProvider { + + /** + * Gets the status of the adapter identified by the given identifier. + * + * @param adapterInstanceId The identifier of the adapter instance. + * @return The status of the adapter instance. + * @throws NullPointerException if adapterInstanceId is {@code null}. + */ + AdapterInstanceStatus getStatus(String adapterInstanceId); + + /** + * Gets the identifiers of the adapter instances from the given collection + * that have the {@link AdapterInstanceStatus#DEAD} status. + *

+ * Compared to {@link #getStatus(String)}, extra measures may be taken here + * to resolve the status of adapter instances otherwise classified as + * {@link AdapterInstanceStatus#SUSPECTED_DEAD} before completing the result future. + * + * @param adapterInstanceIds The identifiers of the adapter instances. + * @return A succeeded future containing the identifiers of the dead adapter instances or a failed future + * indicating the reason why the operation failed. + * @throws NullPointerException if adapterInstanceIds is {@code null}. + */ + Future> getDeadAdapterInstances(Collection adapterInstanceIds); +} diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/CacheBasedDeviceConnectionInfo.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/CacheBasedDeviceConnectionInfo.java new file mode 100644 index 0000000000..9787bb5f70 --- /dev/null +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/CacheBasedDeviceConnectionInfo.java @@ -0,0 +1,694 @@ +/** + * Copyright (c) 2020, 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; + +import java.net.HttpURLConnection; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.hono.cache.Cache; +import org.eclipse.hono.client.ClientErrorException; +import org.eclipse.hono.client.ServerErrorException; +import org.eclipse.hono.client.util.ServiceClient; +import org.eclipse.hono.tracing.TracingHelper; +import org.eclipse.hono.util.AdapterInstanceStatus; +import org.eclipse.hono.util.DeviceConnectionConstants; +import org.eclipse.hono.util.Lifecycle; +import org.eclipse.hono.util.MessageHelper; +import org.eclipse.hono.util.RequestResponseApiConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.healthchecks.HealthCheckHandler; +import io.vertx.ext.healthchecks.Status; + + +/** + * A client for accessing device connection information in a data grid. + */ +public final class CacheBasedDeviceConnectionInfo implements DeviceConnectionInfo, ServiceClient, Lifecycle { + + /** + * Lifespan for last-known-gateway cache entries. + */ + static final Duration LAST_KNOWN_GATEWAY_CACHE_ENTRY_LIFESPAN = Duration.ofDays(28); + + /** + * For viaGateways parameter value lower or equal to this value, the {@link #getCommandHandlingAdapterInstances(String, String, Set, Span)} + * method will use an optimized approach, potentially saving additional cache requests. + */ + static final int VIA_GATEWAYS_OPTIMIZATION_THRESHOLD = 3; + + private static final Logger LOG = LoggerFactory.getLogger(CacheBasedDeviceConnectionInfo.class); + + /** + * Key prefix for cache entries having gateway id values, concerning lastKnownGatewayForDevice + * operations. + */ + private static final String KEY_PREFIX_GATEWAY_ENTRIES_VALUE = "gw"; + /** + * Key prefix for cache entries having protocol adapter instance id values, concerning + * commandHandlingAdapterInstance operations. + */ + private static final String KEY_PREFIX_ADAPTER_INSTANCE_VALUES = "ai"; + private static final String KEY_SEPARATOR = "@@"; + + final Cache cache; + final Tracer tracer; + final AdapterInstanceStatusProvider adapterInstanceStatusProvider; + + private DeviceToAdapterMappingErrorListener deviceToAdapterMappingErrorListener; + + /** + * Creates a client for accessing device connection information. + * + * @param cache The remote cache that contains the data. + * @param tracer The tracer instance. + * @throws NullPointerException if cache or tracer is {@code null}. + */ + public CacheBasedDeviceConnectionInfo(final Cache cache, final Tracer tracer) { + this(cache, tracer, null); + } + + /** + * Creates a client for accessing device connection information. + * + * @param cache The remote cache that contains the data. + * @param tracer The tracer instance. + * @param adapterInstanceStatusProvider The provider of the adapter instance status (may be {@code null}). + * @throws NullPointerException if cache or tracer is {@code null}. + */ + public CacheBasedDeviceConnectionInfo(final Cache cache, final Tracer tracer, + final AdapterInstanceStatusProvider adapterInstanceStatusProvider) { + this.cache = Objects.requireNonNull(cache); + this.tracer = Objects.requireNonNull(tracer); + this.adapterInstanceStatusProvider = Optional.ofNullable(adapterInstanceStatusProvider) + .orElseGet(UnknownStatusProvider::new); + } + + /** + * {@inheritDoc} + * + * If this method is invoked from a vert.x Context, then the returned future will be completed on that context. + */ + @Override + public Future setLastKnownGatewayForDevice( + final String tenantId, + final String deviceId, + final String gatewayId, + final Span span) { + + Objects.requireNonNull(tenantId); + Objects.requireNonNull(deviceId); + Objects.requireNonNull(gatewayId); + Objects.requireNonNull(span); + + final long lifespanMillis = LAST_KNOWN_GATEWAY_CACHE_ENTRY_LIFESPAN.toMillis(); + return cache.put(getGatewayEntryKey(tenantId, deviceId), gatewayId, lifespanMillis, TimeUnit.MILLISECONDS) + .onSuccess(ok -> LOG.debug("set last known gateway [tenant: {}, device-id: {}, gateway: {}]", + tenantId, deviceId, gatewayId)) + .otherwise(t -> { + LOG.debug("failed to set last known gateway [tenant: {}, device-id: {}, gateway: {}]", + tenantId, deviceId, gatewayId, t); + TracingHelper.logError(span, "failed to set last known gateway", t); + throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); + }); + } + + @Override + public Future setLastKnownGatewayForDevice( + final String tenantId, + final Map deviceIdToGatewayIdMap, + final Span span) { + + Objects.requireNonNull(tenantId); + Objects.requireNonNull(deviceIdToGatewayIdMap); + Objects.requireNonNull(span); + + if (deviceIdToGatewayIdMap.isEmpty()) { + return Future.succeededFuture(); + } + + final long lifespanMillis = LAST_KNOWN_GATEWAY_CACHE_ENTRY_LIFESPAN.toMillis(); + final Map mapToBePut = deviceIdToGatewayIdMap.entrySet().stream() + .collect(Collectors.toMap(entry -> getGatewayEntryKey(tenantId, entry.getKey()), Map.Entry::getValue)); + return cache.putAll(mapToBePut, lifespanMillis, TimeUnit.MILLISECONDS) + .onSuccess(ok -> LOG.debug("set {} last known gateway entries [tenant: {}]", + deviceIdToGatewayIdMap.size(), tenantId)) + .otherwise(t -> { + LOG.debug("failed to set {} last known gateway entries [tenant: {}]", + deviceIdToGatewayIdMap.size(), tenantId, t); + TracingHelper.logError(span, "failed to set last known gateway entries", t); + throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); + }); + } + + @Override + public Future getLastKnownGatewayForDevice( + final String tenantId, + final String deviceId, + final Span span) { + + Objects.requireNonNull(tenantId); + Objects.requireNonNull(deviceId); + Objects.requireNonNull(span); + + return cache.get(getGatewayEntryKey(tenantId, deviceId)) + .otherwise(t -> { + LOG.debug("failed to find last known gateway for device [tenant: {}, device-id: {}]", + tenantId, deviceId, t); + TracingHelper.logError(span, "failed to find last known gateway for device", t); + throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); + }) + .compose(gatewayId -> { + if (gatewayId == null) { + LOG.debug("could not find last known gateway for device [tenant: {}, device-id: {}]", tenantId, + deviceId); + return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND)); + } else { + LOG.debug("found last known gateway for device [tenant: {}, device-id: {}]: {}", tenantId, + deviceId, gatewayId); + return Future.succeededFuture(getLastKnownGatewayResultJson(gatewayId)); + } + }); + } + + @Override + public Future setCommandHandlingAdapterInstance( + final String tenantId, + final String deviceId, + final String adapterInstanceId, + final Duration lifespan, + final Span span) { + + Objects.requireNonNull(tenantId); + Objects.requireNonNull(deviceId); + Objects.requireNonNull(adapterInstanceId); + Objects.requireNonNull(span); + + // sanity check, preventing an ArithmeticException in lifespan.toMillis() + final long lifespanMillis = lifespan == null || lifespan.isNegative() + || lifespan.getSeconds() > (Long.MAX_VALUE / 1000L) ? -1 : lifespan.toMillis(); + return cache.put(getAdapterInstanceEntryKey(tenantId, deviceId), adapterInstanceId, lifespanMillis, TimeUnit.MILLISECONDS) + .onSuccess(ok -> LOG.debug( + "set command handling adapter instance [tenant: {}, device-id: {}, adapter-instance: {}, lifespan: {}ms]", + tenantId, deviceId, adapterInstanceId, lifespanMillis)) + .otherwise(t -> { + LOG.debug("failed to set command handling adapter instance [tenant: {}, device-id: {}, adapter-instance: {}, lifespan: {}ms]", + tenantId, deviceId, adapterInstanceId, lifespanMillis, t); + TracingHelper.logError(span, "failed to set command handling adapter instance cache entry", t); + throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); + }); + } + + @Override + public Future removeCommandHandlingAdapterInstance( + final String tenantId, + final String deviceId, + final String adapterInstanceId, + final Span span) { + + Objects.requireNonNull(tenantId); + Objects.requireNonNull(deviceId); + Objects.requireNonNull(adapterInstanceId); + Objects.requireNonNull(span); + + return cache + .remove(getAdapterInstanceEntryKey(tenantId, deviceId), adapterInstanceId) + .otherwise(t -> { + LOG.debug("failed to remove the cache entry for the command handling adapter instance [tenant: {}, device-id: {}, adapter-instance: {}]", + tenantId, deviceId, adapterInstanceId, t); + TracingHelper.logError(span, "failed to remove cache entry for the command handling adapter instance", t); + throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); + }) + .compose(removed -> { + if (!removed) { + LOG.debug("command handling adapter instance was not removed, key not mapped or value didn't match [tenant: {}, device-id: {}, adapter-instance: {}]", + tenantId, deviceId, adapterInstanceId); + return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_PRECON_FAILED)); + } else { + LOG.debug("removed command handling adapter instance [tenant: {}, device-id: {}, adapter-instance: {}]", + tenantId, deviceId, adapterInstanceId); + return Future.succeededFuture(); + } + }); + + } + + @Override + public Future getCommandHandlingAdapterInstances( + final String tenantId, + final String deviceId, + final Set viaGateways, + final Span span) { + + Objects.requireNonNull(tenantId); + Objects.requireNonNull(deviceId); + Objects.requireNonNull(viaGateways); + Objects.requireNonNull(span); + + final Future resultFuture; + if (viaGateways.isEmpty()) { + // get the command handling adapter instance for the device (no gateway involved) + resultFuture = cache.get(getAdapterInstanceEntryKey(tenantId, deviceId)) + .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) + .compose(adapterInstanceId -> checkAdapterInstanceId(adapterInstanceId, tenantId, deviceId, span)) + .compose(adapterInstanceId -> { + if (adapterInstanceId == null) { + LOG.debug("no command handling adapter instances found [tenant: {}, device-id: {}]", + tenantId, deviceId); + span.log("no command handling adapter instances found for device (no via-gateways given)"); + return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND)); + } else { + LOG.debug("found command handling adapter instance '{}' [tenant: {}, device-id: {}]", + adapterInstanceId, tenantId, deviceId); + span.log("returning command handling adapter instance for device itself"); + setTagsForSingleResult(span, adapterInstanceId); + return Future.succeededFuture(getAdapterInstancesResultJson(deviceId, adapterInstanceId)); + } + }); + } else if (viaGateways.size() <= VIA_GATEWAYS_OPTIMIZATION_THRESHOLD) { + resultFuture = getInstancesQueryingAllGatewaysFirst(tenantId, deviceId, viaGateways, span); + } else { + // number of viaGateways is more than threshold value - reduce cache accesses by not checking *all* viaGateways, + // instead trying the last known gateway first + resultFuture = getInstancesGettingLastKnownGatewayFirst(tenantId, deviceId, viaGateways, span); + } + return resultFuture; + } + + @Override + public void setDeviceToAdapterMappingErrorListener( + final DeviceToAdapterMappingErrorListener obsoleteMappingListener) { + this.deviceToAdapterMappingErrorListener = obsoleteMappingListener; + } + + private Future getInstancesQueryingAllGatewaysFirst( + final String tenantId, + final String deviceId, + final Set viaGateways, + final Span span) { + + LOG.debug("using optimized query, retrieving {} via-gateways in one go", viaGateways.size()); + // get the command handling adapter instances for the device and *all* via-gateways in one call first + // (this saves the extra lastKnownGateway check if only one adapter instance is returned) + return cache.getAll(getAdapterInstanceEntryKeys(tenantId, deviceId, viaGateways)) + .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) + .compose(getAllMap -> checkAdapterInstanceIds(tenantId, convertAdapterInstanceEntryKeys(getAllMap), span)) + .compose(deviceToInstanceMap -> { + final Future resultFuture; + if (deviceToInstanceMap.isEmpty()) { + LOG.debug("no command handling adapter instances found [tenant: {}, device-id: {}]", + tenantId, deviceId); + span.log("no command handling adapter instances found for device or given via-gateways (" + + String.join(", ", viaGateways) + ")"); + resultFuture = Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND)); + } else if (deviceToInstanceMap.containsKey(deviceId)) { + // there is a adapter instance set for the device itself - that gets precedence + resultFuture = getAdapterInstanceFoundForDeviceItselfResult(tenantId, deviceId, + deviceToInstanceMap.get(deviceId), span); + } else if (deviceToInstanceMap.size() > 1) { + // multiple gateways found - check last known gateway + resultFuture = cache.get(getGatewayEntryKey(tenantId, deviceId)) + .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) + .compose(lastKnownGateway -> { + if (lastKnownGateway == null) { + // no last known gateway found - just return all found mapping entries + LOG.debug("returning {} command handling adapter instances for device gateways (no last known gateway found) [tenant: {}, device-id: {}]", + deviceToInstanceMap.size(), tenantId, deviceId); + span.log("no last known gateway found, returning all matching adapter instances"); + return Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); + } else if (!viaGateways.contains(lastKnownGateway)) { + // found gateway is not valid anymore - just return all found mapping entries + LOG.debug("returning {} command handling adapter instances for device gateways (last known gateway not valid anymore) [tenant: {}, device-id: {}, lastKnownGateway: {}]", + deviceToInstanceMap.size(), tenantId, deviceId, lastKnownGateway); + span.log(String.format( + "last known gateway '%s' is not valid anymore, returning all matching adapter instances", + lastKnownGateway)); + return Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); + } else if (!deviceToInstanceMap.containsKey(lastKnownGateway)) { + // found gateway has no command handling instance assigned - just return all found mapping entries + LOG.debug("returning {} command handling adapter instances for device gateways (last known gateway not in that list) [tenant: {}, device-id: {}, lastKnownGateway: {}]", + deviceToInstanceMap.size(), tenantId, deviceId, lastKnownGateway); + span.log(String.format( + "last known gateway '%s' has no adapter instance assigned, returning all matching adapter instances", + lastKnownGateway)); + return Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); + } else { + LOG.debug("returning command handling adapter instance '{}' for last known gateway [tenant: {}, device-id: {}, lastKnownGateway: {}]", + deviceToInstanceMap.get(lastKnownGateway), tenantId, deviceId, lastKnownGateway); + span.log("returning adapter instance for last known gateway '" + lastKnownGateway + "'"); + setTagsForSingleResultWithGateway(span, deviceToInstanceMap.get(lastKnownGateway), lastKnownGateway); + return Future.succeededFuture(getAdapterInstancesResultJson(lastKnownGateway, + deviceToInstanceMap.get(lastKnownGateway))); + } + }); + } else { + // one command handling instance found + final Map.Entry foundEntry = deviceToInstanceMap.entrySet().iterator().next(); + LOG.debug("returning command handling adapter instance '{}' associated with gateway {} [tenant: {}, device-id: {}]", + foundEntry.getValue(), foundEntry.getKey(), tenantId, deviceId); + span.log("returning adapter instance associated with gateway '" + foundEntry.getKey() + "'"); + setTagsForSingleResultWithGateway(span, foundEntry.getValue(), foundEntry.getKey()); + resultFuture = Future.succeededFuture(getAdapterInstancesResultJson(foundEntry.getKey(), + foundEntry.getValue())); + } + return resultFuture; + }); + } + + private void setTagsForSingleResultWithGateway(final Span span, final String adapterInstanceId, final String gatewayId) { + setTagsForSingleResult(span, adapterInstanceId); + span.setTag(MessageHelper.APP_PROPERTY_GATEWAY_ID, gatewayId); + } + + private void setTagsForSingleResult(final Span span, final String adapterInstanceId) { + TracingHelper.TAG_ADAPTER_INSTANCE_ID.set(span, adapterInstanceId); + } + + private Future getInstancesGettingLastKnownGatewayFirst( + final String tenantId, + final String deviceId, + final Set viaGateways, + final Span span) { + + return cache.get(getGatewayEntryKey(tenantId, deviceId)) + .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) + .compose(lastKnownGateway -> { + if (lastKnownGateway == null) { + LOG.trace("no last known gateway found [tenant: {}, device-id: {}]", tenantId, deviceId); + span.log("no last known gateway found"); + } else if (!viaGateways.contains(lastKnownGateway)) { + LOG.trace("found gateway is not valid for the device anymore [tenant: {}, device-id: {}]", tenantId, deviceId); + span.log("found gateway '" + lastKnownGateway + "' is not valid anymore"); + } + if (lastKnownGateway != null && viaGateways.contains(lastKnownGateway)) { + // fetch command handling instances for lastKnownGateway and device + return cache.getAll(getAdapterInstanceEntryKeys(tenantId, deviceId, lastKnownGateway)) + .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) + .compose(getAllMap -> checkAdapterInstanceIds(tenantId, convertAdapterInstanceEntryKeys(getAllMap), span)) + .compose(deviceToInstanceMap -> { + if (deviceToInstanceMap.isEmpty()) { + // no adapter instances found for last-known-gateway and device - check all via gateways + span.log(String.format( + "last known gateway '%s' has no adapter instance assigned, returning all matching adapter instances", + lastKnownGateway)); + return getAdapterInstancesWithoutLastKnownGatewayCheck(tenantId, deviceId, viaGateways, span); + } else if (deviceToInstanceMap.containsKey(deviceId)) { + // there is a adapter instance set for the device itself - that gets precedence + return getAdapterInstanceFoundForDeviceItselfResult(tenantId, deviceId, deviceToInstanceMap.get(deviceId), span); + } else { + // adapter instance found for last known gateway + LOG.debug("returning command handling adapter instance '{}' for last known gateway [tenant: {}, device-id: {}, lastKnownGateway: {}]", + deviceToInstanceMap.get(lastKnownGateway), tenantId, deviceId, lastKnownGateway); + span.log("returning adapter instance for last known gateway '" + lastKnownGateway + "'"); + setTagsForSingleResultWithGateway(span, deviceToInstanceMap.get(lastKnownGateway), lastKnownGateway); + return Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); + } + }); + } else { + // last-known-gateway not found or invalid - look for all adapter instances for device and viaGateways + return getAdapterInstancesWithoutLastKnownGatewayCheck(tenantId, deviceId, viaGateways, span); + } + }); + } + + private Future getAdapterInstancesWithoutLastKnownGatewayCheck( + final String tenantId, + final String deviceId, + final Set viaGateways, + final Span span) { + + return cache.getAll(getAdapterInstanceEntryKeys(tenantId, deviceId, viaGateways)) + .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) + .compose(getAllMap -> checkAdapterInstanceIds(tenantId, convertAdapterInstanceEntryKeys(getAllMap), span)) + .compose(deviceToInstanceMap -> { + final Future resultFuture; + if (deviceToInstanceMap.isEmpty()) { + LOG.debug("no command handling adapter instances found [tenant: {}, device-id: {}]", + tenantId, deviceId); + span.log("no command handling adapter instances found for device or given via-gateways (" + + String.join(", ", viaGateways) + ")"); + resultFuture = Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND)); + } else if (deviceToInstanceMap.containsKey(deviceId)) { + // there is a command handling instance set for the device itself - that gets precedence + resultFuture = getAdapterInstanceFoundForDeviceItselfResult(tenantId, deviceId, deviceToInstanceMap.get(deviceId), span); + } else { + LOG.debug("returning {} command handling adapter instance(s) (no last known gateway found) [tenant: {}, device-id: {}]", + deviceToInstanceMap.size(), tenantId, deviceId); + resultFuture = Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); + } + return resultFuture; + }); + } + + private Future getAdapterInstanceFoundForDeviceItselfResult( + final String tenantId, + final String deviceId, + final String adapterInstanceId, + final Span span) { + + LOG.debug("returning command handling adapter instance '{}' for device itself [tenant: {}, device-id: {}]", + adapterInstanceId, tenantId, deviceId); + span.log("returning command handling adapter instance for device itself"); + setTagsForSingleResult(span, adapterInstanceId); + return Future.succeededFuture(getAdapterInstancesResultJson(deviceId, adapterInstanceId)); + } + + private Future failedToGetEntriesWhenGettingInstances( + final String tenantId, + final String deviceId, + final Throwable t, + final Span span) { + + LOG.debug("failed to get cache entries when trying to get command handling adapter instances [tenant: {}, device-id: {}]", + tenantId, deviceId, t); + TracingHelper.logError(span, "failed to get cache entries when trying to get command handling adapter instances", t); + return Future.failedFuture(new ServerErrorException(HttpURLConnection.HTTP_INTERNAL_ERROR, t)); + } + + static String getGatewayEntryKey(final String tenantId, final String deviceId) { + return KEY_PREFIX_GATEWAY_ENTRIES_VALUE + KEY_SEPARATOR + tenantId + KEY_SEPARATOR + deviceId; + } + + static String getAdapterInstanceEntryKey(final String tenantId, final String deviceId) { + return KEY_PREFIX_ADAPTER_INSTANCE_VALUES + KEY_SEPARATOR + tenantId + KEY_SEPARATOR + deviceId; + } + + static Set getAdapterInstanceEntryKeys( + final String tenantId, + final String deviceIdA, + final String deviceIdB) { + + final Set keys = new HashSet<>(2); + keys.add(getAdapterInstanceEntryKey(tenantId, deviceIdA)); + keys.add(getAdapterInstanceEntryKey(tenantId, deviceIdB)); + return keys; + } + + /** + * Puts the entries from the given map, having {@link #getAdapterInstanceEntryKey(String, String)} keys, into + * a new map with just the extracted device ids as keys. + * + * @param map Map to get the entries from. + * @return New map with keys containing just the device id. + */ + private static Map convertAdapterInstanceEntryKeys(final Map map) { + return map.entrySet().stream() + .collect(Collectors.toMap(entry -> getDeviceIdFromAdapterInstanceEntryKey( + entry.getKey()), Map.Entry::getValue)); + } + + private static String getDeviceIdFromAdapterInstanceEntryKey(final String key) { + final int pos = key.lastIndexOf(KEY_SEPARATOR); + return key.substring(pos + KEY_SEPARATOR.length()); + } + + static Set getAdapterInstanceEntryKeys( + final String tenantId, + final String deviceIdA, + final Set additionalDeviceIds) { + + final Set keys = new HashSet<>(additionalDeviceIds.size() + 1); + keys.add(getAdapterInstanceEntryKey(tenantId, deviceIdA)); + additionalDeviceIds.forEach(id -> keys.add(getAdapterInstanceEntryKey(tenantId, id))); + return keys; + } + + private static JsonObject getLastKnownGatewayResultJson(final String gatewayId) { + return new JsonObject().put(DeviceConnectionConstants.FIELD_GATEWAY_ID, gatewayId); + } + + private static JsonObject getAdapterInstancesResultJson(final Map deviceToAdapterInstanceMap) { + final JsonObject jsonObject = new JsonObject(); + final JsonArray adapterInstancesArray = new JsonArray(new ArrayList<>(deviceToAdapterInstanceMap.size())); + for (final Map.Entry resultEntry : deviceToAdapterInstanceMap.entrySet()) { + final JsonObject entryJson = new JsonObject(); + entryJson.put(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID, resultEntry.getKey()); + entryJson.put(DeviceConnectionConstants.FIELD_ADAPTER_INSTANCE_ID, resultEntry.getValue()); + adapterInstancesArray.add(entryJson); + } + jsonObject.put(DeviceConnectionConstants.FIELD_ADAPTER_INSTANCES, adapterInstancesArray); + return jsonObject; + } + + private static JsonObject getAdapterInstancesResultJson(final String deviceId, final String adapterInstanceId) { + return getAdapterInstancesResultJson(Map.of(deviceId, adapterInstanceId)); + } + + private Future> checkAdapterInstanceIds(final String tenantId, + final Map deviceToInstanceIdMap, final Span span) { + + @SuppressWarnings("rawtypes") + final List mappingFutures = new ArrayList<>(); + final Map deviceToInstanceIdMapResult = new HashMap<>(); + deviceToInstanceIdMap.entrySet().forEach(entry -> { + final Future mappingFuture = checkAdapterInstanceId(entry.getValue(), tenantId, entry.getKey(), span) + .map(adapterId -> { + if (adapterId != null) { + deviceToInstanceIdMapResult.put(entry.getKey(), entry.getValue()); + } + return adapterId; + }); + mappingFutures.add(mappingFuture); + }); + + return CompositeFuture.join(mappingFutures).map(deviceToInstanceIdMapResult); + } + + private Future checkAdapterInstanceId( + final String adapterInstanceId, + final String tenantId, + final String deviceId, + final Span span) { + + if (adapterInstanceId != null) { + final AdapterInstanceStatus status = adapterInstanceStatusProvider.getStatus(adapterInstanceId); + if (status == AdapterInstanceStatus.DEAD) { + LOG.debug( + "ignoring found adapter instance id, belongs to already terminated container [tenant: {}, device-id: {}, adapter-instance-id: {}]", + tenantId, deviceId, adapterInstanceId); + span.log("ignoring found adapter instance id [" + adapterInstanceId + + "], belongs to already terminated container"); + final Future listenerResult; + if (deviceToAdapterMappingErrorListener != null) { + listenerResult = deviceToAdapterMappingErrorListener.onObsoleteEntryFound(tenantId, deviceId, + adapterInstanceId, + span); + } else { + listenerResult = Future.succeededFuture(); + } + return listenerResult + .onSuccess(v -> { + if (deviceToAdapterMappingErrorListener != null) { + LOG.debug( + "called listener for obsolete adapter instance id '{}' [tenant: {}, device-id: {}]", + adapterInstanceId, tenantId, deviceId); + } + }) + .onFailure(thr -> { + LOG.debug( + "error calling listener for obsolete adapter instance id '{}' [tenant: {}, device-id: {}]", + adapterInstanceId, tenantId, deviceId, thr); + }) + .compose(s -> { + return cache.remove(getAdapterInstanceEntryKey(tenantId, deviceId), adapterInstanceId) + .onSuccess(removed -> { + if (removed) { + LOG.debug( + "removed entry with obsolete adapter instance id '{}' [tenant: {}, device-id: {}]", + adapterInstanceId, tenantId, deviceId); + } + }) + .onFailure(thr -> { + LOG.debug( + "error removing entry with obsolete adapter instance id '{}' [tenant: {}, device-id: {}]", + adapterInstanceId, tenantId, deviceId, thr); + }); + }) + .recover(thr -> { + // errors treated as not found adapter instance + return Future.succeededFuture(); + }) + .mapEmpty(); + } else if (status == AdapterInstanceStatus.SUSPECTED_DEAD) { + LOG.debug( + "ignoring found adapter instance id, belongs to container with state 'SUSPECTED_DEAD' [tenant: {}, device-id: {}, adapter-instance-id: {}]", + tenantId, deviceId, adapterInstanceId); + span.log("ignoring found adapter instance id [" + adapterInstanceId + + "], belongs to container with state 'SUSPECTED_DEAD'"); + return Future.succeededFuture(); + } + } + return Future.succeededFuture(adapterInstanceId); + } + + /** + * {@inheritDoc} + *

+ * Registers a check which verifies if the underlying cache is available. + * The check times out (and fails) after 1000ms. + */ + @Override + public void registerReadinessChecks(final HealthCheckHandler readinessHandler) { + readinessHandler.register("remote-cache-connection", 1000, this::checkForCacheAvailability); + } + + private void checkForCacheAvailability(final Promise status) { + + cache.checkForCacheAvailability() + .map(Status::OK) + .otherwise(t -> Status.KO()) + .onComplete(ar -> status.tryComplete(ar.result())); + } + + @Override + public void registerLivenessChecks(final HealthCheckHandler livenessHandler) { + // nothing to register + } + + @Override + public Future start() { + if (cache instanceof Lifecycle) { + return ((Lifecycle) cache).start(); + } else { + return Future.succeededFuture(); + } + } + + @Override + public Future stop() { + if (cache instanceof Lifecycle) { + return ((Lifecycle) cache).stop(); + } else { + return Future.succeededFuture(); + } + } +} diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceConnectionInfo.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceConnectionInfo.java new file mode 100644 index 0000000000..2bffcf8812 --- /dev/null +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceConnectionInfo.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2020, 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; + +import java.time.Duration; +import java.util.Map; +import java.util.Set; + +import io.opentracing.Span; +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; + +/** + * A repository for keeping connection information about devices. + * + */ +public interface DeviceConnectionInfo { + + /** + * Sets the gateway that last acted on behalf of a device. + *

+ * If a device connects directly instead of through a gateway, the device identifier itself is to be used as value + * for the gatewayId parameter. + * + * @param tenant The tenant that the device belongs to. + * @param deviceId The device identifier. + * @param gatewayId The gateway identifier. This may be the same as the device identifier if the device is + * (currently) not connected via a gateway but directly to a protocol adapter. + * @param span The active OpenTracing span for this operation. It is not to be closed in this method! + * An implementation should log (error) events on this span and it may set tags and use this span as the + * parent for any spans created in this method. + * @return A future indicating the outcome of the operation. + *

+ * The future will be succeeded if the device connection information has been updated. + * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. + * @throws NullPointerException if any of the parameters is {@code null}. + */ + Future setLastKnownGatewayForDevice(String tenant, String deviceId, String gatewayId, Span span); + + /** + * For a given list of device and gateway combinations, sets the gateway as the last gateway that acted on behalf + * of the device. + *

+ * If a device connects directly instead of through a gateway, the device identifier itself is to be used as + * gateway value. + * + * @param tenant The tenant that the device belongs to. + * @param deviceIdToGatewayIdMap The map containing device identifiers and associated gateway identifiers. The + * gateway identifier may be the same as the device identifier if the device is + * (currently) not connected via a gateway but directly to a protocol adapter. + * @param span The active OpenTracing span for this operation. It is not to be closed in this method! + * An implementation should log (error) events on this span and it may set tags and use this span as the + * parent for any spans created in this method. + * @return A future indicating the outcome of the operation. + *

+ * The future will be succeeded if the device connection information has been updated. + * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. + * The outcome is indeterminate if any of the entries cannot be processed by an implementation. + * In such a case, client code should assume that none of the entries have been updated. + * @throws NullPointerException if any of the parameters is {@code null}. + */ + Future setLastKnownGatewayForDevice(String tenant, Map deviceIdToGatewayIdMap, Span span); + + /** + * Gets the gateway that last acted on behalf of a device. + *

+ * If no last known gateway has been set for the given device yet, a failed future with status + * 404 is returned. + * + * @param tenant The tenant that the device belongs to. + * @param deviceId The device identifier. + * @param span The active OpenTracing span for this operation. It is not to be closed in this method! + * An implementation should log (error) events on this span and it may set tags and use this span as the + * parent for any spans created in this method. + * @return A future indicating the outcome of the operation. + *

+ * The future will be succeeded with a JSON object containing the currently mapped gateway ID + * in the gateway-id property, if device connection information has been found for + * the given device. + * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. + * @throws NullPointerException if any of the parameters is {@code null}. + */ + Future getLastKnownGatewayForDevice(String tenant, String deviceId, Span span); + + /** + * Sets the protocol adapter instance that handles commands for the given device or gateway. + * + * @param tenantId The tenant id. + * @param deviceId The device id. + * @param adapterInstanceId The protocol adapter instance id. + * @param lifespan The lifespan of the mapping entry. Using a negative duration or {@code null} here is + * interpreted as an unlimited lifespan. + * @param span The active OpenTracing span for this operation. It is not to be closed in this method! + * An implementation should log (error) events on this span and it may set tags and use this span as the + * parent for any spans created in this method. + * @return A future indicating the outcome of the operation. + *

+ * The future will be succeeded if the device connection information has been updated. + * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. + * @throws NullPointerException if any of the parameters except lifespan is {@code null}. + */ + Future setCommandHandlingAdapterInstance(String tenantId, String deviceId, String adapterInstanceId, + Duration lifespan, Span span); + + /** + * Removes the mapping information that associates the given device with the given protocol adapter instance + * that handles commands for the given device. The mapping entry is only deleted if its value + * contains the given protocol adapter instance id. + * + * @param tenantId The tenant id. + * @param deviceId The device id. + * @param adapterInstanceId The protocol adapter instance id that the entry to be removed has to contain. + * @param span The active OpenTracing span for this operation. It is not to be closed in this method! + * An implementation should log (error) events on this span and it may set tags and use this span as the + * parent for any spans created in this method. + * @return A future indicating the outcome of the operation. + *

+ * The future will be succeeded if the entry was successfully removed. + * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. + * @throws NullPointerException if any of the parameters except context is {@code null}. + */ + Future removeCommandHandlingAdapterInstance(String tenantId, String deviceId, String adapterInstanceId, Span span); + + /** + * Gets information about the adapter instances that can handle a command for the given device. + *

+ * In order to determine the adapter instances the following rules are applied (in the given order): + *

    + *
  1. If an adapter instance is associated with the given device, this adapter instance is returned as the single + * returned list entry.
  2. + *
  3. Otherwise, if there is an adapter instance registered for the last known gateway associated with the given + * device, this adapter instance is returned as the single returned list entry. The last known gateway has to be + * contained in the given list of gateways for this case.
  4. + *
  5. Otherwise, all adapter instances associated with any of the given gateway identifiers are returned.
  6. + *
+ * That means that for a device communicating via a gateway, the result is reduced to a single element list + * if an adapter instance for the device itself or its last known gateway is found. The adapter instance registered + * for the device itself is given precedence in order to ensure that a gateway having subscribed to commands for + * that particular device is chosen over a gateway that has subscribed to commands for all devices of a tenant. + *

+ * The resulting JSON structure looks like this, possibly containing multiple array entries: + * { + * "adapter-instances": [ + * { + * "adapter-instance-id": "adapter-1", + * "device-id": "4711" + * } + * ] + * } + * + *

+ * If no adapter instances are found, the returned future is failed. + * + * @param tenantId The tenant id. + * @param deviceId The device id. + * @param viaGateways The set of gateways that may act on behalf of the given device. + * @param span The active OpenTracing span for this operation. It is not to be closed in this method! + * An implementation should log (error) events on this span and it may set tags and use this span as the + * parent for any spans created in this method. + * @return A future indicating the outcome of the operation. + *

+ * If instances were found, the future will be succeeded with a JSON object containing one or more mappings + * from device id to adapter instance id. + * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. + * @throws NullPointerException if any of the parameters is {@code null}. + */ + Future getCommandHandlingAdapterInstances(String tenantId, String deviceId, Set viaGateways, Span span); + + /** + * Sets listener to be notified when an incorrect device to adapter mapping is identified. + * + * @param deviceToAdapterMappingErrorListener The listener. + */ + void setDeviceToAdapterMappingErrorListener(DeviceToAdapterMappingErrorListener deviceToAdapterMappingErrorListener); +} diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceToAdapterMappingErrorListener.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceToAdapterMappingErrorListener.java new file mode 100644 index 0000000000..7a4fec04f2 --- /dev/null +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceToAdapterMappingErrorListener.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 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; + +import io.opentracing.Span; +import io.vertx.core.Future; + +/** + * Listener notified when an incorrect device to adapter mapping is found. + */ +public interface DeviceToAdapterMappingErrorListener { + + /** + * Called when an obsolete device to adapter mapping is found. + * + * @param tenantId The tenant identifier. + * @param deviceId The device identifier. + * @param adapterInstanceId The adapter instance identifier. + * @param span The active OpenTracing span for this operation. It is not to be closed in this method! An + * implementation should log (error) events on this span and it may set tags and use this span as the + * parent for any spans created in this method. + * @return A future indicating the outcome of the operation. The future will be succeeded if the listener is + * notified successfully. + */ + Future onObsoleteEntryFound(String tenantId, String deviceId, String adapterInstanceId, Span span); +} diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/UnknownStatusProvider.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/UnknownStatusProvider.java new file mode 100644 index 0000000000..abe7644875 --- /dev/null +++ b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/UnknownStatusProvider.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 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; + +import java.util.Collection; +import java.util.Set; + +import org.eclipse.hono.util.AdapterInstanceStatus; + +import io.vertx.core.Future; + + +/** + * Status provider that always returns the {@link AdapterInstanceStatus#UNKNOWN} status. + * + */ +final class UnknownStatusProvider implements AdapterInstanceStatusProvider { + + @Override + public AdapterInstanceStatus getStatus(final String adapterInstanceId) { + return AdapterInstanceStatus.UNKNOWN; + } + + @Override + public Future> getDeadAdapterInstances( + final Collection adapterInstanceIds) { + return Future.succeededFuture(Set.of()); + } +} diff --git a/pom.xml b/pom.xml index 12f3181be7..d88ff4516f 100644 --- a/pom.xml +++ b/pom.xml @@ -225,7 +225,7 @@ bom core cli - client-device-connection-infinispan + clients demo-certs examples @@ -235,6 +235,8 @@ site test-utils tests + client-device-connection + caches diff --git a/services/command-router/pom.xml b/services/command-router/pom.xml index 17a58b6f5a..1b50625bd1 100644 --- a/services/command-router/pom.xml +++ b/services/command-router/pom.xml @@ -24,10 +24,25 @@ A Quarkus based implementation of Hono's Command Router API that is using Infinispan for storing data. + + + org.eclipse.hono + hono-client-device-connection + + + org.eclipse.hono + hono-cache-infinispan + + + org.eclipse.hono + hono-cache-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..e1e10b3588 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.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..9bfc674e5e 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.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 a8dde95c7c..eb389772d3 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 @@ -64,7 +64,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.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 474264c78f..9dbc6753eb 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 @@ -23,20 +23,18 @@ import javax.enterprise.inject.Produces; import javax.inject.Singleton; +import org.eclipse.hono.cache.Cache; +import org.eclipse.hono.cache.CommonCacheConfig; +import org.eclipse.hono.cache.CommonCacheOptions; import org.eclipse.hono.commandrouter.AdapterInstanceStatusService; 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.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.util.Strings; +import org.eclipse.hono.deviceconnection.CacheBasedDeviceConnectionInfo; +import org.eclipse.hono.deviceconnection.DeviceConnectionInfo; +import org.eclipse.hono.deviceconnection.infinispan.InfinispanRemoteConfigurationOptions; +import org.eclipse.hono.deviceconnection.infinispan.InfinispanRemoteConfigurationProperties; +import org.eclipse.hono.deviceconnection.redis.RedisCache; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; import org.infinispan.configuration.parsing.ParserRegistry; @@ -65,14 +63,14 @@ 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, @@ -81,6 +79,7 @@ BasicCache cache( 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<>( @@ -94,6 +93,9 @@ BasicCache cache( infinispanCacheConfig, commonCacheConfig); } + */ + LOG.info("Creating a new REDIS cache."); + return new RedisCache(); } 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 ef6460b4fa..0ca0b2cdce 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.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 b138292ee8..bcb669597c 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.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..c3568cdc92 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.DeviceConnectionInfo; import org.eclipse.hono.test.VertxMockSupport; import org.eclipse.hono.util.CommandConstants; import org.eclipse.hono.util.EventConstants; From a20267530701ecfb9c1537e3607ed38533c2f489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=C3=A1n=20Freyr=20Stef=C3=A1nsson?= Date: Sat, 26 Aug 2023 02:55:10 +0000 Subject: [PATCH 02/20] Restructure modules as per PR comments --- bom/pom.xml | 16 +- caches/cache-common/pom.xml | 112 --- caches/cache-infinispan/pom.xml | 96 --- .../infinispan/BasicCache.java | 223 ------ .../infinispan/EmbeddedCache.java | 115 --- .../infinispan/HotrodCache.java | 239 ------ .../InfinispanRemoteConfigurationOptions.java | 202 ----- ...finispanRemoteConfigurationProperties.java | 189 ----- caches/cache-redis/pom.xml | 50 -- caches/pom.xml | 95 --- client-device-connection-infinispan/pom.xml | 56 +- .../client/AdapterInstanceStatusProvider.java | 51 -- .../infinispan/client/BasicCache.java | 1 + .../infinispan/client/Cache.java | 119 --- .../CacheBasedDeviceConnectionInfo.java | 693 ------------------ .../infinispan/client/CommonCacheConfig.java | 86 --- .../infinispan/client/CommonCacheOptions.java | 52 -- .../client/DeviceConnectionInfo.java | 186 ----- .../DeviceToAdapterMappingErrorListener.java | 37 - .../infinispan/client/HotrodCache.java | 1 + .../client/UnknownStatusProvider.java | 41 -- .../client/QuarkusPropertyBindingTest.java | 2 + client-device-connection-redis/pom.xml | 175 +++++ .../redis/client}/RedisCache.java | 4 +- .../native-image.properties | 15 + .../resources-config.json | 37 + .../src/main/resources/application.properties | 11 + .../test/resources/common-cache-options.yaml | 6 + .../src/test/resources/logback-test.xml | 37 + .../test/resources/remote-cache-options.yaml | 34 + client-device-connection/pom.xml | 119 ++- .../AdapterInstanceStatusProvider.java | 2 +- .../hono/deviceconnection/common}/Cache.java | 2 +- .../CacheBasedDeviceConnectionInfo.java | 3 +- .../common}/CommonCacheConfig.java | 2 +- .../common}/CommonCacheOptions.java | 2 +- .../{ => common}/DeviceConnectionInfo.java | 2 +- .../DeviceToAdapterMappingErrorListener.java | 2 +- .../{ => common}/UnknownStatusProvider.java | 2 +- .../CacheBasedDeviceConnectionInfoTest.java | 11 +- pom.xml | 6 +- services/command-router/pom.xml | 10 +- .../AdapterInstanceStatusService.java | 2 +- .../commandrouter/CommandTargetMapper.java | 2 +- .../hono/commandrouter/app/Application.java | 2 +- .../app/DeviceConnectionInfoProducer.java | 16 +- .../impl/CommandRouterServiceImpl.java | 2 +- .../impl/CommandTargetMapperImpl.java | 2 +- .../impl/CommandRouterServiceImplTest.java | 2 +- 49 files changed, 492 insertions(+), 2680 deletions(-) delete mode 100644 caches/cache-common/pom.xml delete mode 100644 caches/cache-infinispan/pom.xml delete mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/BasicCache.java delete mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/EmbeddedCache.java delete mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/HotrodCache.java delete mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationOptions.java delete mode 100644 caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationProperties.java delete mode 100644 caches/cache-redis/pom.xml delete mode 100644 caches/pom.xml delete mode 100644 client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/AdapterInstanceStatusProvider.java delete mode 100644 client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/Cache.java delete mode 100644 client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfo.java delete mode 100644 client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheConfig.java delete mode 100644 client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheOptions.java delete mode 100644 client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceConnectionInfo.java delete mode 100644 client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceToAdapterMappingErrorListener.java delete mode 100644 client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/UnknownStatusProvider.java create mode 100644 client-device-connection-redis/pom.xml rename {caches/cache-redis/src/main/java/org/eclipse/hono/deviceconnection/redis => client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client}/RedisCache.java (97%) create mode 100644 client-device-connection-redis/src/main/resources/META-INF/native-image/org.eclipse.hono/client-device-connection-infinispan/native-image.properties create mode 100644 client-device-connection-redis/src/main/resources/META-INF/native-image/org.eclipse.hono/client-device-connection-infinispan/resources-config.json create mode 100644 client-device-connection-redis/src/main/resources/application.properties create mode 100644 client-device-connection-redis/src/test/resources/common-cache-options.yaml create mode 100644 client-device-connection-redis/src/test/resources/logback-test.xml create mode 100644 client-device-connection-redis/src/test/resources/remote-cache-options.yaml rename client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/{ => common}/AdapterInstanceStatusProvider.java (97%) rename {caches/cache-common/src/main/java/org/eclipse/hono/cache => client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common}/Cache.java (98%) rename client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/{ => common}/CacheBasedDeviceConnectionInfo.java (99%) rename {caches/cache-common/src/main/java/org/eclipse/hono/cache => client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common}/CommonCacheConfig.java (97%) rename {caches/cache-common/src/main/java/org/eclipse/hono/cache => client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common}/CommonCacheOptions.java (96%) rename client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/{ => common}/DeviceConnectionInfo.java (99%) rename client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/{ => common}/DeviceToAdapterMappingErrorListener.java (96%) rename client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/{ => common}/UnknownStatusProvider.java (95%) rename {client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client => client-device-connection/src/test/java/org/eclipse/hono/deviceconnection/common}/CacheBasedDeviceConnectionInfoTest.java (99%) diff --git a/bom/pom.xml b/bom/pom.xml index 0f055100da..851588b971 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -328,22 +328,17 @@ quarkus.vertx.max-event-loop-execute-time=${max.event-loop.execute-time:20000} org.eclipse.hono - hono-cache-common + client-device-connection ${project.version} org.eclipse.hono - hono-cache-infinispan - ${project.version} - - - org.eclipse.hono - hono-cache-redis + client-device-connection-infinispan ${project.version} org.eclipse.hono - hono-client-device-connection + client-device-connection-redis ${project.version} @@ -461,11 +456,6 @@ 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-infinispan - ${project.version} - org.eclipse.hono hono-client-application diff --git a/caches/cache-common/pom.xml b/caches/cache-common/pom.xml deleted file mode 100644 index bc8282b651..0000000000 --- a/caches/cache-common/pom.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - - 4.0.0 - - org.eclipse.hono - hono-caches-parent - 2.5.0-SNAPSHOT - - hono-cache-common - - Hono Cache Common - Classes required for implementing Hono caches - - - - io.vertx - vertx-core - - - org.eclipse.hono - hono-client-common - - - 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 - - - - - - - - - diff --git a/caches/cache-infinispan/pom.xml b/caches/cache-infinispan/pom.xml deleted file mode 100644 index 633a7f7376..0000000000 --- a/caches/cache-infinispan/pom.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - 4.0.0 - - org.eclipse.hono - hono-caches-parent - 2.5.0-SNAPSHOT - - hono-cache-infinispan - - Hono Client Device Connection Cache using Infinispan - Infinispan implementation of Hono's Client Device Connection cache - - - - org.eclipse.hono - hono-cache-common - - - org.infinispan - infinispan-client-hotrod - - - io.netty - netty-transport-native-epoll - - - - - org.infinispan - infinispan-query-dsl - - - org.infinispan - infinispan-core - - - 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 - - - - - - - - diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/BasicCache.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/BasicCache.java deleted file mode 100644 index 47fe0603c7..0000000000 --- a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/BasicCache.java +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Copyright (c) 2020, 2021 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.infinispan; - -import java.net.HttpURLConnection; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Function; - -import org.eclipse.hono.cache.Cache; -import org.eclipse.hono.client.ServerErrorException; -import org.eclipse.hono.util.Futures; -import org.eclipse.hono.util.Lifecycle; -import org.infinispan.commons.api.BasicCacheContainer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.vertx.core.AsyncResult; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; - -/** - * An abstract base class for implementing caches based on an - * Infinispan {@link org.infinispan.commons.api.BasicCache}. - * - * @param The type of the key. - * @param The type of the value. - */ -public abstract class BasicCache implements Cache, Lifecycle { - - private static final Logger LOG = LoggerFactory.getLogger(BasicCache.class); - - protected final Vertx vertx; - private final BasicCacheContainer cacheManager; - private final AtomicBoolean stopCalled = new AtomicBoolean(); - - private org.infinispan.commons.api.BasicCache cache; - - /** - * Creates a new instance. - * - * @param vertx The vert.x instance to run on. - * @param cacheManager The cache manager. - */ - protected BasicCache(final Vertx vertx, final BasicCacheContainer cacheManager) { - this.vertx = Objects.requireNonNull(vertx); - this.cacheManager = Objects.requireNonNull(cacheManager); - } - - /** - * Called to trigger connecting the cache. - * - * @return A future tracking the progress, never returns {@code null}. - */ - protected abstract Future connectToCache(); - - /** - * Checks if the cache manager is started. - * - * @return {@code true} if the cache manager is started, {@code false} otherwise. - */ - protected abstract boolean isStarted(); - - @Override - public Future start() { - LOG.info("starting cache"); - return connectToCache(); - } - - @Override - public Future stop() { - if (!stopCalled.compareAndSet(false, true)) { - return Future.succeededFuture(); - } - LOG.info("stopping cache"); - setCache(null); - final Promise result = Promise.promise(); - vertx.executeBlocking(r -> { - try { - cacheManager.stop(); - r.complete(); - } catch (final Exception t) { - r.fail(t); - } - }, (AsyncResult stopAttempt) -> { - if (stopAttempt.succeeded()) { - LOG.info("connection(s) to cache stopped successfully"); - } else { - LOG.info("error trying to stop connection(s) to cache", stopAttempt.cause()); - } - result.handle(stopAttempt); - }); - return result.future(); - } - - protected void setCache(final org.infinispan.commons.api.BasicCache cache) { - this.cache = cache; - } - - protected org.infinispan.commons.api.BasicCache getCache() { - return this.cache; - } - - /** - * Performs a task with a connected cache. - *

- * The method checks if the cache instance has been set. If that is the case, then the - * supplier will be invoked, providing a non-null cache instance. - *

- * If the cache has not been set (yet) or it has been stopped, the supplier will not be - * called and a failed future will be returned, provided by {@link #noConnectionFailure()}. - * - * @param The type of the return value. - * @param futureSupplier The supplier, providing the operation which should be invoked. - * @return The future, tracking the result of the operation. - */ - protected final Future withCache( - final Function, CompletionStage> futureSupplier) { - - return Optional.ofNullable(cache) - .map(c -> Futures.create(() -> futureSupplier.apply(c))) - .orElseGet(BasicCache::noConnectionFailure) - .onComplete(this::postCacheAccess); - } - - /** - * Performs extra processing on the result of a cache operation returned by {@link #withCache(Function)}. - *

- * Subclasses should override this method if needed. - *

- * This default implementation does nothing. - * - * @param The type of the return value. - * @param cacheOperationResult The result of the cache operation. - */ - protected void postCacheAccess(final AsyncResult cacheOperationResult) { - // nothing done by default - } - - @Override - public Future put(final K key, final V value) { - Objects.requireNonNull(key); - Objects.requireNonNull(value); - - return withCache(aCache -> aCache.putAsync(key, value).thenApply(v -> (Void) null)); - } - - @Override - public Future put(final K key, final V value, final long lifespan, final TimeUnit lifespanUnit) { - Objects.requireNonNull(key); - Objects.requireNonNull(value); - Objects.requireNonNull(lifespanUnit); - - return withCache(aCache -> aCache.putAsync(key, value, lifespan, lifespanUnit).thenApply(v -> (Void) null)); - } - - @Override - public Future putAll(final Map data) { - Objects.requireNonNull(data); - - return withCache(aCache -> aCache.putAllAsync(data)); - } - - @Override - public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { - Objects.requireNonNull(data); - Objects.requireNonNull(lifespanUnit); - - return withCache(aCache -> aCache.putAllAsync(data, lifespan, lifespanUnit)); - } - - @Override - public Future remove(final K key, final V value) { - Objects.requireNonNull(key); - Objects.requireNonNull(value); - - return withCache(aCache -> aCache.removeAsync(key, value)); - } - - @Override - public Future get(final K key) { - Objects.requireNonNull(key); - - return withCache(aCache -> aCache.getAsync(key)); - } - - @Override - public Future> getAll(final Set keys) { - Objects.requireNonNull(keys); - - return withCache(aCache -> aCache.getAllAsync(keys)); - } - - /** - * Returns a failed future, reporting a missing connection to the cache. - * - * @param The value type of the returned future. - * @return A failed future, never returns {@code null}. - */ - protected static Future noConnectionFailure() { - - return Future.failedFuture(new ServerErrorException( - HttpURLConnection.HTTP_UNAVAILABLE, "no connection to data grid")); - } - -} diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/EmbeddedCache.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/EmbeddedCache.java deleted file mode 100644 index efbfeee309..0000000000 --- a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/EmbeddedCache.java +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright (c) 2020, 2021 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.infinispan; - -import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.infinispan.manager.EmbeddedCacheManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; - -/** - * An embedded cache. - * - * @param The type of keys used by the cache. - * @param The type of values stored in the cache. - */ -public class EmbeddedCache extends BasicCache { - - private static final Logger LOG = LoggerFactory.getLogger(EmbeddedCache.class); - - private final AtomicBoolean connecting = new AtomicBoolean(false); - - private final EmbeddedCacheManager cacheManager; - private final String cacheName; - - /** - * Creates a new embedded cache instance. - * - * @param vertx The vert.x instance to run on. - * @param cacheManager The connection to the cache. - * @param cacheName The name of the cache. - */ - public EmbeddedCache( - final Vertx vertx, - final EmbeddedCacheManager cacheManager, - final String cacheName) { - super(vertx, cacheManager); - this.cacheManager = Objects.requireNonNull(cacheManager); - this.cacheName = Objects.requireNonNull(cacheName); - } - - @Override - protected boolean isStarted() { - return cacheManager.isRunning(cacheName) && getCache() != null; - } - - @Override - protected Future connectToCache() { - - final Promise result = Promise.promise(); - - if (connecting.compareAndSet(false, true)) { - - vertx.executeBlocking(r -> { - try { - LOG.debug("trying to start cache manager"); - cacheManager.start(); - LOG.info("started cache manager"); - LOG.debug("trying to get cache"); - setCache(cacheManager.getCache(cacheName)); - if (isStarted()) { - r.complete(getCache()); - } else { - r.fail(new IllegalStateException("cache [" + cacheName + "] is not configured")); - } - } catch (final Throwable t) { - r.fail(t); - } - }, attempt -> { - if (attempt.succeeded()) { - LOG.info("successfully connected to cache"); - result.complete(); - } else { - LOG.debug("failed to connect to cache: {}", attempt.cause().getMessage()); - result.fail(attempt.cause()); - } - connecting.set(false); - }); - } else { - LOG.info("already trying to establish connection to cache"); - result.fail("already trying to establish connection to cache"); - } - return result.future(); - } - - @Override - public Future checkForCacheAvailability() { - - if (isStarted()) { - return Future.succeededFuture(new JsonObject()); - } else { - // try to (re-)establish connection - connectToCache(); - return Future.failedFuture("not connected to cache"); - } - } - -} diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/HotrodCache.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/HotrodCache.java deleted file mode 100644 index 6aebc33824..0000000000 --- a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/HotrodCache.java +++ /dev/null @@ -1,239 +0,0 @@ -/** - * Copyright (c) 2020, 2021 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.infinispan; - -import java.time.Duration; -import java.time.Instant; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.eclipse.hono.cache.CommonCacheConfig; -import org.infinispan.client.hotrod.RemoteCache; -import org.infinispan.client.hotrod.RemoteCacheContainer; -import org.infinispan.client.hotrod.RemoteCacheManager; -import org.infinispan.commons.marshall.ProtoStreamMarshaller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.vertx.core.AsyncResult; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; - -/** - * A remote cache that connects to a data grid using the Hotrod protocol. - * - * @param The type of keys used by the cache. - * @param The type of values stored in the cache. - */ -public final class HotrodCache extends BasicCache { - - private static final Logger LOG = LoggerFactory.getLogger(HotrodCache.class); - - /** - * Maximum age for a cached connection check result to be used in {@link #checkForCacheAvailability()}. - */ - private static final Duration CACHED_CONNECTION_CHECK_RESULT_MAX_AGE = Duration.ofSeconds(30); - - private final AtomicBoolean connecting = new AtomicBoolean(false); - private final RemoteCacheContainer cacheManager; - private final String cacheName; - - private final K connectionCheckKey; - private final V connectionCheckValue; - - private ConnectionCheckResult lastConnectionCheckResult; - - /** - * Creates a new HotrodCache instance. - * - * @param vertx The vert.x instance to run on. - * @param cacheManager The connection to the remote cache. - * @param cacheName The name of the (remote) cache. - * @param connectionCheckKey The key to use for checking the connection - * to the data grid. - * @param connectionCheckValue The value to use for checking the connection - * to the data grid. - * @throws NullPointerException if any of the parameters are {@code null}. - */ - HotrodCache( - final Vertx vertx, - final RemoteCacheContainer cacheManager, - final String cacheName, - final K connectionCheckKey, - final V connectionCheckValue) { - super(vertx, cacheManager); - this.cacheManager = Objects.requireNonNull(cacheManager); - this.cacheName = Objects.requireNonNull(cacheName); - this.connectionCheckKey = Objects.requireNonNull(connectionCheckKey); - this.connectionCheckValue = Objects.requireNonNull(connectionCheckValue); - } - - /** - * Creates a new remote cache. - * - * @param vertx The vert.x instance to run on. - * @param properties The remote cache configuration. - * @param commonCacheConfig The common cache configuration. - * @return The remote cache. - * @throws NullPointerException if any of the parameters are {@code null}. - */ - public static HotrodCache from( - final Vertx vertx, - final InfinispanRemoteConfigurationProperties properties, - final CommonCacheConfig commonCacheConfig) { - - Objects.requireNonNull(vertx); - Objects.requireNonNull(properties); - Objects.requireNonNull(commonCacheConfig); - - final var configBuilder = properties.getConfigurationBuilder(); - configBuilder.marshaller(new ProtoStreamMarshaller()); - final var configuration = configBuilder.build(); - if (LOG.isInfoEnabled()) { - LOG.info("creating HotrodCache using configuration: {}", configuration); - } - return new HotrodCache<>( - vertx, - new RemoteCacheManager(configuration, false), - commonCacheConfig.getCacheName(), - commonCacheConfig.getCheckKey(), - commonCacheConfig.getCheckValue()); - } - - @Override - protected Future connectToCache() { - - final Promise result = Promise.promise(); - - if (connecting.compareAndSet(false, true)) { - - vertx.executeBlocking(r -> { - try { - if (!cacheManager.isStarted()) { - LOG.debug("trying to start cache manager"); - cacheManager.start(); - LOG.info("started cache manager, now connecting to remote cache"); - } - LOG.debug("trying to connect to remote cache"); - @SuppressWarnings("unchecked") - final var cache = (RemoteCache) cacheManager.getCache(cacheName); - if (cache == null) { - r.fail(new IllegalStateException("remote cache [" + cacheName + "] does not exist")); - } else { - cache.start(); - setCache(cache); - r.complete(cache); - } - } catch (final Exception t) { - r.fail(t); - } - }, attempt -> { - if (attempt.succeeded()) { - LOG.info("successfully connected to remote cache"); - result.complete(); - } else { - LOG.debug("failed to connect to remote cache: {}", attempt.cause().getMessage()); - result.fail(attempt.cause()); - } - connecting.set(false); - }); - } else { - LOG.info("already trying to establish connection to data grid"); - result.fail("already trying to establish connection to data grid"); - } - return result.future(); - } - - @Override - protected boolean isStarted() { - return cacheManager.isStarted() && getCache() != null; - } - - @Override - protected void postCacheAccess(final AsyncResult cacheOperationResult) { - lastConnectionCheckResult = new ConnectionCheckResult(cacheOperationResult.cause()); - } - - /** - * Checks if the cache is connected. - * - * @return A future that is completed with information about a successful check's result. - * Otherwise, the future will be failed with a {@link org.eclipse.hono.client.ServerErrorException}. - */ - @Override - public Future checkForCacheAvailability() { - - if (isStarted()) { - final ConnectionCheckResult lastResult = lastConnectionCheckResult; - if (lastResult != null && !lastResult.isOlderThan(CACHED_CONNECTION_CHECK_RESULT_MAX_AGE)) { - return lastResult.asFuture(); - } else { - final Promise result = Promise.promise(); - put(connectionCheckKey, connectionCheckValue) - .onComplete(r -> { - if (r.succeeded()) { - result.complete(new JsonObject()); - } else { - LOG.debug("failed to put test value to cache", r.cause()); - result.fail(r.cause()); - } - }); - return result.future(); - } - } else { - // try to (re-)establish connection - connectToCache(); - return Future.failedFuture("not connected to data grid"); - } - } - - /** - * Keeps the result of a connection check. - */ - private static class ConnectionCheckResult { - private final Instant creationTimestamp = Instant.now(); - private final Throwable errorResult; - - /** - * Creates a new ConnectionCheckResult. - * - * @param errorResult The error in case the check failed; use {@code null} if the check succeeded. - */ - ConnectionCheckResult(final Throwable errorResult) { - this.errorResult = errorResult; - } - - /** - * Checks if the result is older than the given time span, determined from the current point in time. - * - * @param timespan The time span. - * @return {@code true} if the result is older. - */ - public boolean isOlderThan(final Duration timespan) { - return creationTimestamp.isBefore(Instant.now().minus(timespan)); - } - - /** - * Gets a future indicating the connection check outcome. - * - * @return A succeeded future if the check succeeded, otherwise a failed future. - */ - public Future asFuture() { - return errorResult != null ? Future.failedFuture(errorResult) : Future.succeededFuture(new JsonObject()); - } - } - -} diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationOptions.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationOptions.java deleted file mode 100644 index e47cdcf43d..0000000000 --- a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationOptions.java +++ /dev/null @@ -1,202 +0,0 @@ -/** - * 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.infinispan; - -import java.util.Map; -import java.util.Optional; - -import io.smallrye.config.ConfigMapping; -import io.smallrye.config.ConfigMapping.NamingStrategy; -import io.smallrye.config.WithDefault; - -/** - * Options for configuring a Hotrod connection to a remote cache. - * - */ -@ConfigMapping(prefix = "hono.cache.infinispan", namingStrategy = NamingStrategy.VERBATIM) -public interface InfinispanRemoteConfigurationOptions { - - /** - * Gets the connection pool options. - * - * @return The options. - */ - Map connectionPool(); - - /** - * Gets the default executor factory options. - * - * @return The options. - */ - Map defaultExecutorFactory(); - - /** - * Gets the SASL properties. - * - * @return The properties. - */ - Map saslProperties(); - - /** - * Gets the cluster options. - * - * @return The options. - */ - Map cluster(); - - /** - * Gets the list of remote servers as a string of the form host1[:port][;host2[:port]]. - * - * @return The servers. - */ - Optional serverList(); - - /** - * Gets the auth server name. - * - * @return The server name. - */ - Optional authServerName(); - - /** - * Gets the user name to use for authentication. - * - * @return The user name. - */ - Optional authUsername(); - - /** - * Gets the password to use for authentication. - * - * @return The password. - */ - Optional authPassword(); - - /** - * Gets the auth realm (for DIGEST-MD5 authentication). - * - * @return The realm. - */ - Optional authRealm(); - - /** - * Gets the SASL mechanism to use for authentication. - * - * @return The mechanism. - */ - Optional saslMechanism(); - - /** - * Gets the socket timeout. - * - * @return The timeout. - */ - @WithDefault("60000") - int socketTimeout(); - - /** - * Gets the connect timeout. - * - * @return The timeout. - */ - @WithDefault("60000") - int connectTimeout(); - - /** - * Gets the path of the trust store. - * - * @return The path. - */ - Optional trustStorePath(); - - /** - * Gets the trust store file name. - * - * @return The file name. - */ - Optional trustStoreFileName(); - - /** - * Gets the type of the trust store (JKS, JCEKS, PCKS12 or PEM). - * - * @return The type. - */ - Optional trustStoreType(); - - /** - * Gets the password of the trust store. - * - * @return The password. - */ - Optional trustStorePassword(); - - /** - * Gets the file name of a keystore to use when using client certificate authentication. - * - * @return The file name. - */ - Optional keyStoreFileName(); - - /** - * Gets the keystore type. - * - * @return The type. - */ - Optional keyStoreType(); - - /** - * Gets the keystore password. - * - * @return The password. - */ - Optional keyStorePassword(); - - /** - * Gets the key alias. - * - * @return The alias. - */ - Optional keyAlias(); - - /** - * Gets the certificate password in the keystore. - * - * @return The password. - */ - Optional keyStoreCertificatePassword(); - - /** - * Checks whether TLS is enabled. - * - * @return {@code true} if TLS is enabled. - */ - @WithDefault("false") - boolean useSsl(); - - /** - * Gets the list of ciphers, separated with spaces and in order of preference, that are used during the TLS - * handshake. - * - * @return The ciphers. - */ - Optional sslCiphers(); - - /** - * Gets the TLS protocol to use (e.g. TLSv1.2). - * - * @return The protocol. - */ - Optional sslProtocol(); -} diff --git a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationProperties.java b/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationProperties.java deleted file mode 100644 index 0251615446..0000000000 --- a/caches/cache-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/InfinispanRemoteConfigurationProperties.java +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Copyright (c) 2020, 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.infinispan; - -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; - -import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; -import org.infinispan.client.hotrod.impl.ConfigurationProperties; - -import com.google.common.base.CaseFormat; -import com.google.common.base.MoreObjects; - - -/** - * Configuration properties for a Hotrod connection to a remote cache. - * - */ -public class InfinispanRemoteConfigurationProperties extends ConfigurationProperties { - - private static final String DEFAULT_EXECUTOR_FACTORY_PREFIX = "infinispan.client.hotrod.default_executor_factory"; - private static final String CONNECTION_POOL_PREFIX = "infinispan.client.hotrod.connection_pool"; - - /** - * Creates properties using default values. - */ - public InfinispanRemoteConfigurationProperties() { - super(); - } - - /** - * Creates properties from existing options. - * - * @param options The options to copy. - */ - @SuppressWarnings("deprecation") - public InfinispanRemoteConfigurationProperties(final InfinispanRemoteConfigurationOptions options) { - super(); - - options.authPassword().ifPresent(this::setAuthPassword); - options.authRealm().ifPresent(this::setAuthRealm); - options.authServerName().ifPresent(this::setAuthServerName); - options.authUsername().ifPresent(this::setAuthUsername); - - setCluster(options.cluster()); - setConnectionPool(options.connectionPool()); - setConnectTimeout(options.connectTimeout()); - - setDefaultExecutorFactory(options.defaultExecutorFactory()); - - options.keyAlias().ifPresent(this::setKeyAlias); - options.keyStoreCertificatePassword().ifPresent(this::setKeyStoreCertificatePassword); - options.keyStoreFileName().ifPresent(this::setKeyStoreFileName); - options.keyStorePassword().ifPresent(this::setKeyStorePassword); - options.keyStoreType().ifPresent(this::setKeyStoreType); - - options.saslMechanism().ifPresent(this::setSaslMechanism); - setSaslProperties(options.saslProperties()); - - options.serverList().ifPresent(this::setServerList); - setSocketTimeout(options.socketTimeout()); - - options.trustStoreFileName().ifPresent(this::setTrustStoreFileName); - options.trustStorePassword().ifPresent(this::setTrustStorePassword); - options.trustStorePath().ifPresent(this::setTrustStorePath); - options.trustStoreType().ifPresent(this::setTrustStoreType); - - setUseSSL(options.useSsl()); - - options.sslCiphers().ifPresent(this::setSSLCiphers); - options.sslProtocol().ifPresent(this::setSSLProtocol); - } - - /** - * Gets a builder for this configuration. - * - * @return A builder that can be used to create a cache. - */ - public final ConfigurationBuilder getConfigurationBuilder() { - return new ConfigurationBuilder().withProperties(getProperties()); - } - - /** - * Sets the properties related to the connection pool. - *

- * Property keys may be in camel case or snake case. - * - * @param poolProperties The properties. - */ - public final void setConnectionPool(final Map poolProperties) { - setProperties(poolProperties, CONNECTION_POOL_PREFIX, this::toSnakeCase); - } - - /** - * Sets the properties related to the default executor factory. - *

- * Property keys may be in camel case or snake case. - * - * @param factoryProperties The properties. - */ - public final void setDefaultExecutorFactory(final Map factoryProperties) { - setProperties(factoryProperties, DEFAULT_EXECUTOR_FACTORY_PREFIX, this::toSnakeCase); - } - - /** - * Sets the properties related to the SASL based authentication. - * - * @param saslProperties The properties. - */ - public final void setSaslProperties(final Map saslProperties) { - setProperties(saslProperties, SASL_PROPERTIES_PREFIX, null); - } - - /** - * Sets the properties related to cluster configuration. - * - * @param clusterProperties The properties. - */ - public final void setCluster(final Map clusterProperties) { - setProperties(clusterProperties, CLUSTER_PROPERTIES_PREFIX, null); - } - - private String toSnakeCase(final String key) { - return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, key); - } - - private void setProperties( - final Map properties, - final String keyPrefix, - final Function keyConverter) { - - properties.forEach((k, v) -> { - final String keySuffix = Optional.ofNullable(keyConverter).map(f -> f.apply(k)).orElse(k); - final String key = String.format("%s.%s", keyPrefix, keySuffix); - getProperties().setProperty(key, v); - }); - } - - // ------- Getters/setters missing in the parent ConfigurationProperties class ------- - - /** - * Gets the keystore certificate password. - * - * @return The password. - */ - public String getKeyStoreCertificatePassword() { - return getProperties().getProperty(KEY_STORE_CERTIFICATE_PASSWORD); - } - - /** - * Gets the SSL ciphers. - * - * @return The ciphers. - */ - public String getSSLCiphers() { - return getProperties().getProperty(SSL_CIPHERS); - } - - /** - * Sets the SSL ciphers. - * - * @param ciphers The ciphers. - */ - public void setSSLCiphers(final String ciphers) { - getProperties().put(SSL_CIPHERS, ciphers); - } - - @Override - public String toString() { - return MoreObjects - .toStringHelper(this) - .add("serverList", this.getServerList()) - .add("authUsername", this.getAuthUsername()) - .toString(); - } -} diff --git a/caches/cache-redis/pom.xml b/caches/cache-redis/pom.xml deleted file mode 100644 index 7a6a5b04ca..0000000000 --- a/caches/cache-redis/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - 4.0.0 - - org.eclipse.hono - hono-caches-parent - 2.5.0-SNAPSHOT - - hono-cache-redis - - Hono Client Device Connection Cache (Redis) - Redis implementation of Hono's Client Device Connection cache - - - - org.eclipse.hono - hono-cache-common - - - - redis.clients - jedis - 4.3.1 - - - - - - - diff --git a/caches/pom.xml b/caches/pom.xml deleted file mode 100644 index 2b25b16dd9..0000000000 --- a/caches/pom.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - 4.0.0 - - org.eclipse.hono - hono-bom - 2.5.0-SNAPSHOT - ../bom - - - hono-caches-parent - pom - - Hono Caches - Cache implementations used for Hono's client device connection (TODO: improve) - - - cache-common - cache-infinispan - cache-redis - - - - - org.eclipse.hono - hono-legal - - - - - org.mockito - mockito-core - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.junit.jupiter - junit-jupiter-params - test - - - io.vertx - vertx-junit5 - test - - - com.google.truth - truth - test - - - org.eclipse.hono - core-test-utils - test - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - org.apache.maven.plugins - maven-dependency-plugin - - - org.jboss.jandex - jandex-maven-plugin - - - org.jacoco - jacoco-maven-plugin - - - - diff --git a/client-device-connection-infinispan/pom.xml b/client-device-connection-infinispan/pom.xml index 8af198fd9f..3abe259fe5 100644 --- a/client-device-connection-infinispan/pom.xml +++ b/client-device-connection-infinispan/pom.xml @@ -20,6 +20,7 @@ 2.5.0-SNAPSHOT ../bom + client-device-connection-infinispan Hotrod Device Connection client @@ -28,12 +29,9 @@ org.eclipse.hono - hono-legal - - - org.slf4j - slf4j-api + client-device-connection + org.infinispan infinispan-client-hotrod @@ -52,23 +50,8 @@ org.infinispan infinispan-core - - org.eclipse.hono - hono-client-common - - - io.vertx - vertx-web - - - com.google.guava - guava - - - redis.clients - jedis - 4.3.1 - + + io.quarkus quarkus-core @@ -105,6 +88,35 @@ + + + org.junit.jupiter diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/AdapterInstanceStatusProvider.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/AdapterInstanceStatusProvider.java deleted file mode 100644 index 2c98c8a2c0..0000000000 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/AdapterInstanceStatusProvider.java +++ /dev/null @@ -1,51 +0,0 @@ -/******************************************************************************* - * 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.infinispan.client; - -import java.util.Collection; -import java.util.Set; - -import org.eclipse.hono.util.AdapterInstanceStatus; - -import io.vertx.core.Future; - -/** - * Provides the status of an adapter instance. - */ -public interface AdapterInstanceStatusProvider { - - /** - * Gets the status of the adapter identified by the given identifier. - * - * @param adapterInstanceId The identifier of the adapter instance. - * @return The status of the adapter instance. - * @throws NullPointerException if adapterInstanceId is {@code null}. - */ - AdapterInstanceStatus getStatus(String adapterInstanceId); - - /** - * Gets the identifiers of the adapter instances from the given collection - * that have the {@link AdapterInstanceStatus#DEAD} status. - *

- * Compared to {@link #getStatus(String)}, extra measures may be taken here - * to resolve the status of adapter instances otherwise classified as - * {@link AdapterInstanceStatus#SUSPECTED_DEAD} before completing the result future. - * - * @param adapterInstanceIds The identifiers of the adapter instances. - * @return A succeeded future containing the identifiers of the dead adapter instances or a failed future - * indicating the reason why the operation failed. - * @throws NullPointerException if adapterInstanceIds is {@code null}. - */ - Future> getDeadAdapterInstances(Collection adapterInstanceIds); -} 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 ae4455aa2a..c027f729fe 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/Cache.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/Cache.java deleted file mode 100644 index eec68f8f7c..0000000000 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/Cache.java +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright (c) 2020, 2021 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.infinispan.client; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; - -/** - * A simple {@code Map} like interface to a data grid cache. - * - * @param The type of keys used for looking up data. - * @param The type of values stored in grid. - */ -public interface Cache { - - /** - * Checks if the cache is connected to the data grid. - *

- * If a cache is found to be not connected here, this method may trigger a connection (re)establishment. - * - * @return A future that is completed with information about a successful check's result. - * Otherwise, the future will be failed with a - * {@link org.eclipse.hono.client.ServerErrorException}. - */ - Future checkForCacheAvailability(); - - /** - * Puts a value to the cache. - * - * @param key The key. - * @param value The value. - * @return A succeeded future if the value has been stored successfully. - * A failed future if the value could not be stored in the cache. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - Future put(K key, V value); - - /** - * Puts a value to the cache. - * - * @param key The key. - * @param value The value. - * @param lifespan The lifespan of the entry. A negative value is interpreted as an unlimited lifespan. - * @param lifespanUnit The time unit for the lifespan. - * @return A succeeded future if the value has been stored successfully. - * A failed future if the value could not be stored in the cache. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - Future put(K key, V value, long lifespan, TimeUnit lifespanUnit); - - /** - * Puts all values of the given map to the cache. - * - * @param data The map with the entries to add. - * @return A succeeded future if the operation succeeded. - * A failed future if there was an error storing the entries in the cache. - * @throws NullPointerException if data is {@code null}. - */ - Future putAll(Map data); - - /** - * Puts all values of the given map to the cache. - * - * @param data The map with the entries to add. - * @param lifespan The lifespan of the entries. A negative value is interpreted as an unlimited lifespan. - * @param lifespanUnit The time unit for the lifespan. - * @return A succeeded future if the operation succeeded. - * A failed future if there was an error storing the entries in the cache. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - Future putAll(Map data, long lifespan, TimeUnit lifespanUnit); - - /** - * Gets a value from the cache. - * - * @param key The key. - * @return A succeeded future containing the value or {@code null} if the - * cache didn't contain the key yet. - * A failed future if the value could not be read from the cache. - * @throws NullPointerException if key is {@code null}. - */ - Future get(K key); - - /** - * Removes a key/value mapping from the cache. - * - * @param key The key. - * @param value The value. - * @return A succeeded future containing {@code true} if the key was - * mapped to the value, {@code false} otherwise. - * A failed future if the value could not be removed from the cache. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - Future remove(K key, V value); - - /** - * Gets the values for the specified keys from the cache. - * - * @param keys The keys. - * @return A succeeded future containing a map with key/value pairs. - * @throws NullPointerException if keys is {@code null}. - */ - Future> getAll(Set keys); -} diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfo.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfo.java deleted file mode 100644 index 519b29eacb..0000000000 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CacheBasedDeviceConnectionInfo.java +++ /dev/null @@ -1,693 +0,0 @@ -/** - * Copyright (c) 2020, 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.infinispan.client; - -import java.net.HttpURLConnection; -import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import org.eclipse.hono.client.ClientErrorException; -import org.eclipse.hono.client.ServerErrorException; -import org.eclipse.hono.client.util.ServiceClient; -import org.eclipse.hono.tracing.TracingHelper; -import org.eclipse.hono.util.AdapterInstanceStatus; -import org.eclipse.hono.util.DeviceConnectionConstants; -import org.eclipse.hono.util.Lifecycle; -import org.eclipse.hono.util.MessageHelper; -import org.eclipse.hono.util.RequestResponseApiConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.opentracing.Span; -import io.opentracing.Tracer; -import io.vertx.core.CompositeFuture; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.healthchecks.HealthCheckHandler; -import io.vertx.ext.healthchecks.Status; - - -/** - * A client for accessing device connection information in a data grid. - */ -public final class CacheBasedDeviceConnectionInfo implements DeviceConnectionInfo, ServiceClient, Lifecycle { - - /** - * Lifespan for last-known-gateway cache entries. - */ - static final Duration LAST_KNOWN_GATEWAY_CACHE_ENTRY_LIFESPAN = Duration.ofDays(28); - - /** - * For viaGateways parameter value lower or equal to this value, the {@link #getCommandHandlingAdapterInstances(String, String, Set, Span)} - * method will use an optimized approach, potentially saving additional cache requests. - */ - static final int VIA_GATEWAYS_OPTIMIZATION_THRESHOLD = 3; - - private static final Logger LOG = LoggerFactory.getLogger(CacheBasedDeviceConnectionInfo.class); - - /** - * Key prefix for cache entries having gateway id values, concerning lastKnownGatewayForDevice - * operations. - */ - private static final String KEY_PREFIX_GATEWAY_ENTRIES_VALUE = "gw"; - /** - * Key prefix for cache entries having protocol adapter instance id values, concerning - * commandHandlingAdapterInstance operations. - */ - private static final String KEY_PREFIX_ADAPTER_INSTANCE_VALUES = "ai"; - private static final String KEY_SEPARATOR = "@@"; - - final Cache cache; - final Tracer tracer; - final AdapterInstanceStatusProvider adapterInstanceStatusProvider; - - private DeviceToAdapterMappingErrorListener deviceToAdapterMappingErrorListener; - - /** - * Creates a client for accessing device connection information. - * - * @param cache The remote cache that contains the data. - * @param tracer The tracer instance. - * @throws NullPointerException if cache or tracer is {@code null}. - */ - public CacheBasedDeviceConnectionInfo(final Cache cache, final Tracer tracer) { - this(cache, tracer, null); - } - - /** - * Creates a client for accessing device connection information. - * - * @param cache The remote cache that contains the data. - * @param tracer The tracer instance. - * @param adapterInstanceStatusProvider The provider of the adapter instance status (may be {@code null}). - * @throws NullPointerException if cache or tracer is {@code null}. - */ - public CacheBasedDeviceConnectionInfo(final Cache cache, final Tracer tracer, - final AdapterInstanceStatusProvider adapterInstanceStatusProvider) { - this.cache = Objects.requireNonNull(cache); - this.tracer = Objects.requireNonNull(tracer); - this.adapterInstanceStatusProvider = Optional.ofNullable(adapterInstanceStatusProvider) - .orElseGet(UnknownStatusProvider::new); - } - - /** - * {@inheritDoc} - * - * If this method is invoked from a vert.x Context, then the returned future will be completed on that context. - */ - @Override - public Future setLastKnownGatewayForDevice( - final String tenantId, - final String deviceId, - final String gatewayId, - final Span span) { - - Objects.requireNonNull(tenantId); - Objects.requireNonNull(deviceId); - Objects.requireNonNull(gatewayId); - Objects.requireNonNull(span); - - final long lifespanMillis = LAST_KNOWN_GATEWAY_CACHE_ENTRY_LIFESPAN.toMillis(); - return cache.put(getGatewayEntryKey(tenantId, deviceId), gatewayId, lifespanMillis, TimeUnit.MILLISECONDS) - .onSuccess(ok -> LOG.debug("set last known gateway [tenant: {}, device-id: {}, gateway: {}]", - tenantId, deviceId, gatewayId)) - .otherwise(t -> { - LOG.debug("failed to set last known gateway [tenant: {}, device-id: {}, gateway: {}]", - tenantId, deviceId, gatewayId, t); - TracingHelper.logError(span, "failed to set last known gateway", t); - throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); - }); - } - - @Override - public Future setLastKnownGatewayForDevice( - final String tenantId, - final Map deviceIdToGatewayIdMap, - final Span span) { - - Objects.requireNonNull(tenantId); - Objects.requireNonNull(deviceIdToGatewayIdMap); - Objects.requireNonNull(span); - - if (deviceIdToGatewayIdMap.isEmpty()) { - return Future.succeededFuture(); - } - - final long lifespanMillis = LAST_KNOWN_GATEWAY_CACHE_ENTRY_LIFESPAN.toMillis(); - final Map mapToBePut = deviceIdToGatewayIdMap.entrySet().stream() - .collect(Collectors.toMap(entry -> getGatewayEntryKey(tenantId, entry.getKey()), Map.Entry::getValue)); - return cache.putAll(mapToBePut, lifespanMillis, TimeUnit.MILLISECONDS) - .onSuccess(ok -> LOG.debug("set {} last known gateway entries [tenant: {}]", - deviceIdToGatewayIdMap.size(), tenantId)) - .otherwise(t -> { - LOG.debug("failed to set {} last known gateway entries [tenant: {}]", - deviceIdToGatewayIdMap.size(), tenantId, t); - TracingHelper.logError(span, "failed to set last known gateway entries", t); - throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); - }); - } - - @Override - public Future getLastKnownGatewayForDevice( - final String tenantId, - final String deviceId, - final Span span) { - - Objects.requireNonNull(tenantId); - Objects.requireNonNull(deviceId); - Objects.requireNonNull(span); - - return cache.get(getGatewayEntryKey(tenantId, deviceId)) - .otherwise(t -> { - LOG.debug("failed to find last known gateway for device [tenant: {}, device-id: {}]", - tenantId, deviceId, t); - TracingHelper.logError(span, "failed to find last known gateway for device", t); - throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); - }) - .compose(gatewayId -> { - if (gatewayId == null) { - LOG.debug("could not find last known gateway for device [tenant: {}, device-id: {}]", tenantId, - deviceId); - return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND)); - } else { - LOG.debug("found last known gateway for device [tenant: {}, device-id: {}]: {}", tenantId, - deviceId, gatewayId); - return Future.succeededFuture(getLastKnownGatewayResultJson(gatewayId)); - } - }); - } - - @Override - public Future setCommandHandlingAdapterInstance( - final String tenantId, - final String deviceId, - final String adapterInstanceId, - final Duration lifespan, - final Span span) { - - Objects.requireNonNull(tenantId); - Objects.requireNonNull(deviceId); - Objects.requireNonNull(adapterInstanceId); - Objects.requireNonNull(span); - - // sanity check, preventing an ArithmeticException in lifespan.toMillis() - final long lifespanMillis = lifespan == null || lifespan.isNegative() - || lifespan.getSeconds() > (Long.MAX_VALUE / 1000L) ? -1 : lifespan.toMillis(); - return cache.put(getAdapterInstanceEntryKey(tenantId, deviceId), adapterInstanceId, lifespanMillis, TimeUnit.MILLISECONDS) - .onSuccess(ok -> LOG.debug( - "set command handling adapter instance [tenant: {}, device-id: {}, adapter-instance: {}, lifespan: {}ms]", - tenantId, deviceId, adapterInstanceId, lifespanMillis)) - .otherwise(t -> { - LOG.debug("failed to set command handling adapter instance [tenant: {}, device-id: {}, adapter-instance: {}, lifespan: {}ms]", - tenantId, deviceId, adapterInstanceId, lifespanMillis, t); - TracingHelper.logError(span, "failed to set command handling adapter instance cache entry", t); - throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); - }); - } - - @Override - public Future removeCommandHandlingAdapterInstance( - final String tenantId, - final String deviceId, - final String adapterInstanceId, - final Span span) { - - Objects.requireNonNull(tenantId); - Objects.requireNonNull(deviceId); - Objects.requireNonNull(adapterInstanceId); - Objects.requireNonNull(span); - - return cache - .remove(getAdapterInstanceEntryKey(tenantId, deviceId), adapterInstanceId) - .otherwise(t -> { - LOG.debug("failed to remove the cache entry for the command handling adapter instance [tenant: {}, device-id: {}, adapter-instance: {}]", - tenantId, deviceId, adapterInstanceId, t); - TracingHelper.logError(span, "failed to remove cache entry for the command handling adapter instance", t); - throw new ServerErrorException(tenantId, HttpURLConnection.HTTP_INTERNAL_ERROR, t); - }) - .compose(removed -> { - if (!removed) { - LOG.debug("command handling adapter instance was not removed, key not mapped or value didn't match [tenant: {}, device-id: {}, adapter-instance: {}]", - tenantId, deviceId, adapterInstanceId); - return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_PRECON_FAILED)); - } else { - LOG.debug("removed command handling adapter instance [tenant: {}, device-id: {}, adapter-instance: {}]", - tenantId, deviceId, adapterInstanceId); - return Future.succeededFuture(); - } - }); - - } - - @Override - public Future getCommandHandlingAdapterInstances( - final String tenantId, - final String deviceId, - final Set viaGateways, - final Span span) { - - Objects.requireNonNull(tenantId); - Objects.requireNonNull(deviceId); - Objects.requireNonNull(viaGateways); - Objects.requireNonNull(span); - - final Future resultFuture; - if (viaGateways.isEmpty()) { - // get the command handling adapter instance for the device (no gateway involved) - resultFuture = cache.get(getAdapterInstanceEntryKey(tenantId, deviceId)) - .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) - .compose(adapterInstanceId -> checkAdapterInstanceId(adapterInstanceId, tenantId, deviceId, span)) - .compose(adapterInstanceId -> { - if (adapterInstanceId == null) { - LOG.debug("no command handling adapter instances found [tenant: {}, device-id: {}]", - tenantId, deviceId); - span.log("no command handling adapter instances found for device (no via-gateways given)"); - return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND)); - } else { - LOG.debug("found command handling adapter instance '{}' [tenant: {}, device-id: {}]", - adapterInstanceId, tenantId, deviceId); - span.log("returning command handling adapter instance for device itself"); - setTagsForSingleResult(span, adapterInstanceId); - return Future.succeededFuture(getAdapterInstancesResultJson(deviceId, adapterInstanceId)); - } - }); - } else if (viaGateways.size() <= VIA_GATEWAYS_OPTIMIZATION_THRESHOLD) { - resultFuture = getInstancesQueryingAllGatewaysFirst(tenantId, deviceId, viaGateways, span); - } else { - // number of viaGateways is more than threshold value - reduce cache accesses by not checking *all* viaGateways, - // instead trying the last known gateway first - resultFuture = getInstancesGettingLastKnownGatewayFirst(tenantId, deviceId, viaGateways, span); - } - return resultFuture; - } - - @Override - public void setDeviceToAdapterMappingErrorListener( - final DeviceToAdapterMappingErrorListener obsoleteMappingListener) { - this.deviceToAdapterMappingErrorListener = obsoleteMappingListener; - } - - private Future getInstancesQueryingAllGatewaysFirst( - final String tenantId, - final String deviceId, - final Set viaGateways, - final Span span) { - - LOG.debug("using optimized query, retrieving {} via-gateways in one go", viaGateways.size()); - // get the command handling adapter instances for the device and *all* via-gateways in one call first - // (this saves the extra lastKnownGateway check if only one adapter instance is returned) - return cache.getAll(getAdapterInstanceEntryKeys(tenantId, deviceId, viaGateways)) - .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) - .compose(getAllMap -> checkAdapterInstanceIds(tenantId, convertAdapterInstanceEntryKeys(getAllMap), span)) - .compose(deviceToInstanceMap -> { - final Future resultFuture; - if (deviceToInstanceMap.isEmpty()) { - LOG.debug("no command handling adapter instances found [tenant: {}, device-id: {}]", - tenantId, deviceId); - span.log("no command handling adapter instances found for device or given via-gateways (" - + String.join(", ", viaGateways) + ")"); - resultFuture = Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND)); - } else if (deviceToInstanceMap.containsKey(deviceId)) { - // there is a adapter instance set for the device itself - that gets precedence - resultFuture = getAdapterInstanceFoundForDeviceItselfResult(tenantId, deviceId, - deviceToInstanceMap.get(deviceId), span); - } else if (deviceToInstanceMap.size() > 1) { - // multiple gateways found - check last known gateway - resultFuture = cache.get(getGatewayEntryKey(tenantId, deviceId)) - .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) - .compose(lastKnownGateway -> { - if (lastKnownGateway == null) { - // no last known gateway found - just return all found mapping entries - LOG.debug("returning {} command handling adapter instances for device gateways (no last known gateway found) [tenant: {}, device-id: {}]", - deviceToInstanceMap.size(), tenantId, deviceId); - span.log("no last known gateway found, returning all matching adapter instances"); - return Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); - } else if (!viaGateways.contains(lastKnownGateway)) { - // found gateway is not valid anymore - just return all found mapping entries - LOG.debug("returning {} command handling adapter instances for device gateways (last known gateway not valid anymore) [tenant: {}, device-id: {}, lastKnownGateway: {}]", - deviceToInstanceMap.size(), tenantId, deviceId, lastKnownGateway); - span.log(String.format( - "last known gateway '%s' is not valid anymore, returning all matching adapter instances", - lastKnownGateway)); - return Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); - } else if (!deviceToInstanceMap.containsKey(lastKnownGateway)) { - // found gateway has no command handling instance assigned - just return all found mapping entries - LOG.debug("returning {} command handling adapter instances for device gateways (last known gateway not in that list) [tenant: {}, device-id: {}, lastKnownGateway: {}]", - deviceToInstanceMap.size(), tenantId, deviceId, lastKnownGateway); - span.log(String.format( - "last known gateway '%s' has no adapter instance assigned, returning all matching adapter instances", - lastKnownGateway)); - return Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); - } else { - LOG.debug("returning command handling adapter instance '{}' for last known gateway [tenant: {}, device-id: {}, lastKnownGateway: {}]", - deviceToInstanceMap.get(lastKnownGateway), tenantId, deviceId, lastKnownGateway); - span.log("returning adapter instance for last known gateway '" + lastKnownGateway + "'"); - setTagsForSingleResultWithGateway(span, deviceToInstanceMap.get(lastKnownGateway), lastKnownGateway); - return Future.succeededFuture(getAdapterInstancesResultJson(lastKnownGateway, - deviceToInstanceMap.get(lastKnownGateway))); - } - }); - } else { - // one command handling instance found - final Map.Entry foundEntry = deviceToInstanceMap.entrySet().iterator().next(); - LOG.debug("returning command handling adapter instance '{}' associated with gateway {} [tenant: {}, device-id: {}]", - foundEntry.getValue(), foundEntry.getKey(), tenantId, deviceId); - span.log("returning adapter instance associated with gateway '" + foundEntry.getKey() + "'"); - setTagsForSingleResultWithGateway(span, foundEntry.getValue(), foundEntry.getKey()); - resultFuture = Future.succeededFuture(getAdapterInstancesResultJson(foundEntry.getKey(), - foundEntry.getValue())); - } - return resultFuture; - }); - } - - private void setTagsForSingleResultWithGateway(final Span span, final String adapterInstanceId, final String gatewayId) { - setTagsForSingleResult(span, adapterInstanceId); - span.setTag(MessageHelper.APP_PROPERTY_GATEWAY_ID, gatewayId); - } - - private void setTagsForSingleResult(final Span span, final String adapterInstanceId) { - TracingHelper.TAG_ADAPTER_INSTANCE_ID.set(span, adapterInstanceId); - } - - private Future getInstancesGettingLastKnownGatewayFirst( - final String tenantId, - final String deviceId, - final Set viaGateways, - final Span span) { - - return cache.get(getGatewayEntryKey(tenantId, deviceId)) - .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) - .compose(lastKnownGateway -> { - if (lastKnownGateway == null) { - LOG.trace("no last known gateway found [tenant: {}, device-id: {}]", tenantId, deviceId); - span.log("no last known gateway found"); - } else if (!viaGateways.contains(lastKnownGateway)) { - LOG.trace("found gateway is not valid for the device anymore [tenant: {}, device-id: {}]", tenantId, deviceId); - span.log("found gateway '" + lastKnownGateway + "' is not valid anymore"); - } - if (lastKnownGateway != null && viaGateways.contains(lastKnownGateway)) { - // fetch command handling instances for lastKnownGateway and device - return cache.getAll(getAdapterInstanceEntryKeys(tenantId, deviceId, lastKnownGateway)) - .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) - .compose(getAllMap -> checkAdapterInstanceIds(tenantId, convertAdapterInstanceEntryKeys(getAllMap), span)) - .compose(deviceToInstanceMap -> { - if (deviceToInstanceMap.isEmpty()) { - // no adapter instances found for last-known-gateway and device - check all via gateways - span.log(String.format( - "last known gateway '%s' has no adapter instance assigned, returning all matching adapter instances", - lastKnownGateway)); - return getAdapterInstancesWithoutLastKnownGatewayCheck(tenantId, deviceId, viaGateways, span); - } else if (deviceToInstanceMap.containsKey(deviceId)) { - // there is a adapter instance set for the device itself - that gets precedence - return getAdapterInstanceFoundForDeviceItselfResult(tenantId, deviceId, deviceToInstanceMap.get(deviceId), span); - } else { - // adapter instance found for last known gateway - LOG.debug("returning command handling adapter instance '{}' for last known gateway [tenant: {}, device-id: {}, lastKnownGateway: {}]", - deviceToInstanceMap.get(lastKnownGateway), tenantId, deviceId, lastKnownGateway); - span.log("returning adapter instance for last known gateway '" + lastKnownGateway + "'"); - setTagsForSingleResultWithGateway(span, deviceToInstanceMap.get(lastKnownGateway), lastKnownGateway); - return Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); - } - }); - } else { - // last-known-gateway not found or invalid - look for all adapter instances for device and viaGateways - return getAdapterInstancesWithoutLastKnownGatewayCheck(tenantId, deviceId, viaGateways, span); - } - }); - } - - private Future getAdapterInstancesWithoutLastKnownGatewayCheck( - final String tenantId, - final String deviceId, - final Set viaGateways, - final Span span) { - - return cache.getAll(getAdapterInstanceEntryKeys(tenantId, deviceId, viaGateways)) - .recover(t -> failedToGetEntriesWhenGettingInstances(tenantId, deviceId, t, span)) - .compose(getAllMap -> checkAdapterInstanceIds(tenantId, convertAdapterInstanceEntryKeys(getAllMap), span)) - .compose(deviceToInstanceMap -> { - final Future resultFuture; - if (deviceToInstanceMap.isEmpty()) { - LOG.debug("no command handling adapter instances found [tenant: {}, device-id: {}]", - tenantId, deviceId); - span.log("no command handling adapter instances found for device or given via-gateways (" - + String.join(", ", viaGateways) + ")"); - resultFuture = Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND)); - } else if (deviceToInstanceMap.containsKey(deviceId)) { - // there is a command handling instance set for the device itself - that gets precedence - resultFuture = getAdapterInstanceFoundForDeviceItselfResult(tenantId, deviceId, deviceToInstanceMap.get(deviceId), span); - } else { - LOG.debug("returning {} command handling adapter instance(s) (no last known gateway found) [tenant: {}, device-id: {}]", - deviceToInstanceMap.size(), tenantId, deviceId); - resultFuture = Future.succeededFuture(getAdapterInstancesResultJson(deviceToInstanceMap)); - } - return resultFuture; - }); - } - - private Future getAdapterInstanceFoundForDeviceItselfResult( - final String tenantId, - final String deviceId, - final String adapterInstanceId, - final Span span) { - - LOG.debug("returning command handling adapter instance '{}' for device itself [tenant: {}, device-id: {}]", - adapterInstanceId, tenantId, deviceId); - span.log("returning command handling adapter instance for device itself"); - setTagsForSingleResult(span, adapterInstanceId); - return Future.succeededFuture(getAdapterInstancesResultJson(deviceId, adapterInstanceId)); - } - - private Future failedToGetEntriesWhenGettingInstances( - final String tenantId, - final String deviceId, - final Throwable t, - final Span span) { - - LOG.debug("failed to get cache entries when trying to get command handling adapter instances [tenant: {}, device-id: {}]", - tenantId, deviceId, t); - TracingHelper.logError(span, "failed to get cache entries when trying to get command handling adapter instances", t); - return Future.failedFuture(new ServerErrorException(HttpURLConnection.HTTP_INTERNAL_ERROR, t)); - } - - static String getGatewayEntryKey(final String tenantId, final String deviceId) { - return KEY_PREFIX_GATEWAY_ENTRIES_VALUE + KEY_SEPARATOR + tenantId + KEY_SEPARATOR + deviceId; - } - - static String getAdapterInstanceEntryKey(final String tenantId, final String deviceId) { - return KEY_PREFIX_ADAPTER_INSTANCE_VALUES + KEY_SEPARATOR + tenantId + KEY_SEPARATOR + deviceId; - } - - static Set getAdapterInstanceEntryKeys( - final String tenantId, - final String deviceIdA, - final String deviceIdB) { - - final Set keys = new HashSet<>(2); - keys.add(getAdapterInstanceEntryKey(tenantId, deviceIdA)); - keys.add(getAdapterInstanceEntryKey(tenantId, deviceIdB)); - return keys; - } - - /** - * Puts the entries from the given map, having {@link #getAdapterInstanceEntryKey(String, String)} keys, into - * a new map with just the extracted device ids as keys. - * - * @param map Map to get the entries from. - * @return New map with keys containing just the device id. - */ - private static Map convertAdapterInstanceEntryKeys(final Map map) { - return map.entrySet().stream() - .collect(Collectors.toMap(entry -> getDeviceIdFromAdapterInstanceEntryKey( - entry.getKey()), Map.Entry::getValue)); - } - - private static String getDeviceIdFromAdapterInstanceEntryKey(final String key) { - final int pos = key.lastIndexOf(KEY_SEPARATOR); - return key.substring(pos + KEY_SEPARATOR.length()); - } - - static Set getAdapterInstanceEntryKeys( - final String tenantId, - final String deviceIdA, - final Set additionalDeviceIds) { - - final Set keys = new HashSet<>(additionalDeviceIds.size() + 1); - keys.add(getAdapterInstanceEntryKey(tenantId, deviceIdA)); - additionalDeviceIds.forEach(id -> keys.add(getAdapterInstanceEntryKey(tenantId, id))); - return keys; - } - - private static JsonObject getLastKnownGatewayResultJson(final String gatewayId) { - return new JsonObject().put(DeviceConnectionConstants.FIELD_GATEWAY_ID, gatewayId); - } - - private static JsonObject getAdapterInstancesResultJson(final Map deviceToAdapterInstanceMap) { - final JsonObject jsonObject = new JsonObject(); - final JsonArray adapterInstancesArray = new JsonArray(new ArrayList<>(deviceToAdapterInstanceMap.size())); - for (final Map.Entry resultEntry : deviceToAdapterInstanceMap.entrySet()) { - final JsonObject entryJson = new JsonObject(); - entryJson.put(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID, resultEntry.getKey()); - entryJson.put(DeviceConnectionConstants.FIELD_ADAPTER_INSTANCE_ID, resultEntry.getValue()); - adapterInstancesArray.add(entryJson); - } - jsonObject.put(DeviceConnectionConstants.FIELD_ADAPTER_INSTANCES, adapterInstancesArray); - return jsonObject; - } - - private static JsonObject getAdapterInstancesResultJson(final String deviceId, final String adapterInstanceId) { - return getAdapterInstancesResultJson(Map.of(deviceId, adapterInstanceId)); - } - - private Future> checkAdapterInstanceIds(final String tenantId, - final Map deviceToInstanceIdMap, final Span span) { - - @SuppressWarnings("rawtypes") - final List mappingFutures = new ArrayList<>(); - final Map deviceToInstanceIdMapResult = new HashMap<>(); - deviceToInstanceIdMap.entrySet().forEach(entry -> { - final Future mappingFuture = checkAdapterInstanceId(entry.getValue(), tenantId, entry.getKey(), span) - .map(adapterId -> { - if (adapterId != null) { - deviceToInstanceIdMapResult.put(entry.getKey(), entry.getValue()); - } - return adapterId; - }); - mappingFutures.add(mappingFuture); - }); - - return CompositeFuture.join(mappingFutures).map(deviceToInstanceIdMapResult); - } - - private Future checkAdapterInstanceId( - final String adapterInstanceId, - final String tenantId, - final String deviceId, - final Span span) { - - if (adapterInstanceId != null) { - final AdapterInstanceStatus status = adapterInstanceStatusProvider.getStatus(adapterInstanceId); - if (status == AdapterInstanceStatus.DEAD) { - LOG.debug( - "ignoring found adapter instance id, belongs to already terminated container [tenant: {}, device-id: {}, adapter-instance-id: {}]", - tenantId, deviceId, adapterInstanceId); - span.log("ignoring found adapter instance id [" + adapterInstanceId - + "], belongs to already terminated container"); - final Future listenerResult; - if (deviceToAdapterMappingErrorListener != null) { - listenerResult = deviceToAdapterMappingErrorListener.onObsoleteEntryFound(tenantId, deviceId, - adapterInstanceId, - span); - } else { - listenerResult = Future.succeededFuture(); - } - return listenerResult - .onSuccess(v -> { - if (deviceToAdapterMappingErrorListener != null) { - LOG.debug( - "called listener for obsolete adapter instance id '{}' [tenant: {}, device-id: {}]", - adapterInstanceId, tenantId, deviceId); - } - }) - .onFailure(thr -> { - LOG.debug( - "error calling listener for obsolete adapter instance id '{}' [tenant: {}, device-id: {}]", - adapterInstanceId, tenantId, deviceId, thr); - }) - .compose(s -> { - return cache.remove(getAdapterInstanceEntryKey(tenantId, deviceId), adapterInstanceId) - .onSuccess(removed -> { - if (removed) { - LOG.debug( - "removed entry with obsolete adapter instance id '{}' [tenant: {}, device-id: {}]", - adapterInstanceId, tenantId, deviceId); - } - }) - .onFailure(thr -> { - LOG.debug( - "error removing entry with obsolete adapter instance id '{}' [tenant: {}, device-id: {}]", - adapterInstanceId, tenantId, deviceId, thr); - }); - }) - .recover(thr -> { - // errors treated as not found adapter instance - return Future.succeededFuture(); - }) - .mapEmpty(); - } else if (status == AdapterInstanceStatus.SUSPECTED_DEAD) { - LOG.debug( - "ignoring found adapter instance id, belongs to container with state 'SUSPECTED_DEAD' [tenant: {}, device-id: {}, adapter-instance-id: {}]", - tenantId, deviceId, adapterInstanceId); - span.log("ignoring found adapter instance id [" + adapterInstanceId + - "], belongs to container with state 'SUSPECTED_DEAD'"); - return Future.succeededFuture(); - } - } - return Future.succeededFuture(adapterInstanceId); - } - - /** - * {@inheritDoc} - *

- * Registers a check which verifies if the underlying cache is available. - * The check times out (and fails) after 1000ms. - */ - @Override - public void registerReadinessChecks(final HealthCheckHandler readinessHandler) { - readinessHandler.register("remote-cache-connection", 1000, this::checkForCacheAvailability); - } - - private void checkForCacheAvailability(final Promise status) { - - cache.checkForCacheAvailability() - .map(Status::OK) - .otherwise(t -> Status.KO()) - .onComplete(ar -> status.tryComplete(ar.result())); - } - - @Override - public void registerLivenessChecks(final HealthCheckHandler livenessHandler) { - // nothing to register - } - - @Override - public Future start() { - if (cache instanceof Lifecycle) { - return ((Lifecycle) cache).start(); - } else { - return Future.succeededFuture(); - } - } - - @Override - public Future stop() { - if (cache instanceof Lifecycle) { - return ((Lifecycle) cache).stop(); - } else { - return Future.succeededFuture(); - } - } -} diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheConfig.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheConfig.java deleted file mode 100644 index ec4109424c..0000000000 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheConfig.java +++ /dev/null @@ -1,86 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2020, 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.infinispan.client; - -import com.google.common.base.MoreObjects; - -/** - * Common cache configuration options. - */ -public class CommonCacheConfig { - - /** - * The default name of the (remote) cache in the data grid that is used for - * storing device connection information. - */ - public static final String DEFAULT_CACHE_NAME = "device-connection"; - - private String cacheName = DEFAULT_CACHE_NAME; - - private String checkKey = "KEY_CONNECTION_CHECK"; - private String checkValue = "VALUE_CONNECTION_CHECK"; - - /** - * Creates properties for default values. - */ - public CommonCacheConfig() { - super(); - } - - /** - * Creates properties for existing options. - * - * @param options The options to copy. - */ - public CommonCacheConfig(final CommonCacheOptions options) { - super(); - this.cacheName = options.cacheName(); - this.checkKey = options.checkKey(); - this.checkValue = options.checkValue(); - } - - public void setCacheName(final String cacheName) { - this.cacheName = cacheName; - } - - public String getCacheName() { - return cacheName; - } - - public void setCheckKey(final String checkKey) { - this.checkKey = checkKey; - } - - public String getCheckKey() { - return checkKey; - } - - public void setCheckValue(final String checkValue) { - this.checkValue = checkValue; - } - - public String getCheckValue() { - return checkValue; - } - - @Override - public String toString() { - return MoreObjects - .toStringHelper(this) - .add("cacheName", this.cacheName) - .add("checkKey", this.checkKey) - .add("checkValue", this.checkValue) - .toString(); - } -} diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheOptions.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheOptions.java deleted file mode 100644 index 8118a198f5..0000000000 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/CommonCacheOptions.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2021 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.infinispan.client; - -import org.eclipse.hono.util.CommandRouterConstants; - -import io.smallrye.config.ConfigMapping; -import io.smallrye.config.ConfigMapping.NamingStrategy; -import io.smallrye.config.WithDefault; - -/** - * Common options for configuring a cache. - * - */ -@ConfigMapping(prefix = "hono.cache.common", namingStrategy = NamingStrategy.VERBATIM) -public interface CommonCacheOptions { - - /** - * Gets the name of the cache. - * - * @return The name. - */ - @WithDefault(CommandRouterConstants.DEFAULT_CACHE_NAME) - String cacheName(); - - /** - * Gets the key to use for checking the cache's availability. - * - * @return The key. - */ - @WithDefault("KEY_CONNECTION_CHECK") - String checkKey(); - - /** - * The value to use for checking the cache's availability. - * - * @return The value. - */ - @WithDefault("VALUE_CONNECTION_CHECK") - String checkValue(); -} diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceConnectionInfo.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceConnectionInfo.java deleted file mode 100644 index 4b7e800298..0000000000 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceConnectionInfo.java +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright (c) 2020, 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.infinispan.client; - -import java.time.Duration; -import java.util.Map; -import java.util.Set; - -import io.opentracing.Span; -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; - -/** - * A repository for keeping connection information about devices. - * - */ -public interface DeviceConnectionInfo { - - /** - * Sets the gateway that last acted on behalf of a device. - *

- * If a device connects directly instead of through a gateway, the device identifier itself is to be used as value - * for the gatewayId parameter. - * - * @param tenant The tenant that the device belongs to. - * @param deviceId The device identifier. - * @param gatewayId The gateway identifier. This may be the same as the device identifier if the device is - * (currently) not connected via a gateway but directly to a protocol adapter. - * @param span The active OpenTracing span for this operation. It is not to be closed in this method! - * An implementation should log (error) events on this span and it may set tags and use this span as the - * parent for any spans created in this method. - * @return A future indicating the outcome of the operation. - *

- * The future will be succeeded if the device connection information has been updated. - * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - Future setLastKnownGatewayForDevice(String tenant, String deviceId, String gatewayId, Span span); - - /** - * For a given list of device and gateway combinations, sets the gateway as the last gateway that acted on behalf - * of the device. - *

- * If a device connects directly instead of through a gateway, the device identifier itself is to be used as - * gateway value. - * - * @param tenant The tenant that the device belongs to. - * @param deviceIdToGatewayIdMap The map containing device identifiers and associated gateway identifiers. The - * gateway identifier may be the same as the device identifier if the device is - * (currently) not connected via a gateway but directly to a protocol adapter. - * @param span The active OpenTracing span for this operation. It is not to be closed in this method! - * An implementation should log (error) events on this span and it may set tags and use this span as the - * parent for any spans created in this method. - * @return A future indicating the outcome of the operation. - *

- * The future will be succeeded if the device connection information has been updated. - * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. - * The outcome is indeterminate if any of the entries cannot be processed by an implementation. - * In such a case, client code should assume that none of the entries have been updated. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - Future setLastKnownGatewayForDevice(String tenant, Map deviceIdToGatewayIdMap, Span span); - - /** - * Gets the gateway that last acted on behalf of a device. - *

- * If no last known gateway has been set for the given device yet, a failed future with status - * 404 is returned. - * - * @param tenant The tenant that the device belongs to. - * @param deviceId The device identifier. - * @param span The active OpenTracing span for this operation. It is not to be closed in this method! - * An implementation should log (error) events on this span and it may set tags and use this span as the - * parent for any spans created in this method. - * @return A future indicating the outcome of the operation. - *

- * The future will be succeeded with a JSON object containing the currently mapped gateway ID - * in the gateway-id property, if device connection information has been found for - * the given device. - * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - Future getLastKnownGatewayForDevice(String tenant, String deviceId, Span span); - - /** - * Sets the protocol adapter instance that handles commands for the given device or gateway. - * - * @param tenantId The tenant id. - * @param deviceId The device id. - * @param adapterInstanceId The protocol adapter instance id. - * @param lifespan The lifespan of the mapping entry. Using a negative duration or {@code null} here is - * interpreted as an unlimited lifespan. - * @param span The active OpenTracing span for this operation. It is not to be closed in this method! - * An implementation should log (error) events on this span and it may set tags and use this span as the - * parent for any spans created in this method. - * @return A future indicating the outcome of the operation. - *

- * The future will be succeeded if the device connection information has been updated. - * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. - * @throws NullPointerException if any of the parameters except lifespan is {@code null}. - */ - Future setCommandHandlingAdapterInstance(String tenantId, String deviceId, String adapterInstanceId, - Duration lifespan, Span span); - - /** - * Removes the mapping information that associates the given device with the given protocol adapter instance - * that handles commands for the given device. The mapping entry is only deleted if its value - * contains the given protocol adapter instance id. - * - * @param tenantId The tenant id. - * @param deviceId The device id. - * @param adapterInstanceId The protocol adapter instance id that the entry to be removed has to contain. - * @param span The active OpenTracing span for this operation. It is not to be closed in this method! - * An implementation should log (error) events on this span and it may set tags and use this span as the - * parent for any spans created in this method. - * @return A future indicating the outcome of the operation. - *

- * The future will be succeeded if the entry was successfully removed. - * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. - * @throws NullPointerException if any of the parameters except context is {@code null}. - */ - Future removeCommandHandlingAdapterInstance(String tenantId, String deviceId, String adapterInstanceId, Span span); - - /** - * Gets information about the adapter instances that can handle a command for the given device. - *

- * In order to determine the adapter instances the following rules are applied (in the given order): - *

    - *
  1. If an adapter instance is associated with the given device, this adapter instance is returned as the single - * returned list entry.
  2. - *
  3. Otherwise, if there is an adapter instance registered for the last known gateway associated with the given - * device, this adapter instance is returned as the single returned list entry. The last known gateway has to be - * contained in the given list of gateways for this case.
  4. - *
  5. Otherwise, all adapter instances associated with any of the given gateway identifiers are returned.
  6. - *
- * That means that for a device communicating via a gateway, the result is reduced to a single element list - * if an adapter instance for the device itself or its last known gateway is found. The adapter instance registered - * for the device itself is given precedence in order to ensure that a gateway having subscribed to commands for - * that particular device is chosen over a gateway that has subscribed to commands for all devices of a tenant. - *

- * The resulting JSON structure looks like this, possibly containing multiple array entries: - * { - * "adapter-instances": [ - * { - * "adapter-instance-id": "adapter-1", - * "device-id": "4711" - * } - * ] - * } - * - *

- * If no adapter instances are found, the returned future is failed. - * - * @param tenantId The tenant id. - * @param deviceId The device id. - * @param viaGateways The set of gateways that may act on behalf of the given device. - * @param span The active OpenTracing span for this operation. It is not to be closed in this method! - * An implementation should log (error) events on this span and it may set tags and use this span as the - * parent for any spans created in this method. - * @return A future indicating the outcome of the operation. - *

- * If instances were found, the future will be succeeded with a JSON object containing one or more mappings - * from device id to adapter instance id. - * Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - Future getCommandHandlingAdapterInstances(String tenantId, String deviceId, Set viaGateways, Span span); - - /** - * Sets listener to be notified when an incorrect device to adapter mapping is identified. - * - * @param deviceToAdapterMappingErrorListener The listener. - */ - void setDeviceToAdapterMappingErrorListener(DeviceToAdapterMappingErrorListener deviceToAdapterMappingErrorListener); -} diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceToAdapterMappingErrorListener.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceToAdapterMappingErrorListener.java deleted file mode 100644 index bdb45e069b..0000000000 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/DeviceToAdapterMappingErrorListener.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 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.infinispan.client; - -import io.opentracing.Span; -import io.vertx.core.Future; - -/** - * Listener notified when an incorrect device to adapter mapping is found. - */ -public interface DeviceToAdapterMappingErrorListener { - - /** - * Called when an obsolete device to adapter mapping is found. - * - * @param tenantId The tenant identifier. - * @param deviceId The device identifier. - * @param adapterInstanceId The adapter instance identifier. - * @param span The active OpenTracing span for this operation. It is not to be closed in this method! An - * implementation should log (error) events on this span and it may set tags and use this span as the - * parent for any spans created in this method. - * @return A future indicating the outcome of the operation. The future will be succeeded if the listener is - * notified successfully. - */ - Future onObsoleteEntryFound(String tenantId, String deviceId, String adapterInstanceId, Span span); -} 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..754554f0c7 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; diff --git a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/UnknownStatusProvider.java b/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/UnknownStatusProvider.java deleted file mode 100644 index d52468ce44..0000000000 --- a/client-device-connection-infinispan/src/main/java/org/eclipse/hono/deviceconnection/infinispan/client/UnknownStatusProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 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.infinispan.client; - -import java.util.Collection; -import java.util.Set; - -import org.eclipse.hono.util.AdapterInstanceStatus; - -import io.vertx.core.Future; - - -/** - * Status provider that always returns the {@link AdapterInstanceStatus#UNKNOWN} status. - * - */ -final class UnknownStatusProvider implements AdapterInstanceStatusProvider { - - @Override - public AdapterInstanceStatus getStatus(final String adapterInstanceId) { - return AdapterInstanceStatus.UNKNOWN; - } - - @Override - public Future> getDeadAdapterInstances( - final Collection adapterInstanceIds) { - return Future.succeededFuture(Set.of()); - } -} 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/QuarkusPropertyBindingTest.java index 53fe74cf6a..9cbed5b014 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/QuarkusPropertyBindingTest.java @@ -19,6 +19,8 @@ import javax.security.sasl.Sasl; +import org.eclipse.hono.deviceconnection.common.CommonCacheConfig; +import org.eclipse.hono.deviceconnection.common.CommonCacheOptions; import org.eclipse.hono.test.ConfigMappingSupport; import org.infinispan.client.hotrod.configuration.ClusterConfiguration; import org.infinispan.client.hotrod.configuration.Configuration; diff --git a/client-device-connection-redis/pom.xml b/client-device-connection-redis/pom.xml new file mode 100644 index 0000000000..8c6be49009 --- /dev/null +++ b/client-device-connection-redis/pom.xml @@ -0,0 +1,175 @@ + + + + 4.0.0 + + org.eclipse.hono + hono-bom + 2.5.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 + + + + redis.clients + jedis + 4.3.1 + + + + + + + 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 + + + + \ No newline at end of file diff --git a/caches/cache-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/RedisCache.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java similarity index 97% rename from caches/cache-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/RedisCache.java rename to client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java index 74809a3d0c..a7486ed1bb 100644 --- a/caches/cache-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/RedisCache.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java @@ -11,13 +11,13 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.hono.deviceconnection.redis; +package org.eclipse.hono.deviceconnection.redis.client; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; -import org.eclipse.hono.cache.Cache; +import org.eclipse.hono.deviceconnection.common.Cache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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/common-cache-options.yaml b/client-device-connection-redis/src/test/resources/common-cache-options.yaml new file mode 100644 index 0000000000..edacdbfc7d --- /dev/null +++ b/client-device-connection-redis/src/test/resources/common-cache-options.yaml @@ -0,0 +1,6 @@ +hono: + cache: + common: + cacheName: "the-cache" + checkKey: "the-key" + checkValue: "the-value" 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..0da04c2e0b --- /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 index ce4177a6a8..73614b6cb5 100644 --- a/client-device-connection/pom.xml +++ b/client-device-connection/pom.xml @@ -1,6 +1,6 @@ - + 4.0.0 org.eclipse.hono @@ -21,43 +20,125 @@ 2.5.0-SNAPSHOT ../bom - hono-client-device-connection + + client-device-connection - Hono Client Device Connection Cache - Common classes for Hono's Client Device Connection cache + Device Connection client + Base classes for client for accessing device connection information in a remote cache / data grid. org.eclipse.hono - hono-client-common + hono-legal + + + org.slf4j + slf4j-api org.eclipse.hono - hono-cache-common + hono-client-common - + - + + 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 + + + + \ No newline at end of file diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/AdapterInstanceStatusProvider.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/AdapterInstanceStatusProvider.java similarity index 97% rename from client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/AdapterInstanceStatusProvider.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/AdapterInstanceStatusProvider.java index 7a55f28d6e..64a18cf0f6 100644 --- a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/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; +package org.eclipse.hono.deviceconnection.common; import java.util.Collection; import java.util.Set; diff --git a/caches/cache-common/src/main/java/org/eclipse/hono/cache/Cache.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/Cache.java similarity index 98% rename from caches/cache-common/src/main/java/org/eclipse/hono/cache/Cache.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/Cache.java index ff48131da7..42d28a015c 100644 --- a/caches/cache-common/src/main/java/org/eclipse/hono/cache/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.cache; +package org.eclipse.hono.deviceconnection.common; import java.util.Map; import java.util.Set; diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/CacheBasedDeviceConnectionInfo.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CacheBasedDeviceConnectionInfo.java similarity index 99% rename from client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/CacheBasedDeviceConnectionInfo.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CacheBasedDeviceConnectionInfo.java index 9787bb5f70..b6e65f8b41 100644 --- a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/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; +package org.eclipse.hono.deviceconnection.common; import java.net.HttpURLConnection; import java.time.Duration; @@ -26,7 +26,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import org.eclipse.hono.cache.Cache; import org.eclipse.hono.client.ClientErrorException; import org.eclipse.hono.client.ServerErrorException; import org.eclipse.hono.client.util.ServiceClient; diff --git a/caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheConfig.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheConfig.java similarity index 97% rename from caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheConfig.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheConfig.java index 573474bb12..c969ab3c49 100644 --- a/caches/cache-common/src/main/java/org/eclipse/hono/cache/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.cache; +package org.eclipse.hono.deviceconnection.common; import com.google.common.base.MoreObjects; diff --git a/caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheOptions.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheOptions.java similarity index 96% rename from caches/cache-common/src/main/java/org/eclipse/hono/cache/CommonCacheOptions.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/CommonCacheOptions.java index 1820c99a06..bbd69c036d 100644 --- a/caches/cache-common/src/main/java/org/eclipse/hono/cache/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.cache; +package org.eclipse.hono.deviceconnection.common; import org.eclipse.hono.util.CommandRouterConstants; diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceConnectionInfo.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceConnectionInfo.java similarity index 99% rename from client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceConnectionInfo.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceConnectionInfo.java index 2bffcf8812..f3c2d2a35c 100644 --- a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/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; +package org.eclipse.hono.deviceconnection.common; import java.time.Duration; import java.util.Map; diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceToAdapterMappingErrorListener.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceToAdapterMappingErrorListener.java similarity index 96% rename from client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/DeviceToAdapterMappingErrorListener.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/DeviceToAdapterMappingErrorListener.java index 7a4fec04f2..0fa4d7fb95 100644 --- a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/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; +package org.eclipse.hono.deviceconnection.common; import io.opentracing.Span; import io.vertx.core.Future; diff --git a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/UnknownStatusProvider.java b/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/UnknownStatusProvider.java similarity index 95% rename from client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/UnknownStatusProvider.java rename to client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/common/UnknownStatusProvider.java index abe7644875..861ecfda79 100644 --- a/client-device-connection/src/main/java/org/eclipse/hono/deviceconnection/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; +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..28f5671c6f 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; @@ -50,6 +50,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; import io.opentracing.Span; import io.opentracing.Tracer; @@ -396,7 +397,7 @@ public void testGetCommandHandlingAdapterInstancesWithExpiredEntry(final VertxTe @Test public void testGetCommandHandlingAdapterInstancesWithTerminatedAdapterInstance(final VertxTestContext ctx) { - final AdapterInstanceStatusProvider statusProvider = mock(AdapterInstanceStatusProvider.class); + final AdapterInstanceStatusProvider statusProvider = Mockito.mock(AdapterInstanceStatusProvider.class); info = new CacheBasedDeviceConnectionInfo(cache, tracer, statusProvider); final Promise listenerTracker = Promise.promise(); @@ -441,7 +442,7 @@ public void testGetCommandHandlingAdapterInstancesWithTerminatedAdapterInstance( @Test public void testGetCommandHandlingAdapterInstancesWithSuspectedAdapterInstance(final VertxTestContext ctx) { - final AdapterInstanceStatusProvider statusProvider = mock(AdapterInstanceStatusProvider.class); + final AdapterInstanceStatusProvider statusProvider = Mockito.mock(AdapterInstanceStatusProvider.class); info = new CacheBasedDeviceConnectionInfo(cache, tracer, statusProvider); final String deviceId = "testDevice"; @@ -761,7 +762,7 @@ public void testGetCommandHandlingAdapterInstancesWithoutLastKnownGatewayIsGivin @MethodSource("extraUnusedViaGateways") public void testGetCommandHandlingAdapterInstancesWithTerminatedAdapterInstanceContainer(final Set extraUnusedViaGateways, final VertxTestContext ctx) { - final AdapterInstanceStatusProvider statusProvider = mock(AdapterInstanceStatusProvider.class); + final AdapterInstanceStatusProvider statusProvider = Mockito.mock(AdapterInstanceStatusProvider.class); info = new CacheBasedDeviceConnectionInfo(cache, tracer, statusProvider); final String deviceId = "testDevice"; @@ -813,7 +814,7 @@ public void testGetCommandHandlingAdapterInstancesWithTerminatedAdapterInstanceC @MethodSource("extraUnusedViaGateways") public void testGetCommandHandlingAdapterInstancesWithSuspectedAdapterInstanceContainer(final Set extraUnusedViaGateways, final VertxTestContext ctx) { - final AdapterInstanceStatusProvider statusProvider = mock(AdapterInstanceStatusProvider.class); + final AdapterInstanceStatusProvider statusProvider = Mockito.mock(AdapterInstanceStatusProvider.class); info = new CacheBasedDeviceConnectionInfo(cache, tracer, statusProvider); final String deviceId = "testDevice"; diff --git a/pom.xml b/pom.xml index d88ff4516f..439212acb6 100644 --- a/pom.xml +++ b/pom.xml @@ -225,7 +225,9 @@ bom core cli - + client-device-connection + client-device-connection-infinispan + client-device-connection-redis clients demo-certs examples @@ -235,8 +237,6 @@ site test-utils tests - client-device-connection - caches diff --git a/services/command-router/pom.xml b/services/command-router/pom.xml index 1b50625bd1..d35c53eaff 100644 --- a/services/command-router/pom.xml +++ b/services/command-router/pom.xml @@ -25,22 +25,18 @@ org.eclipse.hono hono-client-device-connection + --> org.eclipse.hono - hono-cache-infinispan + client-device-connection-infinispan org.eclipse.hono - hono-cache-redis + client-device-connection-redis 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 e1e10b3588..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.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 9bfc674e5e..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.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 eb389772d3..8f5654f020 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 @@ -64,7 +64,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.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 9dbc6753eb..588f43fe16 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 @@ -23,18 +23,18 @@ import javax.enterprise.inject.Produces; import javax.inject.Singleton; -import org.eclipse.hono.cache.Cache; -import org.eclipse.hono.cache.CommonCacheConfig; -import org.eclipse.hono.cache.CommonCacheOptions; import org.eclipse.hono.commandrouter.AdapterInstanceStatusService; 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.CacheBasedDeviceConnectionInfo; -import org.eclipse.hono.deviceconnection.DeviceConnectionInfo; -import org.eclipse.hono.deviceconnection.infinispan.InfinispanRemoteConfigurationOptions; -import org.eclipse.hono.deviceconnection.infinispan.InfinispanRemoteConfigurationProperties; -import org.eclipse.hono.deviceconnection.redis.RedisCache; +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.InfinispanRemoteConfigurationOptions; +import org.eclipse.hono.deviceconnection.infinispan.client.InfinispanRemoteConfigurationProperties; +import org.eclipse.hono.deviceconnection.redis.client.RedisCache; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; import org.infinispan.configuration.parsing.ParserRegistry; 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 0ca0b2cdce..25651125c5 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.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 bcb669597c..c85844b3de 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.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 c3568cdc92..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.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; From f68e63da1599dd7b66040c3e84af4d6c61c08446 Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Tue, 29 Aug 2023 22:34:15 +0000 Subject: [PATCH 03/20] WIP: experimenting with using the vertx-redis client library. --- bom/pom.xml | 5 + .../infinispan/client/BasicCache.java | 9 +- client-device-connection-redis/pom.xml | 41 ++- .../redis/client/RedisCache.java | 251 +++++++++++++----- .../RedisRemoteConfigurationOptions.java | 27 ++ .../RedisRemoteConfigurationProperties.java | 23 ++ .../app/DeviceConnectionInfoProducer.java | 16 +- tests/pom.xml | 33 ++- 8 files changed, 334 insertions(+), 71 deletions(-) create mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationOptions.java create mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationProperties.java diff --git a/bom/pom.xml b/bom/pom.xml index 851588b971..0740a6a084 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -593,6 +593,11 @@ quarkus.vertx.max-event-loop-execute-time=${max.event-loop.execute-time:20000} vertx-mongo-client ${vertx.version} + + io.vertx + vertx-redis-client + ${vertx.version} + io.vertx vertx-kafka-client 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 c027f729fe..0df0a06f5b 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 @@ -205,7 +205,14 @@ public Future get(final K key) { public Future> getAll(final Set keys) { Objects.requireNonNull(keys); - return withCache(aCache -> aCache.getAllAsync(keys)); + final var result = withCache(aCache -> aCache.getAllAsync(keys)); + result.onSuccess(r -> { + LOG.info("BasicCache: getAll() ({})", keys.size()); + final var resultKeys = r.keySet(); + r.forEach((k, v) -> LOG.info("BasicCache#getAll() result: {}:{}", k, v)); + keys.forEach((k) -> LOG.info("BasicCache#getAll() keys exist in result: {} / {}", k, resultKeys.contains(k))); + }); + return result; } /** diff --git a/client-device-connection-redis/pom.xml b/client-device-connection-redis/pom.xml index 8c6be49009..d7f335624c 100644 --- a/client-device-connection-redis/pom.xml +++ b/client-device-connection-redis/pom.xml @@ -33,9 +33,44 @@ - redis.clients - jedis - 4.3.1 + io.vertx + vertx-redis-client + + + + 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 + + magenta - magenta green @@ -760,6 +759,7 @@ 27017 9094 + 6379 15671 15672 @@ -1283,6 +1283,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} @@ -2243,7 +2272,7 @@ true **/hono-*-test:* - hono-*-test-*,all-in-one-*,cp-*,postgres-* + hono-*-test-*,all-in-one-*,cp-*,postgres-*,redis-* From 2a20828fbf1c9c7a4628b88607ef213fd5620d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=C3=A1n=20Freyr=20Stef=C3=A1nsson?= Date: Wed, 30 Aug 2023 22:43:52 +0000 Subject: [PATCH 04/20] Add Lifecycle interface to RedisCache and simplify creation and closing of redis client (stop retrying connections for now). --- .../redis/client/RedisCache.java | 79 ++++++++++++------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java index 00aefa95c5..50c5691651 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.hono.deviceconnection.common.Cache; +import org.eclipse.hono.util.Lifecycle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +41,7 @@ * @param TODO * @param TODO */ -public class RedisCache implements Cache { +public class RedisCache implements Cache, Lifecycle { private static final Logger LOG = LoggerFactory.getLogger(RedisCache.class); @@ -59,7 +60,7 @@ public class RedisCache implements Cache { * @param vertx TODO. * @param properties TODO. */ - public RedisCache(final Vertx vertx, final RedisRemoteConfigurationProperties properties) { + private RedisCache(final Vertx vertx, final RedisRemoteConfigurationProperties properties) { Objects.requireNonNull(vertx); Objects.requireNonNull(properties); @@ -68,9 +69,9 @@ public RedisCache(final Vertx vertx, final RedisRemoteConfigurationProperties pr this.properties = properties; LOG.info("Initializing REDIS cache!"); - createRedisClient() - .onSuccess( c -> LOG.info("Connected to Redis")) - .onFailure( t -> LOG.error("Could not connect to Redis", t)); + //createRedisClient() + // .onSuccess( c -> LOG.info("Connected to Redis")) + // .onFailure( t -> LOG.error("Could not connect to Redis", t)); } /** @@ -90,9 +91,33 @@ public static RedisCache from( return new RedisCache<>(vertx, properties); } - /** + @Override + public Future start() { + LOG.info("REDIS: starting cache"); + final Promise promise = Promise.promise(); + /* + createRedisClient() + .onSuccess(c -> promise.complete()) + .onFailure(promise::fail); + return promise.future(); + */ + redis = Redis.createClient(vertx, properties); + redis.connect() + .onSuccess(c -> client = c) + .onFailure(promise::fail); + return promise.future(); + } + + @Override + public Future stop() { + LOG.info("REDIS: stopping cache"); + redis.close(); + return Future.succeededFuture(); + } + + /* * Will create a redis client and set up a reconnect handler when there is an exception in the connection. - */ + * private Future createRedisClient() { final Promise promise = Promise.promise(); @@ -143,6 +168,7 @@ private void attemptReconnect(final int retry) { vertx.setTimer(backoff, timer -> createRedisClient().onFailure(t -> attemptReconnect(retry + 1))); } } + */ @Override public Future checkForCacheAvailability() { @@ -151,11 +177,10 @@ public Future checkForCacheAvailability() { Objects.requireNonNull(client); final Promise promise = Promise.promise(); - final RedisAPI redis = RedisAPI.api(client); - redis.ping(List.of()) + final RedisAPI api = RedisAPI.api(client); + api.ping(List.of()) .onSuccess(v -> promise.complete(new JsonObject())) .onFailure(promise::fail); - return promise.future(); } @@ -163,8 +188,8 @@ public Future checkForCacheAvailability() { public Future put(final K key, final V value) { LOG.info("REDIS: put {}={}", key, value); final Promise promise = Promise.promise(); - final RedisAPI redis = RedisAPI.api(client); - redis.set(List.of(key.toString(), value.toString())) + final RedisAPI api = RedisAPI.api(client); + api.set(List.of(key.toString(), value.toString())) .onSuccess(v -> promise.complete()) .onFailure(promise::fail); return promise.future(); @@ -175,12 +200,12 @@ public Future put(final K key, final V value, final long lifespan, final T LOG.info("REDIS: put {}={} ({} {})", key, value, lifespan, lifespanUnit); final long millis = lifespanUnit.toMillis(lifespan); final Promise promise = Promise.promise(); - final RedisAPI redis = RedisAPI.api(client); + final RedisAPI api = RedisAPI.api(client); final List params = new ArrayList<>(List.of(key.toString(), value.toString())); if (millis > 0) { params.addAll(List.of("PX", String.valueOf(millis))); } - redis.set(params) + api.set(params) .onSuccess(v -> promise.complete()) .onFailure(promise::fail); return promise.future(); @@ -190,12 +215,12 @@ public Future put(final K key, final V value, final long lifespan, final T public Future putAll(final Map data) { LOG.info("REDIS: putAll ({})", data.size()); final Promise promise = Promise.promise(); - final RedisAPI redis = RedisAPI.api(client); + final RedisAPI api = RedisAPI.api(client); final List keyValues = new ArrayList<>(data.size() * 2); data.forEach((k, v) -> { keyValues.add(k.toString()); keyValues.add(v.toString()); }); - redis.mset(keyValues) + api.mset(keyValues) .onSuccess(v -> promise.complete()) .onFailure(promise::fail); return promise.future(); @@ -206,16 +231,16 @@ public Future putAll(final Map data, final long LOG.info("REDIS: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); final Promise promise = Promise.promise(); final long millis = lifespanUnit.toMillis(lifespan); - final RedisAPI redis = RedisAPI.api(client); - redis.multi(); + final RedisAPI api = RedisAPI.api(client); + api.multi(); data.forEach((k, v) -> { final List params = new ArrayList<>(List.of(k.toString(), v.toString())); if (millis > 0) { params.addAll(List.of("PX", String.valueOf(millis))); } - redis.set(params); + api.set(params); }); - redis.exec() + api.exec() .onSuccess(v -> promise.complete()) .onFailure(promise::fail); return promise.future(); @@ -225,8 +250,8 @@ public Future putAll(final Map data, final long public Future get(final K key) { LOG.info("REDIS: get {}", key); final Promise promise = Promise.promise(); - final RedisAPI redis = RedisAPI.api(client); - redis.get(key.toString()) + final RedisAPI api = RedisAPI.api(client); + api.get(key.toString()) .onSuccess(v -> promise.complete((V) v) ) .onFailure(promise::fail); return promise.future(); @@ -237,8 +262,8 @@ public Future remove(final K key, final V value) { LOG.info("REDIS: remove {}={}", key, value); //TODO: why is the value being passed here? Do we need to use that? final Promise promise = Promise.promise(); - final RedisAPI redis = RedisAPI.api(client); - redis.del(List.of(key.toString())) + final RedisAPI api = RedisAPI.api(client); + api.del(List.of(key.toString())) .onSuccess(v -> promise.complete(true)) .onFailure(promise::fail); return promise.future(); @@ -249,19 +274,19 @@ public Future> getAll(final Set keys) { LOG.info("REDIS: getAll {}", keys.size()); final Promise> promise = Promise.promise(); - final RedisAPI redis = RedisAPI.api(client); + final RedisAPI api = RedisAPI.api(client); // Make sure the keys are in order and we can pop off the front final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); keyList.forEach(i -> LOG.info("REDIS: Item: {}", i)); final Map result = new HashMap<>(keyList.size()); - redis.mget(keyList) + api.mget(keyList) .onComplete(v -> { LOG.info("REDIS: Got {} items back...", v.result().stream().toList().size()); v.result().forEach(i -> { LOG.info("Iterating through result list: {}", 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((K) keyList.removeFirst(), i == null ? null : (V) i.toString()); + result.put((K) keyList.removeFirst(), (V) i.toString()); } } catch (Exception e) { LOG.info(" - got exception {}", e.getMessage()); From 2f41346615ee198ede6a3dd3bb908f36fd6ebf72 Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Mon, 25 Sep 2023 11:15:07 +0000 Subject: [PATCH 05/20] Minor code formatting cleanup from PR comments. --- client-device-connection-redis/pom.xml | 4 ++-- client-device-connection/pom.xml | 2 +- .../common/CacheBasedDeviceConnectionInfo.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client-device-connection-redis/pom.xml b/client-device-connection-redis/pom.xml index d7f335624c..b3e55e9e8e 100644 --- a/client-device-connection-redis/pom.xml +++ b/client-device-connection-redis/pom.xml @@ -1,6 +1,6 @@ + org.eclipse.hono + hono-legal + - org.infinispan - infinispan-core + org.eclipse.hono + hono-core + + + org.eclipse.hono + hono-client-common + + + + org.slf4j + slf4j-api + + + io.smallrye.config + smallrye-config-core + + + com.google.guava + guava + + + + io.vertx + vertx-core - io.quarkus quarkus-core @@ -88,35 +118,6 @@ - - - org.junit.jupiter diff --git a/client-device-connection-redis/pom.xml b/client-device-connection-redis/pom.xml index b3e55e9e8e..4d45436759 100644 --- a/client-device-connection-redis/pom.xml +++ b/client-device-connection-redis/pom.xml @@ -37,117 +37,29 @@ vertx-redis-client - - 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.eclipse.hono + hono-legal - - diff --git a/client-device-connection/pom.xml b/client-device-connection/pom.xml index 6130550049..f21c437496 100644 --- a/client-device-connection/pom.xml +++ b/client-device-connection/pom.xml @@ -30,18 +30,40 @@ org.eclipse.hono hono-legal + - org.slf4j - slf4j-api + 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-web + vertx-health-check + com.google.guava From e66006d85242d0a2b8383b52a3f0eec9d1d32faa Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Wed, 27 Sep 2023 23:37:07 +0000 Subject: [PATCH 08/20] Integration tests with RedisCache actually working. --- .../infinispan/client/BasicCache.java | 16 +- .../redis/client/RedisCache.java | 214 +++++++++++------- .../app/DeviceConnectionInfoProducer.java | 4 +- 3 files changed, 142 insertions(+), 92 deletions(-) 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 0df0a06f5b..0a31f07af6 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 @@ -14,6 +14,7 @@ package org.eclipse.hono.deviceconnection.infinispan.client; import java.net.HttpURLConnection; +import java.util.LinkedList; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -156,6 +157,7 @@ protected void postCacheAccess(final AsyncResult cacheOperationResult) { @Override public Future put(final K key, final V value) { + LOG.info("BASIC: put {}={}", key, value); Objects.requireNonNull(key); Objects.requireNonNull(value); @@ -164,6 +166,7 @@ public Future put(final K key, final V value) { @Override public Future put(final K key, final V value, final long lifespan, final TimeUnit lifespanUnit) { + LOG.info("BASIC: put {}={} ({} {})", key, value, lifespan, lifespanUnit); Objects.requireNonNull(key); Objects.requireNonNull(value); Objects.requireNonNull(lifespanUnit); @@ -173,6 +176,7 @@ public Future put(final K key, final V value, final long lifespan, final T @Override public Future putAll(final Map data) { + LOG.info("BASIC: putAll ({})", data.size()); Objects.requireNonNull(data); return withCache(aCache -> aCache.putAllAsync(data)); @@ -180,6 +184,7 @@ public Future putAll(final Map data) { @Override public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { + LOG.info("BASIC: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); Objects.requireNonNull(data); Objects.requireNonNull(lifespanUnit); @@ -188,6 +193,7 @@ public Future putAll(final Map data, final long @Override public Future remove(final K key, final V value) { + LOG.info("BASIC: remove {}={}", key, value); Objects.requireNonNull(key); Objects.requireNonNull(value); @@ -196,6 +202,7 @@ public Future remove(final K key, final V value) { @Override public Future get(final K key) { + LOG.info("BASIC: get {}", key); Objects.requireNonNull(key); return withCache(aCache -> aCache.getAsync(key)); @@ -203,14 +210,15 @@ public Future get(final K key) { @Override public Future> getAll(final Set keys) { + LOG.info("BASIC: getAll {}", keys.size()); Objects.requireNonNull(keys); + final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); + keyList.forEach(i -> LOG.info("BASIC: Key: {}", i)); final var result = withCache(aCache -> aCache.getAllAsync(keys)); result.onSuccess(r -> { - LOG.info("BasicCache: getAll() ({})", keys.size()); - final var resultKeys = r.keySet(); - r.forEach((k, v) -> LOG.info("BasicCache#getAll() result: {}:{}", k, v)); - keys.forEach((k) -> LOG.info("BasicCache#getAll() keys exist in result: {} / {}", k, resultKeys.contains(k))); + LOG.info("BASIC: Got {} items back...", r.size()); + r.forEach((k, v) -> LOG.info("Iterating through result list: {}", v)); }); return result; } diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java index 50c5691651..009a2a798f 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java @@ -14,6 +14,7 @@ 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; @@ -28,6 +29,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; @@ -35,6 +37,7 @@ import io.vertx.redis.client.Redis; import io.vertx.redis.client.RedisAPI; import io.vertx.redis.client.RedisConnection; +import io.vertx.redis.client.Response; /** * TODO. @@ -94,18 +97,22 @@ public static RedisCache from( @Override public Future start() { LOG.info("REDIS: starting cache"); - final Promise promise = Promise.promise(); + return createRedisClient() + .flatMap(c -> Future.succeededFuture()); /* + final Promise promise = Promise.promise(); createRedisClient() .onSuccess(c -> promise.complete()) .onFailure(promise::fail); return promise.future(); */ + /* redis = Redis.createClient(vertx, properties); redis.connect() .onSuccess(c -> client = c) .onFailure(promise::fail); return promise.future(); + */ } @Override @@ -117,7 +124,7 @@ public Future stop() { /* * Will create a redis client and set up a reconnect handler when there is an exception in the connection. - * + */ private Future createRedisClient() { final Promise promise = Promise.promise(); @@ -128,8 +135,7 @@ private Future createRedisClient() { if (CONNECTING.compareAndSet(false, true)) { redis = Redis.createClient(vertx, properties); - redis - .connect() + redis.connect() .onSuccess(conn -> { client = conn; @@ -168,7 +174,6 @@ private void attemptReconnect(final int retry) { vertx.setTimer(backoff, timer -> createRedisClient().onFailure(t -> attemptReconnect(retry + 1))); } } - */ @Override public Future checkForCacheAvailability() { @@ -176,125 +181,160 @@ public Future checkForCacheAvailability() { Objects.requireNonNull(client); - final Promise promise = Promise.promise(); - final RedisAPI api = RedisAPI.api(client); - api.ping(List.of()) - .onSuccess(v -> promise.complete(new JsonObject())) - .onFailure(promise::fail); - return promise.future(); + return redis.connect().flatMap(connection -> { + final RedisAPI api = RedisAPI.api(connection); + return api.ping(List.of()) + .eventually(ignored -> connection.close()) + .mapEmpty(); + }); } @Override public Future put(final K key, final V value) { LOG.info("REDIS: put {}={}", key, value); - final Promise promise = Promise.promise(); - final RedisAPI api = RedisAPI.api(client); - api.set(List.of(key.toString(), value.toString())) - .onSuccess(v -> promise.complete()) - .onFailure(promise::fail); - return promise.future(); + Objects.requireNonNull(client); + + return redis.connect().flatMap(connection -> { + final RedisAPI api = RedisAPI.api(connection); + return api.set(List.of(String.valueOf(key), String.valueOf(value))) + .eventually(ignored -> connection.close()) + .mapEmpty(); + }); } @Override public Future put(final K key, final V value, final long lifespan, final TimeUnit lifespanUnit) { LOG.info("REDIS: put {}={} ({} {})", key, value, lifespan, lifespanUnit); - final long millis = lifespanUnit.toMillis(lifespan); - final Promise promise = Promise.promise(); - final RedisAPI api = RedisAPI.api(client); - final List params = new ArrayList<>(List.of(key.toString(), value.toString())); - if (millis > 0) { - params.addAll(List.of("PX", String.valueOf(millis))); - } - api.set(params) - .onSuccess(v -> promise.complete()) - .onFailure(promise::fail); - return promise.future(); + Objects.requireNonNull(client); + + return redis.connect().flatMap(connection -> { + final RedisAPI api = RedisAPI.api(connection); + final List params = new ArrayList<>(List.of(key.toString(), value.toString())); + final long millis = lifespanUnit.toMillis(lifespan); + if (millis > 0) { + params.addAll(List.of("PX", String.valueOf(millis))); + } + return api.set(params) + .eventually(ignored -> connection.close()) + .mapEmpty(); + }); } @Override public Future putAll(final Map data) { LOG.info("REDIS: putAll ({})", data.size()); - final Promise promise = Promise.promise(); - final RedisAPI api = RedisAPI.api(client); - final List keyValues = new ArrayList<>(data.size() * 2); - data.forEach((k, v) -> { - keyValues.add(k.toString()); - keyValues.add(v.toString()); }); - api.mset(keyValues) - .onSuccess(v -> promise.complete()) - .onFailure(promise::fail); - return promise.future(); + Objects.requireNonNull(client); + + return redis.connect().flatMap(connection -> { + final RedisAPI api = RedisAPI.api(connection); + final List keyValues = new ArrayList<>(data.size() * 2); + data.forEach((k, v) -> { + keyValues.add(k.toString()); + keyValues.add(v.toString()); + }); + return api.mset(keyValues) + .eventually(ignored -> connection.close()) + .mapEmpty(); + }); } @Override public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { LOG.info("REDIS: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); - final Promise promise = Promise.promise(); + Objects.requireNonNull(client); + final long millis = lifespanUnit.toMillis(lifespan); - final RedisAPI api = RedisAPI.api(client); - api.multi(); - data.forEach((k, v) -> { - final List params = new ArrayList<>(List.of(k.toString(), v.toString())); - if (millis > 0) { - params.addAll(List.of("PX", String.valueOf(millis))); - } - api.set(params); + return redis.connect().flatMap(connection -> { + final RedisAPI api = RedisAPI.api(connection); + return api.multi() + .flatMap(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 CompositeFuture.all(Collections.unmodifiableList(futures)); + }) + .flatMap(ignored -> api.exec()) + // null reply means transaction aborted + .map(Objects::nonNull) + .eventually(ignored -> connection.close()) + .mapEmpty(); }); - api.exec() - .onSuccess(v -> promise.complete()) - .onFailure(promise::fail); - return promise.future(); } @Override public Future get(final K key) { LOG.info("REDIS: get {}", key); - final Promise promise = Promise.promise(); - final RedisAPI api = RedisAPI.api(client); - api.get(key.toString()) - .onSuccess(v -> promise.complete((V) v) ) - .onFailure(promise::fail); - return promise.future(); + Objects.requireNonNull(client); + + return redis.connect().flatMap(connection -> { + final RedisAPI api = RedisAPI.api(connection); + return api.get(String.valueOf(key)) + .flatMap(value -> Future.succeededFuture((V) String.valueOf(value))) + .eventually(ignored -> connection.close()); + }); } @Override public Future remove(final K key, final V value) { LOG.info("REDIS: remove {}={}", key, value); - //TODO: why is the value being passed here? Do we need to use that? - final Promise promise = Promise.promise(); - final RedisAPI api = RedisAPI.api(client); - api.del(List.of(key.toString())) - .onSuccess(v -> promise.complete(true)) - .onFailure(promise::fail); - return promise.future(); + Objects.requireNonNull(client); + + return redis.connect().flatMap(connection -> { + final RedisAPI api = RedisAPI.api(connection); + return api.watch(List.of(String.valueOf(key))) + .flatMap(ignored -> api.get(String.valueOf(key))) + .flatMap(response -> { + if (response == null) { + // key does not exist + return Future.succeededFuture(false); + } + if (String.valueOf(response).equals(value)) { + return api.multi() + .flatMap(ignored -> api.del(List.of(String.valueOf(key)))) + .flatMap(ignored -> api.exec()) + // null reply means transaction aborted + .map(Objects::nonNull); + } else { + return Future.succeededFuture(false); + } + }) + .eventually(ignored -> connection.close()); + }); } @Override public Future> getAll(final Set keys) { LOG.info("REDIS: getAll {}", keys.size()); - final Promise> promise = Promise.promise(); - - final RedisAPI api = RedisAPI.api(client); - // Make sure the keys are in order and we can pop off the front - final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); - keyList.forEach(i -> LOG.info("REDIS: Item: {}", i)); - final Map result = new HashMap<>(keyList.size()); - api.mget(keyList) - .onComplete(v -> { - LOG.info("REDIS: Got {} items back...", v.result().stream().toList().size()); - v.result().forEach(i -> { - LOG.info("Iterating through result list: {}", 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((K) keyList.removeFirst(), (V) i.toString()); + Objects.requireNonNull(client); + + return redis.connect().flatMap(connection -> { + final RedisAPI api = RedisAPI.api(connection); + final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); + keyList.forEach(i -> LOG.info("REDIS: Key: {}", i)); + final Map result = new HashMap<>(keyList.size()); + return api.mget(keyList) + .flatMap(values -> { + LOG.info("REDIS: Got {} items back...", values.stream().toList().size()); + values.forEach(i -> { + LOG.info("Iterating through result list: {}", 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((K) keyList.removeFirst(), (V) i.toString()); + } else { + keyList.removeFirst(); + } + } catch (Exception e) { + LOG.info(" - got exception {}", e.getMessage()); } - } catch (Exception e) { - LOG.info(" - got exception {}", e.getMessage()); - } - }); - promise.complete(result); - }) - .onFailure(promise::fail); - return promise.future(); + }); + return Future.succeededFuture(result); + }) + .eventually(ignored -> connection.close()); + }); } } 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 2806e61651..9e8ddc7be8 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 @@ -84,7 +84,9 @@ Cache cache( final var commonCacheConfig = new CommonCacheConfig(commonCacheOptions); final var infinispanCacheConfig = new InfinispanRemoteConfigurationProperties(remoteCacheConfigurationOptions); - if (true) { + final String cacheBackend = System.getProperty("cache.backend"); + LOG.info("######################### Cache Backend: {}", cacheBackend); + if ("redis".equalsIgnoreCase(cacheBackend)) { LOG.info("Creating a new REDIS cache."); final var p = new RedisRemoteConfigurationProperties(); p.setConnectionString("redis://redis:6379"); From b3a45c21554bc27f0ad885070e72100d11de5481 Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Wed, 27 Sep 2023 23:48:36 +0000 Subject: [PATCH 09/20] working on dependencies --- client-device-connection-infinispan/pom.xml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/client-device-connection-infinispan/pom.xml b/client-device-connection-infinispan/pom.xml index 5ed3ef25f0..f3a5910266 100644 --- a/client-device-connection-infinispan/pom.xml +++ b/client-device-connection-infinispan/pom.xml @@ -32,6 +32,15 @@ client-device-connection + + org.eclipse.hono + hono-legal + + + org.slf4j + slf4j-api + + org.infinispan infinispan-core @@ -51,10 +60,6 @@ infinispan-query-dsl - - org.eclipse.hono - hono-legal - org.eclipse.hono hono-core @@ -64,10 +69,6 @@ hono-client-common - - org.slf4j - slf4j-api - io.smallrye.config smallrye-config-core From d94f28c3d034f1575e09ce271ef71bcda88fd219 Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Wed, 27 Sep 2023 23:54:24 +0000 Subject: [PATCH 10/20] working on dependencies --- client-device-connection-infinispan/pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client-device-connection-infinispan/pom.xml b/client-device-connection-infinispan/pom.xml index f3a5910266..b23fbc76d8 100644 --- a/client-device-connection-infinispan/pom.xml +++ b/client-device-connection-infinispan/pom.xml @@ -41,10 +41,6 @@ slf4j-api - - org.infinispan - infinispan-core - org.infinispan infinispan-client-hotrod @@ -59,6 +55,10 @@ org.infinispan infinispan-query-dsl + + org.infinispan + infinispan-core + org.eclipse.hono From 91549e6934de74794750e66d322207795b618f7d Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Thu, 28 Sep 2023 00:02:52 +0000 Subject: [PATCH 11/20] working on dependencies --- client-device-connection-infinispan/pom.xml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client-device-connection-infinispan/pom.xml b/client-device-connection-infinispan/pom.xml index b23fbc76d8..cdaceb9f76 100644 --- a/client-device-connection-infinispan/pom.xml +++ b/client-device-connection-infinispan/pom.xml @@ -69,18 +69,19 @@ hono-client-common - - io.smallrye.config - smallrye-config-core + + io.vertx + vertx-core + com.google.guava guava - - io.vertx - vertx-core + + io.smallrye.config + smallrye-config-core From 0e6474c58d3766f2978fefa5f42f99c01510ce30 Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Thu, 28 Sep 2023 00:20:10 +0000 Subject: [PATCH 12/20] working on dependencies --- tests/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/pom.xml b/tests/pom.xml index 986aa6f55a..4dd110221b 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -56,6 +56,7 @@ 120000 magenta + magenta green From 00c20fb3656a8d8b7f841f44f5deefdb28a1d8ee Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Tue, 3 Oct 2023 18:39:08 +0000 Subject: [PATCH 13/20] First implementation of a Quarkus Redis Client cache. --- bom/pom.xml | 16 ++- client-device-connection-redis/pom.xml | 4 +- .../redis/client/RedisCache.java | 60 ++++---- .../redis/client/RedisQuarkusCache.java | 133 ++++++++++++++++++ .../app/DeviceConnectionInfoProducer.java | 9 ++ .../commandrouter/redis-cache/application.yml | 95 +++++++++++++ 6 files changed, 279 insertions(+), 38 deletions(-) create mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisQuarkusCache.java create mode 100644 tests/src/test/resources/commandrouter/redis-cache/application.yml diff --git a/bom/pom.xml b/bom/pom.xml index d2e1492d0f..d2f8fe5d60 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -266,6 +266,17 @@ quarkus.vertx.max-event-loop-execute-time=${max.event-loop.execute-time:20000} + + io.quarkus + quarkus-redis-client + ${quarkus.platform.version} + + + io.quarkus + quarkus-ide-launcher + + + io.quarkus quarkus-vertx @@ -593,11 +604,6 @@ quarkus.vertx.max-event-loop-execute-time=${max.event-loop.execute-time:20000} vertx-mongo-client ${vertx.version} - - io.vertx - vertx-redis-client - ${vertx.version} - io.vertx vertx-kafka-client diff --git a/client-device-connection-redis/pom.xml b/client-device-connection-redis/pom.xml index 4d45436759..0d85739cce 100644 --- a/client-device-connection-redis/pom.xml +++ b/client-device-connection-redis/pom.xml @@ -33,8 +33,8 @@ - io.vertx - vertx-redis-client + io.quarkus + quarkus-redis-client diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java index 009a2a798f..413b977267 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java @@ -41,10 +41,8 @@ /** * TODO. - * @param TODO - * @param TODO */ -public class RedisCache implements Cache, Lifecycle { +public class RedisCache implements Cache, Lifecycle { private static final Logger LOG = LoggerFactory.getLogger(RedisCache.class); @@ -85,18 +83,18 @@ private RedisCache(final Vertx vertx, final RedisRemoteConfigurationProperties p * * @return TODO. */ - public static RedisCache from( + public static RedisCache from( final Vertx vertx, final RedisRemoteConfigurationProperties properties) { Objects.requireNonNull(vertx); Objects.requireNonNull(properties); - return new RedisCache<>(vertx, properties); + return new RedisCache(vertx, properties); } @Override public Future start() { - LOG.info("REDIS: starting cache"); + LOG.info("VREDIS: starting cache"); return createRedisClient() .flatMap(c -> Future.succeededFuture()); /* @@ -117,7 +115,7 @@ public Future start() { @Override public Future stop() { - LOG.info("REDIS: stopping cache"); + LOG.info("VREDIS: stopping cache"); redis.close(); return Future.succeededFuture(); } @@ -130,7 +128,7 @@ private Future createRedisClient() { // make sure to invalidate old connection if present if (redis != null) { - redis.close();; + redis.close(); } if (CONNECTING.compareAndSet(false, true)) { @@ -177,7 +175,7 @@ private void attemptReconnect(final int retry) { @Override public Future checkForCacheAvailability() { - LOG.info("REDIS: checking for cache availability"); + LOG.info("VREDIS: checking for cache availability"); Objects.requireNonNull(client); @@ -190,8 +188,8 @@ public Future checkForCacheAvailability() { } @Override - public Future put(final K key, final V value) { - LOG.info("REDIS: put {}={}", key, value); + public Future put(final String key, final String value) { + LOG.info("VREDIS: put {}={}", key, value); Objects.requireNonNull(client); return redis.connect().flatMap(connection -> { @@ -203,13 +201,13 @@ public Future put(final K key, final V value) { } @Override - public Future put(final K key, final V value, final long lifespan, final TimeUnit lifespanUnit) { - LOG.info("REDIS: put {}={} ({} {})", key, value, lifespan, lifespanUnit); + 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(client); return redis.connect().flatMap(connection -> { final RedisAPI api = RedisAPI.api(connection); - final List params = new ArrayList<>(List.of(key.toString(), value.toString())); + 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))); @@ -221,16 +219,16 @@ public Future put(final K key, final V value, final long lifespan, final T } @Override - public Future putAll(final Map data) { - LOG.info("REDIS: putAll ({})", data.size()); + public Future putAll(final Map data) { + LOG.info("VREDIS: putAll ({})", data.size()); Objects.requireNonNull(client); return redis.connect().flatMap(connection -> { final RedisAPI api = RedisAPI.api(connection); final List keyValues = new ArrayList<>(data.size() * 2); data.forEach((k, v) -> { - keyValues.add(k.toString()); - keyValues.add(v.toString()); + keyValues.add(k); + keyValues.add(v); }); return api.mset(keyValues) .eventually(ignored -> connection.close()) @@ -239,8 +237,8 @@ public Future putAll(final Map data) { } @Override - public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { - LOG.info("REDIS: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); + public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { + LOG.info("VREDIS: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); Objects.requireNonNull(client); final long millis = lifespanUnit.toMillis(lifespan); @@ -267,21 +265,21 @@ public Future putAll(final Map data, final long } @Override - public Future get(final K key) { - LOG.info("REDIS: get {}", key); + public Future get(final String key) { + LOG.info("VREDIS: get {}", key); Objects.requireNonNull(client); return redis.connect().flatMap(connection -> { final RedisAPI api = RedisAPI.api(connection); return api.get(String.valueOf(key)) - .flatMap(value -> Future.succeededFuture((V) String.valueOf(value))) + .flatMap(value -> Future.succeededFuture(String.valueOf(value))) .eventually(ignored -> connection.close()); }); } @Override - public Future remove(final K key, final V value) { - LOG.info("REDIS: remove {}={}", key, value); + public Future remove(final String key, final String value) { + LOG.info("VREDIS: remove {}={}", key, value); Objects.requireNonNull(client); return redis.connect().flatMap(connection -> { @@ -308,23 +306,23 @@ public Future remove(final K key, final V value) { } @Override - public Future> getAll(final Set keys) { - LOG.info("REDIS: getAll {}", keys.size()); + public Future> getAll(final Set keys) { + LOG.info("VREDIS: getAll {}", keys.size()); Objects.requireNonNull(client); return redis.connect().flatMap(connection -> { final RedisAPI api = RedisAPI.api(connection); final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); - keyList.forEach(i -> LOG.info("REDIS: Key: {}", i)); - final Map result = new HashMap<>(keyList.size()); + keyList.forEach(i -> LOG.info("VREDIS: Key: {}", i)); + final Map result = new HashMap<>(keyList.size()); return api.mget(keyList) .flatMap(values -> { - LOG.info("REDIS: Got {} items back...", values.stream().toList().size()); + LOG.info("VREDIS: Got {} items back...", values.stream().toList().size()); values.forEach(i -> { LOG.info("Iterating through result list: {}", 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((K) keyList.removeFirst(), (V) i.toString()); + result.put(keyList.removeFirst(), i.toString()); } else { keyList.removeFirst(); } diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisQuarkusCache.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisQuarkusCache.java new file mode 100644 index 0000000000..34585fe1d9 --- /dev/null +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisQuarkusCache.java @@ -0,0 +1,133 @@ +/* +TODO + */ + +package org.eclipse.hono.deviceconnection.redis.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.eclipse.hono.deviceconnection.common.Cache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.quarkus.redis.datasource.ReactiveRedisDataSource; +import io.quarkus.redis.datasource.transactions.OptimisticLockingTransactionResult; +import io.quarkus.redis.datasource.transactions.ReactiveTransactionalRedisDataSource; +import io.quarkus.redis.datasource.transactions.TransactionResult; +import io.quarkus.redis.datasource.value.ReactiveTransactionalValueCommands; +import io.quarkus.redis.datasource.value.ReactiveValueCommands; +import io.quarkus.redis.datasource.value.SetArgs; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; + +/** + * An implementation of the Redis device connection class using the Quarkus Redis client library. + */ +//@javax.enterprise.context.ApplicationScoped +public class RedisQuarkusCache implements Cache { + private static final Logger LOG = LoggerFactory.getLogger(RedisCache.class); + + //private final RedisAPI redisApi; + private final ReactiveRedisDataSource reactiveRedisDataSource; + private final ReactiveValueCommands valueCommands; + + /** + * TODO. + * @param reactiveRedisDataSource TODO + */ + public RedisQuarkusCache(final ReactiveRedisDataSource reactiveRedisDataSource) { + //this.redisApi = redisApi; + this.reactiveRedisDataSource = reactiveRedisDataSource; + valueCommands = reactiveRedisDataSource.value(String.class, String.class); + } + + + @Override + public Future checkForCacheAvailability() { + LOG.info("QREDIS: checking for cache availability"); + return Future.fromCompletionStage( + valueCommands.set("TEST_KEY", "TEST_VALUE") + .onItem().transform(setResult -> valueCommands.get("TEST_KEY")) + .onItem().transform(getResult -> new JsonObject()) + .subscribeAsCompletionStage()); + } + + @Override + public Future put(final String key, final String value) { + LOG.info("QREDIS: put {}={}", key, value); + return Future.fromCompletionStage(valueCommands.set(key, value).subscribeAsCompletionStage()); + } + + @Override + public Future put(final String key, final String value, final long lifespan, final TimeUnit lifespanUnit) { + LOG.info("QREDIS: put {}={} ({} {})", key, value, lifespan, lifespanUnit); + final long millis = lifespanUnit.toMillis(lifespan); + return Future.fromCompletionStage(valueCommands.set(key, value, new SetArgs().px(millis)).subscribeAsCompletionStage()); + } + + @Override + public Future putAll(final Map data) { + LOG.info("QREDIS: putAll ({})", data.size()); + return Future.fromCompletionStage(valueCommands.mset((Map) data).subscribeAsCompletionStage()); + } + + @Override + public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { + LOG.info("QREDIS: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); + final long millis = lifespanUnit.toMillis(lifespan); + final Function> txBlock = ds -> { + final ReactiveTransactionalValueCommands txValueCommends = ds.value(String.class, String.class); + final List> unis = new ArrayList<>(); + data.forEach((k, v) -> { + unis.add(txValueCommends.set(k, v, new SetArgs().px(millis))); + }); + return Uni.join().all(unis).andFailFast().replaceWithVoid(); + }; + final Uni result = reactiveRedisDataSource.withTransaction(txBlock); + return Future.fromCompletionStage(result.onItem().transformToUni(txResult -> Uni.createFrom().voidItem()).subscribeAsCompletionStage()); + } + + @Override + public Future get(final String key) { + LOG.info("QREDIS: get {}", key); + return Future.fromCompletionStage(valueCommands.get(key).subscribeAsCompletionStage()); + } + + @Override + public Future remove(final String key, final String value) { + LOG.info("QREDIS: remove {}={}", key, value); + final Function> preTxBlock = ds -> { + return ds.value(String.class, String.class).get(key); + }; + final BiFunction> txBlock = (redisKeyValue, ds) -> { + if (value.equals(redisKeyValue)) { + return ds.key(String.class).del(key); + } else { + return ds.discard(); + } + }; + final Uni> result = reactiveRedisDataSource.withTransaction(preTxBlock, txBlock, key); + return Future.fromCompletionStage( + result.onItem().transformToUni(txResult -> Uni.createFrom().item(!txResult.isEmpty())).subscribeAsCompletionStage()); + } + + @Override + public Future> getAll(final Set keys) { + LOG.info("QREDIS: getAll {}", keys.size()); + return Future.fromCompletionStage( + valueCommands.mget(keys.toArray(new String[0])) + .onItem().transform(result -> { + result.values().removeIf(Objects::isNull); + return result; + }) + .subscribeAsCompletionStage()); + } +} 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 9e8ddc7be8..6fd06d6c31 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 @@ -21,6 +21,7 @@ import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.Produces; +import javax.inject.Inject; import javax.inject.Singleton; import org.eclipse.hono.commandrouter.AdapterInstanceStatusService; @@ -37,6 +38,7 @@ import org.eclipse.hono.deviceconnection.infinispan.client.InfinispanRemoteConfigurationOptions; import org.eclipse.hono.deviceconnection.infinispan.client.InfinispanRemoteConfigurationProperties; import org.eclipse.hono.deviceconnection.redis.client.RedisCache; +import org.eclipse.hono.deviceconnection.redis.client.RedisQuarkusCache; import org.eclipse.hono.deviceconnection.redis.client.RedisRemoteConfigurationProperties; import org.eclipse.hono.util.Strings; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -48,6 +50,7 @@ import org.slf4j.LoggerFactory; import io.opentracing.Tracer; +import io.quarkus.redis.datasource.ReactiveRedisDataSource; import io.smallrye.config.ConfigMapping; import io.vertx.core.Vertx; @@ -65,6 +68,9 @@ public class DeviceConnectionInfoProducer { @ConfigProperty(name = "hono.commandRouter.cache.embedded.configurationFile", defaultValue = "/etc/hono/cache-config.xml") String configFile; + @Inject + ReactiveRedisDataSource reactiveRedisDataSource; + @Produces DeviceConnectionInfo deviceConnectionInfo( final Cache cache, @@ -86,6 +92,9 @@ Cache cache( final String cacheBackend = System.getProperty("cache.backend"); LOG.info("######################### Cache Backend: {}", cacheBackend); + if (true) { + return new RedisQuarkusCache(reactiveRedisDataSource); + } if ("redis".equalsIgnoreCase(cacheBackend)) { LOG.info("Creating a new REDIS cache."); final var p = new RedisRemoteConfigurationProperties(); 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..f7c4d2f115 --- /dev/null +++ b/tests/src/test/resources/commandrouter/redis-cache/application.yml @@ -0,0 +1,95 @@ +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: + serverList: "${hono.infinispan.host}:${hono.infinispan.port}" + 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: + redis: + hosts: "redis://redis:6379" + opentelemetry: + tracer: + exporter: + otlp: + endpoint: "${otel-collector.endpoint}" + log: + console: + color: true + level: INFO + min-level: TRACE + category: + "io.quarkus.vertx.core.runtime": + level: DEBUG + vertx: + max-event-loop-execute-time: ${max.event-loop.execute-time} From f4c31b68ef05bd8682d7017a555592fe5659155e Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Tue, 3 Oct 2023 18:46:52 +0000 Subject: [PATCH 14/20] Minor refactoring to unify the names of the Redis cache implementations. --- .../{RedisQuarkusCache.java => QuarkusRedisCache.java} | 6 +++--- .../client/{RedisCache.java => VertxRedisCache.java} | 10 +++++----- .../app/DeviceConnectionInfoProducer.java | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) rename client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/{RedisQuarkusCache.java => QuarkusRedisCache.java} (96%) rename client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/{RedisCache.java => VertxRedisCache.java} (97%) diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisQuarkusCache.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/QuarkusRedisCache.java similarity index 96% rename from client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisQuarkusCache.java rename to client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/QuarkusRedisCache.java index 34585fe1d9..c623b84643 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisQuarkusCache.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/QuarkusRedisCache.java @@ -32,8 +32,8 @@ * An implementation of the Redis device connection class using the Quarkus Redis client library. */ //@javax.enterprise.context.ApplicationScoped -public class RedisQuarkusCache implements Cache { - private static final Logger LOG = LoggerFactory.getLogger(RedisCache.class); +public class QuarkusRedisCache implements Cache { + private static final Logger LOG = LoggerFactory.getLogger(QuarkusRedisCache.class); //private final RedisAPI redisApi; private final ReactiveRedisDataSource reactiveRedisDataSource; @@ -43,7 +43,7 @@ public class RedisQuarkusCache implements Cache { * TODO. * @param reactiveRedisDataSource TODO */ - public RedisQuarkusCache(final ReactiveRedisDataSource reactiveRedisDataSource) { + public QuarkusRedisCache(final ReactiveRedisDataSource reactiveRedisDataSource) { //this.redisApi = redisApi; this.reactiveRedisDataSource = reactiveRedisDataSource; valueCommands = reactiveRedisDataSource.value(String.class, String.class); diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java similarity index 97% rename from client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java rename to client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java index 413b977267..005634585a 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCache.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java @@ -42,9 +42,9 @@ /** * TODO. */ -public class RedisCache implements Cache, Lifecycle { +public class VertxRedisCache implements Cache, Lifecycle { - private static final Logger LOG = LoggerFactory.getLogger(RedisCache.class); + private static final Logger LOG = LoggerFactory.getLogger(VertxRedisCache.class); private static final int MAX_RECONNECT_RETRIES = 16; @@ -61,7 +61,7 @@ public class RedisCache implements Cache, Lifecycle { * @param vertx TODO. * @param properties TODO. */ - private RedisCache(final Vertx vertx, final RedisRemoteConfigurationProperties properties) { + private VertxRedisCache(final Vertx vertx, final RedisRemoteConfigurationProperties properties) { Objects.requireNonNull(vertx); Objects.requireNonNull(properties); @@ -83,13 +83,13 @@ private RedisCache(final Vertx vertx, final RedisRemoteConfigurationProperties p * * @return TODO. */ - public static RedisCache from( + public static VertxRedisCache from( final Vertx vertx, final RedisRemoteConfigurationProperties properties) { Objects.requireNonNull(vertx); Objects.requireNonNull(properties); - return new RedisCache(vertx, properties); + return new VertxRedisCache(vertx, properties); } @Override 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 6fd06d6c31..366cd6aa30 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 @@ -37,9 +37,9 @@ 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.RedisCache; -import org.eclipse.hono.deviceconnection.redis.client.RedisQuarkusCache; +import org.eclipse.hono.deviceconnection.redis.client.QuarkusRedisCache; import org.eclipse.hono.deviceconnection.redis.client.RedisRemoteConfigurationProperties; +import org.eclipse.hono.deviceconnection.redis.client.VertxRedisCache; import org.eclipse.hono.util.Strings; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; @@ -93,13 +93,13 @@ Cache cache( final String cacheBackend = System.getProperty("cache.backend"); LOG.info("######################### Cache Backend: {}", cacheBackend); if (true) { - return new RedisQuarkusCache(reactiveRedisDataSource); + return new QuarkusRedisCache(reactiveRedisDataSource); } if ("redis".equalsIgnoreCase(cacheBackend)) { LOG.info("Creating a new REDIS cache."); final var p = new RedisRemoteConfigurationProperties(); p.setConnectionString("redis://redis:6379"); - return RedisCache.from(vertx, p); + return VertxRedisCache.from(vertx, p); } else if (Strings.isNullOrEmpty(infinispanCacheConfig.getServerList())) { LOG.info("configuring embedded cache"); return new EmbeddedCache<>( From 372c9f4f207903bfc796ff5411091f812a63252d Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Sat, 2 Mar 2024 01:29:16 +0000 Subject: [PATCH 15/20] Baby steps. Now using the quarkus vertx redis API that is injected by the quarkus-redis-client extension and not manually managing connections. Minor cleanup based on PR comments. Experiment with configuration. --- client-device-connection-infinispan/pom.xml | 7 +- client-device-connection-redis/pom.xml | 3 +- .../RedisRemoteConfigurationOptions.java | 8 + .../RedisRemoteConfigurationProperties.java | 22 ++ .../redis/client/VertxRedisCache.java | 319 ++++++------------ .../src/test/resources/logback-test.xml | 2 +- .../app/DeviceConnectionInfoProducer.java | 46 +-- tests/pom.xml | 17 + .../commandrouter/redis-cache/application.yml | 15 +- 9 files changed, 182 insertions(+), 257 deletions(-) diff --git a/client-device-connection-infinispan/pom.xml b/client-device-connection-infinispan/pom.xml index e47ae16da3..ea843c2d3d 100644 --- a/client-device-connection-infinispan/pom.xml +++ b/client-device-connection-infinispan/pom.xml @@ -20,7 +20,6 @@ 2.5.0-SNAPSHOT ../bom - client-device-connection-infinispan Hotrod Device Connection client @@ -32,7 +31,7 @@ client-device-connection - + org.eclipse.hono hono-legal @@ -76,7 +75,7 @@ hono-client-common - + io.vertx vertx-core @@ -86,7 +85,7 @@ guava - + io.smallrye.config smallrye-config-core diff --git a/client-device-connection-redis/pom.xml b/client-device-connection-redis/pom.xml index 0d85739cce..01b6471bd3 100644 --- a/client-device-connection-redis/pom.xml +++ b/client-device-connection-redis/pom.xml @@ -20,7 +20,6 @@ 2.5.0-SNAPSHOT ../bom - client-device-connection-redis Redis Device Connection client @@ -37,7 +36,7 @@ quarkus-redis-client - + org.eclipse.hono hono-legal diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationOptions.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationOptions.java index 60bd96971a..376895943e 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationOptions.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationOptions.java @@ -14,6 +14,9 @@ package org.eclipse.hono.deviceconnection.redis.client; +import java.util.List; +import java.util.Optional; + import io.smallrye.config.ConfigMapping; import io.smallrye.config.ConfigMapping.NamingStrategy; //import io.smallrye.config.WithDefault; @@ -24,4 +27,9 @@ */ @ConfigMapping(prefix = "hono.cache.redis", namingStrategy = NamingStrategy.VERBATIM) public interface RedisRemoteConfigurationOptions { + /** + * TODO. + * @return TODO + */ + Optional> hosts(); } diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationProperties.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationProperties.java index 0cc56f33f9..9f698b7662 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationProperties.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationProperties.java @@ -14,10 +14,32 @@ package org.eclipse.hono.deviceconnection.redis.client; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.vertx.redis.client.RedisOptions; /** * TODO. */ public class RedisRemoteConfigurationProperties extends RedisOptions { + + private static final Logger LOG = LoggerFactory.getLogger(RedisRemoteConfigurationProperties.class); + + /** + * TODO. + */ + public RedisRemoteConfigurationProperties() { + super(); + } + + /** + * TODO. + * @param options TODO. + */ + public RedisRemoteConfigurationProperties(final RedisRemoteConfigurationOptions options) { + super(); + LOG.info("Setting Redis hosts configuration to {}", options.hosts()); + options.hosts().ifPresent(this::setEndpoints); + } } diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java index 005634585a..13482f7ab2 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java @@ -22,21 +22,15 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; 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.CompositeFuture; import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; -import io.vertx.redis.client.Redis; import io.vertx.redis.client.RedisAPI; -import io.vertx.redis.client.RedisConnection; import io.vertx.redis.client.Response; /** @@ -46,293 +40,172 @@ public class VertxRedisCache implements Cache, Lifecycle { private static final Logger LOG = LoggerFactory.getLogger(VertxRedisCache.class); - private static final int MAX_RECONNECT_RETRIES = 16; - - private final Vertx vertx; - private final RedisRemoteConfigurationProperties properties; - - private Redis redis; - private RedisConnection client; - private final AtomicBoolean CONNECTING = new AtomicBoolean(); + private final RedisAPI api; /** * TODO. * - * @param vertx TODO. - * @param properties TODO. + * @param api TODO. */ - private VertxRedisCache(final Vertx vertx, final RedisRemoteConfigurationProperties properties) { - - Objects.requireNonNull(vertx); - Objects.requireNonNull(properties); - - this.vertx = vertx; - this.properties = properties; - - LOG.info("Initializing REDIS cache!"); - //createRedisClient() - // .onSuccess( c -> LOG.info("Connected to Redis")) - // .onFailure( t -> LOG.error("Could not connect to Redis", t)); + private VertxRedisCache(final RedisAPI api) { + Objects.requireNonNull(api); + this.api = api; } /** * TODO. * - * @param vertx TODO. - * @param properties TODO. - * + * @param api TODO. * @return TODO. */ - public static VertxRedisCache from( - final Vertx vertx, final RedisRemoteConfigurationProperties properties) { - - Objects.requireNonNull(vertx); - Objects.requireNonNull(properties); - - return new VertxRedisCache(vertx, properties); + public static VertxRedisCache from(final RedisAPI api) { + LOG.info("VREDIS: creating cache with api: {}", api); + Objects.requireNonNull(api); + return new VertxRedisCache(api); } @Override public Future start() { LOG.info("VREDIS: starting cache"); - return createRedisClient() - .flatMap(c -> Future.succeededFuture()); - /* - final Promise promise = Promise.promise(); - createRedisClient() - .onSuccess(c -> promise.complete()) - .onFailure(promise::fail); - return promise.future(); - */ - /* - redis = Redis.createClient(vertx, properties); - redis.connect() - .onSuccess(c -> client = c) - .onFailure(promise::fail); - return promise.future(); - */ + return checkForCacheAvailability().mapEmpty(); } @Override public Future stop() { LOG.info("VREDIS: stopping cache"); - redis.close(); + api.close(); return Future.succeededFuture(); } - /* - * Will create a redis client and set up a reconnect handler when there is an exception in the connection. - */ - private Future createRedisClient() { - final Promise promise = Promise.promise(); - - // make sure to invalidate old connection if present - if (redis != null) { - redis.close(); - } - - if (CONNECTING.compareAndSet(false, true)) { - redis = Redis.createClient(vertx, properties); - redis.connect() - .onSuccess(conn -> { - client = conn; - - // make sure the client is reconnected on error - // eg, the underlying TCP connection is closed but the client side doesn't know it yet - // the client tries to use the staled connection to talk to server. An exceptions will be raised - conn.exceptionHandler(e -> attemptReconnect(0)); - - // make sure the client is reconnected on connection close - // eg, the underlying TCP connection is closed with normal 4-Way-Handshake - // this handler will be notified instantly - conn.endHandler(placeHolder -> attemptReconnect(0)); - - // allow further processing - promise.complete(conn); - CONNECTING.set(false); - }).onFailure(t -> { - promise.fail(t); - CONNECTING.set(false); - }); - } else { - promise.complete(); - } - - return promise.future(); - } - - private void attemptReconnect(final int retry) { - if (retry > MAX_RECONNECT_RETRIES) { - // we should stop now, as there's nothing we can do. - CONNECTING.set(false); - } else { - // retry with backoff up to 10240 ms - final long backoff = (long) (Math.pow(2, Math.min(retry, 10)) * 10); - - vertx.setTimer(backoff, timer -> createRedisClient().onFailure(t -> attemptReconnect(retry + 1))); - } - } - @Override public Future checkForCacheAvailability() { - LOG.info("VREDIS: checking for cache availability"); + LOG.info("VREDIS: checking for cache availability using api: {}", api); - Objects.requireNonNull(client); + Objects.requireNonNull(api); - return redis.connect().flatMap(connection -> { - final RedisAPI api = RedisAPI.api(connection); - return api.ping(List.of()) - .eventually(ignored -> connection.close()) - .mapEmpty(); - }); + 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(client); + Objects.requireNonNull(api); - return redis.connect().flatMap(connection -> { - final RedisAPI api = RedisAPI.api(connection); - return api.set(List.of(String.valueOf(key), String.valueOf(value))) - .eventually(ignored -> connection.close()) - .mapEmpty(); - }); + 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(client); + Objects.requireNonNull(api); - return redis.connect().flatMap(connection -> { - final RedisAPI api = RedisAPI.api(connection); - 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) - .eventually(ignored -> connection.close()) - .mapEmpty(); - }); + 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(client); + Objects.requireNonNull(api); - return redis.connect().flatMap(connection -> { - final RedisAPI api = RedisAPI.api(connection); - final List keyValues = new ArrayList<>(data.size() * 2); - data.forEach((k, v) -> { - keyValues.add(k); - keyValues.add(v); - }); - return api.mset(keyValues) - .eventually(ignored -> connection.close()) - .mapEmpty(); + 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) { + public Future putAll(final Map data, final long lifespan, + final TimeUnit lifespanUnit) { LOG.info("VREDIS: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); - Objects.requireNonNull(client); + Objects.requireNonNull(api); final long millis = lifespanUnit.toMillis(lifespan); - return redis.connect().flatMap(connection -> { - final RedisAPI api = RedisAPI.api(connection); - return api.multi() - .flatMap(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 CompositeFuture.all(Collections.unmodifiableList(futures)); - }) - .flatMap(ignored -> api.exec()) - // null reply means transaction aborted - .map(Objects::nonNull) - .eventually(ignored -> connection.close()) - .mapEmpty(); - }); + 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(client); + Objects.requireNonNull(api); - return redis.connect().flatMap(connection -> { - final RedisAPI api = RedisAPI.api(connection); - return api.get(String.valueOf(key)) - .flatMap(value -> Future.succeededFuture(String.valueOf(value))) - .eventually(ignored -> connection.close()); - }); + 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(client); - - return redis.connect().flatMap(connection -> { - final RedisAPI api = RedisAPI.api(connection); - return api.watch(List.of(String.valueOf(key))) - .flatMap(ignored -> api.get(String.valueOf(key))) - .flatMap(response -> { - if (response == null) { - // key does not exist - return Future.succeededFuture(false); - } - if (String.valueOf(response).equals(value)) { - return api.multi() - .flatMap(ignored -> api.del(List.of(String.valueOf(key)))) - .flatMap(ignored -> api.exec()) - // null reply means transaction aborted - .map(Objects::nonNull); - } else { - return Future.succeededFuture(false); - } - }) - .eventually(ignored -> connection.close()); - }); + 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(client); - - return redis.connect().flatMap(connection -> { - final RedisAPI api = RedisAPI.api(connection); - final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); - keyList.forEach(i -> LOG.info("VREDIS: Key: {}", i)); - final Map result = new HashMap<>(keyList.size()); - return api.mget(keyList) - .flatMap(values -> { - LOG.info("VREDIS: Got {} items back...", values.stream().toList().size()); - values.forEach(i -> { - LOG.info("Iterating through result list: {}", 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()); + Objects.requireNonNull(api); + + final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); + keyList.forEach(i -> LOG.info("VREDIS: Key: {}", i)); + final Map result = new HashMap<>(keyList.size()); + return api.mget(keyList) + .compose(values -> { + LOG.info("VREDIS: Got {} items back...", values.stream().toList().size()); + values.forEach(i -> { + LOG.info("Iterating through result list: {}", 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(); } - }); - return Future.succeededFuture(result); - }) - .eventually(ignored -> connection.close()); - }); + } catch (Exception e) { + LOG.info(" - got exception {}", e.getMessage()); + } + }); + return Future.succeededFuture(result); + }); } } diff --git a/client-device-connection-redis/src/test/resources/logback-test.xml b/client-device-connection-redis/src/test/resources/logback-test.xml index 0da04c2e0b..fb0c54d42d 100644 --- a/client-device-connection-redis/src/test/resources/logback-test.xml +++ b/client-device-connection-redis/src/test/resources/logback-test.xml @@ -31,7 +31,7 @@ - + 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 6496da3c0b..a525ae6376 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 @@ -32,7 +32,7 @@ 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.QuarkusRedisCache; +import org.eclipse.hono.deviceconnection.redis.client.RedisRemoteConfigurationOptions; import org.eclipse.hono.deviceconnection.redis.client.RedisRemoteConfigurationProperties; import org.eclipse.hono.deviceconnection.redis.client.VertxRedisCache; import org.eclipse.hono.util.Strings; @@ -45,9 +45,9 @@ import org.slf4j.LoggerFactory; import io.opentracing.Tracer; -import io.quarkus.redis.datasource.ReactiveRedisDataSource; 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.Inject; @@ -68,7 +68,7 @@ public class DeviceConnectionInfoProducer { String configFile; @Inject - ReactiveRedisDataSource reactiveRedisDataSource; + RedisAPI redisApi; @Produces DeviceConnectionInfo deviceConnectionInfo( @@ -84,34 +84,42 @@ Cache cache( @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 RedisRemoteConfigurationOptions redisCacheConfigurationOptions + ) { final var commonCacheConfig = new CommonCacheConfig(commonCacheOptions); final var infinispanCacheConfig = new InfinispanRemoteConfigurationProperties(remoteCacheConfigurationOptions); + final var redisCacheConfig = new RedisRemoteConfigurationProperties(redisCacheConfigurationOptions); final String cacheBackend = System.getProperty("cache.backend"); LOG.info("######################### Cache Backend: {}", cacheBackend); - if (true) { - return new QuarkusRedisCache(reactiveRedisDataSource); - } - if ("redis".equalsIgnoreCase(cacheBackend)) { - LOG.info("Creating a new REDIS cache."); - final var p = new RedisRemoteConfigurationProperties(); - p.setConnectionString("redis://redis:6379"); - return VertxRedisCache.from(vertx, p); - } else if (Strings.isNullOrEmpty(infinispanCacheConfig.getServerList())) { - LOG.info("configuring embedded cache"); - return new EmbeddedCache<>( - vertx, - embeddedCacheManager(commonCacheConfig), - commonCacheConfig.getCacheName()); - } else { + //if (true) { + // return new QuarkusRedisCache(reactiveRedisDataSource); + //} + + if (!Strings.isNullOrEmpty(infinispanCacheConfig.getServerList())) { LOG.info("configuring remote cache"); return HotrodCache.from( vertx, infinispanCacheConfig, commonCacheConfig); } + + if (!redisCacheConfig.getEndpoints().isEmpty()) { + LOG.info("Creating a new REDIS cache using {}", redisApi); + redisCacheConfig.getEndpoints().forEach(i -> LOG.info(" - {}", i)); + //final var p = new RedisRemoteConfigurationProperties(); + //p.setConnectionString("redis://redis:6379"); + return VertxRedisCache.from(redisApi); + } + + LOG.info("configuring embedded cache"); + return new EmbeddedCache<>( + vertx, + embeddedCacheManager(commonCacheConfig), + commonCacheConfig.getCacheName()); } private EmbeddedCacheManager embeddedCacheManager(final CommonCacheConfig cacheConfig) { diff --git a/tests/pom.xml b/tests/pom.xml index 9b227c45d1..db0196c9d9 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -88,6 +88,7 @@ hono-service-command-router 300000000 ${default.java-options} + false @@ -464,6 +465,22 @@ 350000000 + + + redis_cache + + + hono.commandrouting.cache + redis + + + + true + redis-cache + 350000000 + + + amqp diff --git a/tests/src/test/resources/commandrouter/redis-cache/application.yml b/tests/src/test/resources/commandrouter/redis-cache/application.yml index f7c4d2f115..c1f55b530a 100644 --- a/tests/src/test/resources/commandrouter/redis-cache/application.yml +++ b/tests/src/test/resources/commandrouter/redis-cache/application.yml @@ -15,7 +15,7 @@ hono: insecurePortBindAddress: "0.0.0.0" cache: redis: - serverList: "${hono.infinispan.host}:${hono.infinispan.port}" + hosts: "redis://redis:6379" messaging: name: 'Hono Command Router' host: "${hono.amqp-network.host}" @@ -78,14 +78,13 @@ hono: quarkus: redis: hosts: "redis://redis:6379" - opentelemetry: - tracer: - exporter: - otlp: - endpoint: "${otel-collector.endpoint}" + otel: + exporter: + otlp: + endpoint: "${otel-collector.endpoint}" + console: + color: true log: - console: - color: true level: INFO min-level: TRACE category: From c84ce95eac217547d6163a562ccccc8b44c69e51 Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Sat, 2 Mar 2024 02:38:53 +0000 Subject: [PATCH 16/20] Add config relocate interceptor that redirects requests for quarkus.redis config values to hono.commandRouter.cache.redis. --- .../services/io.smallrye.config.ConfigSourceInterceptor | 1 + .../test/resources/commandrouter/redis-cache/application.yml | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor diff --git a/client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor b/client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor new file mode 100644 index 0000000000..42b88254ce --- /dev/null +++ b/client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor @@ -0,0 +1 @@ +org.eclipse.hono.deviceconnection.redis.client.RelocateRedisConfigInterceptor \ No newline at end of file diff --git a/tests/src/test/resources/commandrouter/redis-cache/application.yml b/tests/src/test/resources/commandrouter/redis-cache/application.yml index c1f55b530a..7c5eada006 100644 --- a/tests/src/test/resources/commandrouter/redis-cache/application.yml +++ b/tests/src/test/resources/commandrouter/redis-cache/application.yml @@ -76,8 +76,6 @@ hono: delivery.timeout.ms: ${kafka-client.producer.delivery-timeout-ms} quarkus: - redis: - hosts: "redis://redis:6379" otel: exporter: otlp: From 0f40bf4400d7d56594a6712c21981ff7ad0b9f2d Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Sat, 2 Mar 2024 02:38:53 +0000 Subject: [PATCH 17/20] Add config relocate interceptor that redirects requests for quarkus.redis config values to hono.commandRouter.cache.redis. --- .../RelocateRedisConfigInterceptor.java | 20 +++++++++++++++++++ ...io.smallrye.config.ConfigSourceInterceptor | 1 + .../commandrouter/redis-cache/application.yml | 2 -- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RelocateRedisConfigInterceptor.java create mode 100644 client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RelocateRedisConfigInterceptor.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RelocateRedisConfigInterceptor.java new file mode 100644 index 0000000000..c4544c32c7 --- /dev/null +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RelocateRedisConfigInterceptor.java @@ -0,0 +1,20 @@ +/* +TODO + */ + +package org.eclipse.hono.deviceconnection.redis.client; + +import io.smallrye.config.RelocateConfigSourceInterceptor; + +/** + * TODO. + */ +public class RelocateRedisConfigInterceptor extends RelocateConfigSourceInterceptor { + /** + * TODO. + */ + public RelocateRedisConfigInterceptor() { + super(name -> name.startsWith("quarkus.redis") ? + name.replaceAll("quarkus\\.redis", "hono.commandRouter.cache.redis") : name); + } +} diff --git a/client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor b/client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor new file mode 100644 index 0000000000..42b88254ce --- /dev/null +++ b/client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor @@ -0,0 +1 @@ +org.eclipse.hono.deviceconnection.redis.client.RelocateRedisConfigInterceptor \ No newline at end of file diff --git a/tests/src/test/resources/commandrouter/redis-cache/application.yml b/tests/src/test/resources/commandrouter/redis-cache/application.yml index c1f55b530a..7c5eada006 100644 --- a/tests/src/test/resources/commandrouter/redis-cache/application.yml +++ b/tests/src/test/resources/commandrouter/redis-cache/application.yml @@ -76,8 +76,6 @@ hono: delivery.timeout.ms: ${kafka-client.producer.delivery-timeout-ms} quarkus: - redis: - hosts: "redis://redis:6379" otel: exporter: otlp: From b7320d776ebfe3b2deb77711493d1cef39bae311 Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Mon, 25 Mar 2024 02:47:42 +0000 Subject: [PATCH 18/20] Major refactoring, not using Quarkus Redis client anymore and instead copying the behavior of it into Hono. Integration Tests are now working for both embedded and redis cache configurations. Still some work to be done to enable metrics and health checks. --- client-device-connection-infinispan/pom.xml | 10 +- .../infinispan/client/HotrodCache.java | 2 +- ...emoteCacheQuarkusPropertyBindingTest.java} | 20 +- client-device-connection-redis/pom.xml | 8 +- ...txRedisCache.java => RedisCacheVertx.java} | 23 +- .../RedisRemoteConfigurationOptions.java | 35 --- .../RedisRemoteConfigurationProperties.java | 45 ---- .../RelocateRedisConfigInterceptor.java | 20 -- .../redis/client/config/NetConfig.java | 135 +++++++++++ .../redis/client/config/ProxyConfig.java | 47 ++++ .../redis/client/config/RedisConfig.java | 197 ++++++++++++++++ .../redis/client/config/TlsConfig.java | 214 ++++++++++++++++++ .../client/vertx/VertxRedisClientFactory.java | 153 +++++++++++++ ...io.smallrye.config.ConfigSourceInterceptor | 1 - .../RedisCacheQuarkus.java} | 6 +- client-device-connection/pom.xml | 3 +- ...CommonCacheQuarkusPropertyBindingTest.java | 39 ++++ .../test/resources/common-cache-options.yaml | 0 .../src/test/resources/logback-test.xml | 36 +++ services/command-router/pom.xml | 4 +- .../app/DeviceConnectionInfoProducer.java | 32 +-- 21 files changed, 858 insertions(+), 172 deletions(-) rename client-device-connection-infinispan/src/test/java/org/eclipse/hono/deviceconnection/infinispan/client/{QuarkusPropertyBindingTest.java => RemoteCacheQuarkusPropertyBindingTest.java} (83%) rename client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/{VertxRedisCache.java => RedisCacheVertx.java} (89%) delete mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationOptions.java delete mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationProperties.java delete mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RelocateRedisConfigInterceptor.java create mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/NetConfig.java create mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/ProxyConfig.java create mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/RedisConfig.java create mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/config/TlsConfig.java create mode 100644 client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/vertx/VertxRedisClientFactory.java delete mode 100644 client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor rename client-device-connection-redis/src/{main/java/org/eclipse/hono/deviceconnection/redis/client/QuarkusRedisCache.java => scratch/RedisCacheQuarkus.java} (96%) create mode 100644 client-device-connection/src/test/java/org/eclipse/hono/deviceconnection/common/CommonCacheQuarkusPropertyBindingTest.java rename {client-device-connection-infinispan => client-device-connection}/src/test/resources/common-cache-options.yaml (100%) create mode 100644 client-device-connection/src/test/resources/logback-test.xml diff --git a/client-device-connection-infinispan/pom.xml b/client-device-connection-infinispan/pom.xml index 6854e664f2..c4782fb300 100644 --- a/client-device-connection-infinispan/pom.xml +++ b/client-device-connection-infinispan/pom.xml @@ -26,11 +26,6 @@ A Hotrod based client for accessing device connection information in an Infinispan data grid. - - org.eclipse.hono - client-device-connection - - org.eclipse.hono hono-legal @@ -70,11 +65,16 @@ org.eclipse.hono hono-core + + org.eclipse.hono + client-device-connection + org.eclipse.hono hono-client-common + io.vertx vertx-core 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 754554f0c7..471207efaf 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 @@ -175,7 +175,7 @@ protected void postCacheAccess(final AsyncResult cacheOperationResult) { */ @Override public Future checkForCacheAvailability() { - + LOG.info("HOTROD: 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 83% 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 9cbed5b014..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 @@ -19,32 +19,16 @@ import javax.security.sasl.Sasl; -import org.eclipse.hono.deviceconnection.common.CommonCacheConfig; -import org.eclipse.hono.deviceconnection.common.CommonCacheOptions; import org.eclipse.hono.test.ConfigMappingSupport; import org.infinispan.client.hotrod.configuration.ClusterConfiguration; import org.infinispan.client.hotrod.configuration.Configuration; 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 index 01b6471bd3..25a09d01ad 100644 --- a/client-device-connection-redis/pom.xml +++ b/client-device-connection-redis/pom.xml @@ -17,7 +17,7 @@ org.eclipse.hono hono-bom - 2.5.0-SNAPSHOT + 2.6.0-SNAPSHOT ../bom client-device-connection-redis @@ -33,7 +33,7 @@ io.quarkus - quarkus-redis-client + quarkus-vertx @@ -59,6 +59,10 @@ io.vertx vertx-core + + io.vertx + vertx-redis-client + diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCacheVertx.java similarity index 89% rename from client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java rename to client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCacheVertx.java index 13482f7ab2..9777fdb181 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/VertxRedisCache.java +++ b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisCacheVertx.java @@ -36,9 +36,9 @@ /** * TODO. */ -public class VertxRedisCache implements Cache, Lifecycle { +public class RedisCacheVertx implements Cache, Lifecycle { - private static final Logger LOG = LoggerFactory.getLogger(VertxRedisCache.class); + private static final Logger LOG = LoggerFactory.getLogger(RedisCacheVertx.class); private final RedisAPI api; @@ -47,7 +47,7 @@ public class VertxRedisCache implements Cache, Lifecycle { * * @param api TODO. */ - private VertxRedisCache(final RedisAPI api) { + private RedisCacheVertx(final RedisAPI api) { Objects.requireNonNull(api); this.api = api; } @@ -58,29 +58,27 @@ private VertxRedisCache(final RedisAPI api) { * @param api TODO. * @return TODO. */ - public static VertxRedisCache from(final RedisAPI api) { - LOG.info("VREDIS: creating cache with api: {}", api); + public static RedisCacheVertx from(final RedisAPI api) { Objects.requireNonNull(api); - return new VertxRedisCache(api); + return new RedisCacheVertx(api); } @Override public Future start() { - LOG.info("VREDIS: starting cache"); + LOG.info("VREDIS: start()"); return checkForCacheAvailability().mapEmpty(); } @Override public Future stop() { - LOG.info("VREDIS: stopping cache"); + LOG.info("VREDIS: stop()"); api.close(); return Future.succeededFuture(); } @Override public Future checkForCacheAvailability() { - LOG.info("VREDIS: checking for cache availability using api: {}", api); - + LOG.info("VREDIS: checkForCacheAvailability()"); Objects.requireNonNull(api); return api.ping(List.of()) @@ -184,17 +182,14 @@ public Future remove(final String key, final String value) { @Override public Future> getAll(final Set keys) { - LOG.info("VREDIS: getAll {}", keys.size()); + LOG.info("VREDIS: getAll ({})", keys.size()); Objects.requireNonNull(api); final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); - keyList.forEach(i -> LOG.info("VREDIS: Key: {}", i)); final Map result = new HashMap<>(keyList.size()); return api.mget(keyList) .compose(values -> { - LOG.info("VREDIS: Got {} items back...", values.stream().toList().size()); values.forEach(i -> { - LOG.info("Iterating through result list: {}", 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()); diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationOptions.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationOptions.java deleted file mode 100644 index 376895943e..0000000000 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationOptions.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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.List; -import java.util.Optional; - -import io.smallrye.config.ConfigMapping; -import io.smallrye.config.ConfigMapping.NamingStrategy; -//import io.smallrye.config.WithDefault; - -/** - * Options for configuring a Redis connection to a remote cache. - * - */ -@ConfigMapping(prefix = "hono.cache.redis", namingStrategy = NamingStrategy.VERBATIM) -public interface RedisRemoteConfigurationOptions { - /** - * TODO. - * @return TODO - */ - Optional> hosts(); -} diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationProperties.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationProperties.java deleted file mode 100644 index 9f698b7662..0000000000 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RedisRemoteConfigurationProperties.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.vertx.redis.client.RedisOptions; - -/** - * TODO. - */ -public class RedisRemoteConfigurationProperties extends RedisOptions { - - private static final Logger LOG = LoggerFactory.getLogger(RedisRemoteConfigurationProperties.class); - - /** - * TODO. - */ - public RedisRemoteConfigurationProperties() { - super(); - } - - /** - * TODO. - * @param options TODO. - */ - public RedisRemoteConfigurationProperties(final RedisRemoteConfigurationOptions options) { - super(); - LOG.info("Setting Redis hosts configuration to {}", options.hosts()); - options.hosts().ifPresent(this::setEndpoints); - } -} diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RelocateRedisConfigInterceptor.java b/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RelocateRedisConfigInterceptor.java deleted file mode 100644 index c4544c32c7..0000000000 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/RelocateRedisConfigInterceptor.java +++ /dev/null @@ -1,20 +0,0 @@ -/* -TODO - */ - -package org.eclipse.hono.deviceconnection.redis.client; - -import io.smallrye.config.RelocateConfigSourceInterceptor; - -/** - * TODO. - */ -public class RelocateRedisConfigInterceptor extends RelocateConfigSourceInterceptor { - /** - * TODO. - */ - public RelocateRedisConfigInterceptor() { - super(name -> name.startsWith("quarkus.redis") ? - name.replaceAll("quarkus\\.redis", "hono.commandRouter.cache.redis") : name); - } -} 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/services/io.smallrye.config.ConfigSourceInterceptor b/client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor deleted file mode 100644 index 42b88254ce..0000000000 --- a/client-device-connection-redis/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor +++ /dev/null @@ -1 +0,0 @@ -org.eclipse.hono.deviceconnection.redis.client.RelocateRedisConfigInterceptor \ No newline at end of file diff --git a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/QuarkusRedisCache.java b/client-device-connection-redis/src/scratch/RedisCacheQuarkus.java similarity index 96% rename from client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/QuarkusRedisCache.java rename to client-device-connection-redis/src/scratch/RedisCacheQuarkus.java index c623b84643..5a0401d356 100644 --- a/client-device-connection-redis/src/main/java/org/eclipse/hono/deviceconnection/redis/client/QuarkusRedisCache.java +++ b/client-device-connection-redis/src/scratch/RedisCacheQuarkus.java @@ -32,8 +32,8 @@ * An implementation of the Redis device connection class using the Quarkus Redis client library. */ //@javax.enterprise.context.ApplicationScoped -public class QuarkusRedisCache implements Cache { - private static final Logger LOG = LoggerFactory.getLogger(QuarkusRedisCache.class); +public class RedisCacheQuarkus implements Cache { + private static final Logger LOG = LoggerFactory.getLogger(RedisCacheQuarkus.class); //private final RedisAPI redisApi; private final ReactiveRedisDataSource reactiveRedisDataSource; @@ -43,7 +43,7 @@ public class QuarkusRedisCache implements Cache { * TODO. * @param reactiveRedisDataSource TODO */ - public QuarkusRedisCache(final ReactiveRedisDataSource reactiveRedisDataSource) { + public RedisCacheQuarkus(final ReactiveRedisDataSource reactiveRedisDataSource) { //this.redisApi = redisApi; this.reactiveRedisDataSource = reactiveRedisDataSource; valueCommands = reactiveRedisDataSource.value(String.class, String.class); diff --git a/client-device-connection/pom.xml b/client-device-connection/pom.xml index f21c437496..89aebf0933 100644 --- a/client-device-connection/pom.xml +++ b/client-device-connection/pom.xml @@ -17,10 +17,9 @@ org.eclipse.hono hono-bom - 2.5.0-SNAPSHOT + 2.6.0-SNAPSHOT ../bom - client-device-connection Device Connection client 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..742aabd489 --- /dev/null +++ b/client-device-connection/src/test/java/org/eclipse/hono/deviceconnection/common/CommonCacheQuarkusPropertyBindingTest.java @@ -0,0 +1,39 @@ +/** + * 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/services/command-router/pom.xml b/services/command-router/pom.xml index eda625725d..fb530d9aab 100644 --- a/services/command-router/pom.xml +++ b/services/command-router/pom.xml @@ -24,12 +24,10 @@ A Quarkus based implementation of Hono's Command Router API that is using Infinispan for storing data. - org.eclipse.hono client-device-connection-infinispan 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 a525ae6376..291d794d67 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 @@ -32,9 +32,9 @@ 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.RedisRemoteConfigurationOptions; -import org.eclipse.hono.deviceconnection.redis.client.RedisRemoteConfigurationProperties; -import org.eclipse.hono.deviceconnection.redis.client.VertxRedisCache; +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; @@ -50,7 +50,6 @@ import io.vertx.redis.client.RedisAPI; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; -import jakarta.inject.Inject; import jakarta.inject.Singleton; /** @@ -67,9 +66,6 @@ public class DeviceConnectionInfoProducer { @ConfigProperty(name = "hono.commandRouter.cache.embedded.configurationFile", defaultValue = "/etc/hono/cache-config.xml") String configFile; - @Inject - RedisAPI redisApi; - @Produces DeviceConnectionInfo deviceConnectionInfo( final Cache cache, @@ -86,36 +82,26 @@ Cache cache( @ConfigMapping(prefix = "hono.commandRouter.cache.remote") final InfinispanRemoteConfigurationOptions remoteCacheConfigurationOptions, @ConfigMapping(prefix = "hono.commandRouter.cache.redis") - final RedisRemoteConfigurationOptions redisCacheConfigurationOptions + final RedisConfig redisCacheConfiguration ) { final var commonCacheConfig = new CommonCacheConfig(commonCacheOptions); final var infinispanCacheConfig = new InfinispanRemoteConfigurationProperties(remoteCacheConfigurationOptions); - final var redisCacheConfig = new RedisRemoteConfigurationProperties(redisCacheConfigurationOptions); - - final String cacheBackend = System.getProperty("cache.backend"); - LOG.info("######################### Cache Backend: {}", cacheBackend); - //if (true) { - // return new QuarkusRedisCache(reactiveRedisDataSource); - //} if (!Strings.isNullOrEmpty(infinispanCacheConfig.getServerList())) { - LOG.info("configuring remote cache"); + LOG.info("configuring cache: hotrod ({})", infinispanCacheConfig); return HotrodCache.from( vertx, infinispanCacheConfig, commonCacheConfig); } - if (!redisCacheConfig.getEndpoints().isEmpty()) { - LOG.info("Creating a new REDIS cache using {}", redisApi); - redisCacheConfig.getEndpoints().forEach(i -> LOG.info(" - {}", i)); - //final var p = new RedisRemoteConfigurationProperties(); - //p.setConnectionString("redis://redis:6379"); - return VertxRedisCache.from(redisApi); + if (redisCacheConfiguration.hosts().isPresent()) { + LOG.info("=== REDIS === configuring cache: redis ({})", redisCacheConfiguration.hosts().get()); + return RedisCacheVertx.from(RedisAPI.api(VertxRedisClientFactory.create(vertx, redisCacheConfiguration))); } - LOG.info("configuring embedded cache"); + LOG.info("configuring cache: embedded ({})", commonCacheConfig); return new EmbeddedCache<>( vertx, embeddedCacheManager(commonCacheConfig), From 37c555bfcbe3f1b4811abf17fef147400c746ddd Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Mon, 25 Mar 2024 16:50:00 +0000 Subject: [PATCH 19/20] Minor cleanup of PR --- bom/pom.xml | 11 -- client-device-connection-infinispan/pom.xml | 1 - .../infinispan/client/BasicCache.java | 16 +-- .../infinispan/client/HotrodCache.java | 1 - .../src/scratch/RedisCacheQuarkus.java | 133 ------------------ client-device-connection/pom.xml | 7 +- ...CommonCacheQuarkusPropertyBindingTest.java | 1 - .../app/DeviceConnectionInfoProducer.java | 6 +- tests/pom.xml | 1 - 9 files changed, 7 insertions(+), 170 deletions(-) delete mode 100644 client-device-connection-redis/src/scratch/RedisCacheQuarkus.java diff --git a/bom/pom.xml b/bom/pom.xml index 051f632aa2..d422a7869d 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -255,17 +255,6 @@ quarkus.vertx.max-event-loop-execute-time=${max.event-loop.execute-time:20000} - - io.quarkus - quarkus-redis-client - ${quarkus.platform.version} - - - io.quarkus - quarkus-ide-launcher - - - io.quarkus quarkus-vertx diff --git a/client-device-connection-infinispan/pom.xml b/client-device-connection-infinispan/pom.xml index c4782fb300..c951d639ef 100644 --- a/client-device-connection-infinispan/pom.xml +++ b/client-device-connection-infinispan/pom.xml @@ -74,7 +74,6 @@ hono-client-common - io.vertx vertx-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 aeed07c893..918c6b15e4 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 @@ -160,7 +160,6 @@ protected void postCacheAccess(final AsyncResult cacheOperationResult) { @Override public Future put(final K key, final V value) { - LOG.info("BASIC: put {}={}", key, value); Objects.requireNonNull(key); Objects.requireNonNull(value); @@ -169,7 +168,6 @@ public Future put(final K key, final V value) { @Override public Future put(final K key, final V value, final long lifespan, final TimeUnit lifespanUnit) { - LOG.info("BASIC: put {}={} ({} {})", key, value, lifespan, lifespanUnit); Objects.requireNonNull(key); Objects.requireNonNull(value); Objects.requireNonNull(lifespanUnit); @@ -179,7 +177,6 @@ public Future put(final K key, final V value, final long lifespan, final T @Override public Future putAll(final Map data) { - LOG.info("BASIC: putAll ({})", data.size()); Objects.requireNonNull(data); return withCache(aCache -> aCache.putAllAsync(data)); @@ -187,7 +184,6 @@ public Future putAll(final Map data) { @Override public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { - LOG.info("BASIC: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); Objects.requireNonNull(data); Objects.requireNonNull(lifespanUnit); @@ -196,7 +192,6 @@ public Future putAll(final Map data, final long @Override public Future remove(final K key, final V value) { - LOG.info("BASIC: remove {}={}", key, value); Objects.requireNonNull(key); Objects.requireNonNull(value); @@ -205,7 +200,6 @@ public Future remove(final K key, final V value) { @Override public Future get(final K key) { - LOG.info("BASIC: get {}", key); Objects.requireNonNull(key); return withCache(aCache -> aCache.getAsync(key)); @@ -213,17 +207,9 @@ public Future get(final K key) { @Override public Future> getAll(final Set keys) { - LOG.info("BASIC: getAll {}", keys.size()); Objects.requireNonNull(keys); - final LinkedList keyList = new LinkedList<>(keys.stream().map(String::valueOf).toList()); - keyList.forEach(i -> LOG.info("BASIC: Key: {}", i)); - final var result = withCache(aCache -> aCache.getAllAsync(keys)); - result.onSuccess(r -> { - LOG.info("BASIC: Got {} items back...", r.size()); - r.forEach((k, v) -> LOG.info("Iterating through result list: {}", v)); - }); - return result; + return withCache(aCache -> aCache.getAllAsync(keys)); } /** 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 471207efaf..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 @@ -175,7 +175,6 @@ protected void postCacheAccess(final AsyncResult cacheOperationResult) { */ @Override public Future checkForCacheAvailability() { - LOG.info("HOTROD: checkForCacheAvailability()"); if (isStarted()) { final ConnectionCheckResult lastResult = lastConnectionCheckResult; if (lastResult != null && !lastResult.isOlderThan(CACHED_CONNECTION_CHECK_RESULT_MAX_AGE)) { diff --git a/client-device-connection-redis/src/scratch/RedisCacheQuarkus.java b/client-device-connection-redis/src/scratch/RedisCacheQuarkus.java deleted file mode 100644 index 5a0401d356..0000000000 --- a/client-device-connection-redis/src/scratch/RedisCacheQuarkus.java +++ /dev/null @@ -1,133 +0,0 @@ -/* -TODO - */ - -package org.eclipse.hono.deviceconnection.redis.client; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; -import java.util.function.Function; - -import org.eclipse.hono.deviceconnection.common.Cache; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.quarkus.redis.datasource.ReactiveRedisDataSource; -import io.quarkus.redis.datasource.transactions.OptimisticLockingTransactionResult; -import io.quarkus.redis.datasource.transactions.ReactiveTransactionalRedisDataSource; -import io.quarkus.redis.datasource.transactions.TransactionResult; -import io.quarkus.redis.datasource.value.ReactiveTransactionalValueCommands; -import io.quarkus.redis.datasource.value.ReactiveValueCommands; -import io.quarkus.redis.datasource.value.SetArgs; -import io.smallrye.mutiny.Uni; -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; - -/** - * An implementation of the Redis device connection class using the Quarkus Redis client library. - */ -//@javax.enterprise.context.ApplicationScoped -public class RedisCacheQuarkus implements Cache { - private static final Logger LOG = LoggerFactory.getLogger(RedisCacheQuarkus.class); - - //private final RedisAPI redisApi; - private final ReactiveRedisDataSource reactiveRedisDataSource; - private final ReactiveValueCommands valueCommands; - - /** - * TODO. - * @param reactiveRedisDataSource TODO - */ - public RedisCacheQuarkus(final ReactiveRedisDataSource reactiveRedisDataSource) { - //this.redisApi = redisApi; - this.reactiveRedisDataSource = reactiveRedisDataSource; - valueCommands = reactiveRedisDataSource.value(String.class, String.class); - } - - - @Override - public Future checkForCacheAvailability() { - LOG.info("QREDIS: checking for cache availability"); - return Future.fromCompletionStage( - valueCommands.set("TEST_KEY", "TEST_VALUE") - .onItem().transform(setResult -> valueCommands.get("TEST_KEY")) - .onItem().transform(getResult -> new JsonObject()) - .subscribeAsCompletionStage()); - } - - @Override - public Future put(final String key, final String value) { - LOG.info("QREDIS: put {}={}", key, value); - return Future.fromCompletionStage(valueCommands.set(key, value).subscribeAsCompletionStage()); - } - - @Override - public Future put(final String key, final String value, final long lifespan, final TimeUnit lifespanUnit) { - LOG.info("QREDIS: put {}={} ({} {})", key, value, lifespan, lifespanUnit); - final long millis = lifespanUnit.toMillis(lifespan); - return Future.fromCompletionStage(valueCommands.set(key, value, new SetArgs().px(millis)).subscribeAsCompletionStage()); - } - - @Override - public Future putAll(final Map data) { - LOG.info("QREDIS: putAll ({})", data.size()); - return Future.fromCompletionStage(valueCommands.mset((Map) data).subscribeAsCompletionStage()); - } - - @Override - public Future putAll(final Map data, final long lifespan, final TimeUnit lifespanUnit) { - LOG.info("QREDIS: putAll ({}) ({} {})", data.size(), lifespan, lifespanUnit); - final long millis = lifespanUnit.toMillis(lifespan); - final Function> txBlock = ds -> { - final ReactiveTransactionalValueCommands txValueCommends = ds.value(String.class, String.class); - final List> unis = new ArrayList<>(); - data.forEach((k, v) -> { - unis.add(txValueCommends.set(k, v, new SetArgs().px(millis))); - }); - return Uni.join().all(unis).andFailFast().replaceWithVoid(); - }; - final Uni result = reactiveRedisDataSource.withTransaction(txBlock); - return Future.fromCompletionStage(result.onItem().transformToUni(txResult -> Uni.createFrom().voidItem()).subscribeAsCompletionStage()); - } - - @Override - public Future get(final String key) { - LOG.info("QREDIS: get {}", key); - return Future.fromCompletionStage(valueCommands.get(key).subscribeAsCompletionStage()); - } - - @Override - public Future remove(final String key, final String value) { - LOG.info("QREDIS: remove {}={}", key, value); - final Function> preTxBlock = ds -> { - return ds.value(String.class, String.class).get(key); - }; - final BiFunction> txBlock = (redisKeyValue, ds) -> { - if (value.equals(redisKeyValue)) { - return ds.key(String.class).del(key); - } else { - return ds.discard(); - } - }; - final Uni> result = reactiveRedisDataSource.withTransaction(preTxBlock, txBlock, key); - return Future.fromCompletionStage( - result.onItem().transformToUni(txResult -> Uni.createFrom().item(!txResult.isEmpty())).subscribeAsCompletionStage()); - } - - @Override - public Future> getAll(final Set keys) { - LOG.info("QREDIS: getAll {}", keys.size()); - return Future.fromCompletionStage( - valueCommands.mget(keys.toArray(new String[0])) - .onItem().transform(result -> { - result.values().removeIf(Objects::isNull); - return result; - }) - .subscribeAsCompletionStage()); - } -} diff --git a/client-device-connection/pom.xml b/client-device-connection/pom.xml index 89aebf0933..6a3848e74f 100644 --- a/client-device-connection/pom.xml +++ b/client-device-connection/pom.xml @@ -29,7 +29,6 @@ org.eclipse.hono hono-legal - org.eclipse.hono @@ -52,17 +51,17 @@ io.smallrye.config smallrye-config-core - + io.vertx vertx-core - + io.vertx vertx-health-check - + com.google.guava 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 index 742aabd489..753711f531 100644 --- 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 @@ -26,7 +26,6 @@ public class CommonCacheQuarkusPropertyBindingTest { @Test void testCommonCacheConfigurationPropertiesArePickedUp() { - final var commonCacheConfig = new CommonCacheConfig( ConfigMappingSupport.getConfigMapping( CommonCacheOptions.class, 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 291d794d67..398c9f86c8 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 @@ -89,7 +89,7 @@ Cache cache( final var infinispanCacheConfig = new InfinispanRemoteConfigurationProperties(remoteCacheConfigurationOptions); if (!Strings.isNullOrEmpty(infinispanCacheConfig.getServerList())) { - LOG.info("configuring cache: hotrod ({})", infinispanCacheConfig); + LOG.info("configuring remote cache"); return HotrodCache.from( vertx, infinispanCacheConfig, @@ -97,11 +97,11 @@ Cache cache( } if (redisCacheConfiguration.hosts().isPresent()) { - LOG.info("=== REDIS === configuring cache: redis ({})", redisCacheConfiguration.hosts().get()); + LOG.info("configuring redis cache"); return RedisCacheVertx.from(RedisAPI.api(VertxRedisClientFactory.create(vertx, redisCacheConfiguration))); } - LOG.info("configuring cache: embedded ({})", commonCacheConfig); + LOG.info("configuring embedded cache"); return new EmbeddedCache<>( vertx, embeddedCacheManager(commonCacheConfig), diff --git a/tests/pom.xml b/tests/pom.xml index d858e596bb..c1b956fa05 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -88,7 +88,6 @@ hono-service-command-router 300000000 ${default.java-options} - false From 0f60496c36831a82f5e41f454a04578c3421b770 Mon Sep 17 00:00:00 2001 From: Stefan Freyr Stefansson Date: Mon, 25 Mar 2024 17:03:13 +0000 Subject: [PATCH 20/20] Minor cleanup of PR --- .../deviceconnection/infinispan/client/BasicCache.java | 1 - .../src/test/resources/common-cache-options.yaml | 6 ------ .../commandrouter/app/DeviceConnectionInfoProducer.java | 7 +++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 client-device-connection-redis/src/test/resources/common-cache-options.yaml 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 918c6b15e4..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 @@ -14,7 +14,6 @@ package org.eclipse.hono.deviceconnection.infinispan.client; import java.net.HttpURLConnection; -import java.util.LinkedList; import java.util.Map; import java.util.Objects; import java.util.Optional; diff --git a/client-device-connection-redis/src/test/resources/common-cache-options.yaml b/client-device-connection-redis/src/test/resources/common-cache-options.yaml deleted file mode 100644 index edacdbfc7d..0000000000 --- a/client-device-connection-redis/src/test/resources/common-cache-options.yaml +++ /dev/null @@ -1,6 +0,0 @@ -hono: - cache: - common: - cacheName: "the-cache" - checkKey: "the-key" - checkValue: "the-value" 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 398c9f86c8..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; @@ -88,6 +89,12 @@ Cache cache( final var commonCacheConfig = new CommonCacheConfig(commonCacheOptions); final var infinispanCacheConfig = new InfinispanRemoteConfigurationProperties(remoteCacheConfigurationOptions); + 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(