All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.hono.client.command.ProtocolAdapterCommandConsumerFactoryImpl Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * Copyright (c) 2020, 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.client.command;

import java.net.HttpURLConnection;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;

import org.eclipse.hono.client.ClientErrorException;
import org.eclipse.hono.client.ServiceInvocationException;
import org.eclipse.hono.client.amqp.connection.ConnectionLifecycle;
import org.eclipse.hono.client.util.ServiceClient;
import org.eclipse.hono.util.Lifecycle;
import org.eclipse.hono.util.TenantConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.opentracing.SpanContext;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.healthchecks.HealthCheckHandler;
import io.vertx.ext.healthchecks.Status;

/**
 * A Protocol Adapter factory for creating consumers of command messages.
 * 

* This implementation uses the Command Router service and receives commands forwarded by the Command Router * on the internal command endpoint. */ public class ProtocolAdapterCommandConsumerFactoryImpl implements ProtocolAdapterCommandConsumerFactory, ServiceClient { private static final Logger LOG = LoggerFactory.getLogger(ProtocolAdapterCommandConsumerFactoryImpl.class); private static final AtomicInteger ADAPTER_INSTANCE_ID_COUNTER = new AtomicInteger(); private final Vertx vertx; private final int adapterInstanceIdCounterValue; private final String adapterName; private final CommandHandlers commandHandlers = new CommandHandlers(); private final CommandRouterClient commandRouterClient; private final List internalCommandConsumers = new ArrayList<>(); private final AtomicBoolean stopCalled = new AtomicBoolean(); private int maxTenantIdsPerRequest = 100; private KubernetesContainerInfoProvider kubernetesContainerInfoProvider = KubernetesContainerInfoProvider.getInstance(); private final List> internalCommandConsumerSuppliers = new ArrayList<>(); private HealthCheckHandler readinessHandler; /** * Identifier that has to be unique to this factory instance. * Will be used to represent the protocol adapter (verticle) instance that this factory instance is used in, * when registering command handlers with the command router service client. */ private String adapterInstanceId; private String startFailureMessage; /** * Creates a new factory. * * @param vertx The Vert.x instance to use. * @param commandRouterClient The client to use for accessing the command router service. * @param adapterName The name of the protocol adapter. * @throws NullPointerException if any of the parameters is {@code null}. */ public ProtocolAdapterCommandConsumerFactoryImpl(final Vertx vertx, final CommandRouterClient commandRouterClient, final String adapterName) { this.vertx = Objects.requireNonNull(vertx); this.commandRouterClient = Objects.requireNonNull(commandRouterClient); this.adapterName = Objects.requireNonNull(adapterName); this.adapterInstanceIdCounterValue = ADAPTER_INSTANCE_ID_COUNTER.getAndIncrement(); if (commandRouterClient instanceof ConnectionLifecycle) { ((ConnectionLifecycle) commandRouterClient).addReconnectListener(con -> reenableCommandRouting()); } } void setMaxTenantIdsPerRequest(final int count) { this.maxTenantIdsPerRequest = count; } void setKubernetesContainerInfoProvider(final KubernetesContainerInfoProvider kubernetesContainerInfoProvider) { this.kubernetesContainerInfoProvider = kubernetesContainerInfoProvider; } private void reenableCommandRouting() { final List tenantIds = commandHandlers.getCommandHandlers().stream() .map(CommandHandlerWrapper::getTenantId) .distinct() .toList(); int idx = 0; // re-enable routing of commands in chunks of tenant IDs while (idx < tenantIds.size()) { final int from = idx; final int to = from + Math.min(maxTenantIdsPerRequest, tenantIds.size() - idx); final List chunk = tenantIds.subList(from, to); commandRouterClient.enableCommandRouting(chunk, null); idx = to; } } /** * Registers the command consumer receiving commands on the internal command endpoint of the protocol adapter. * That is the endpoint that the Command Router forwards received commands to. *

* Note that this method needs to be called before invoking {@link #start()}. The {@link #start()} and * {@link #stop()} methods of this factory will invoke the corresponding methods on the internal command consumer. * * @param internalCommandConsumerSupplier Function that returns the consumer. Parameters are the adapter instance * id that identifies the internal command endpoint and the command handlers to choose from when handling * a received command. */ public void registerInternalCommandConsumer( final BiFunction internalCommandConsumerSupplier) { this.internalCommandConsumerSuppliers.add(internalCommandConsumerSupplier); } /** * Starts the registered internal command consumer(s) so that commands forwarded by the Command Router * are received. *

* A failed future is returned if no internal command consumer has been registered yet. * * @return A future indicating the outcome of the startup process. */ @Override public Future start() { if (internalCommandConsumerSuppliers.isEmpty()) { startFailureMessage = "no command consumer registered"; LOG.error("cannot start, {}", startFailureMessage); return Future.failedFuture(startFailureMessage); } return getK8sContainerId(1) .compose(containerId -> { adapterInstanceId = CommandRoutingUtil.getNewAdapterInstanceId(adapterName, containerId, adapterInstanceIdCounterValue); internalCommandConsumerSuppliers.stream() .map(sup -> sup.apply(adapterInstanceId, commandHandlers)) .forEach(consumer -> { LOG.info("created internal command consumer {}", consumer.getClass().getSimpleName()); internalCommandConsumers.add(consumer); Optional.ofNullable(readinessHandler).ifPresent(consumer::registerReadinessChecks); }); internalCommandConsumerSuppliers.clear(); readinessHandler = null; final List> futures = internalCommandConsumers.stream() .map(Lifecycle::start) .toList(); if (futures.isEmpty()) { return Future.failedFuture("no command consumer registered"); } return Future.all(futures).mapEmpty(); }) .recover(thr -> { startFailureMessage = thr.getMessage(); return Future.failedFuture(thr); }).mapEmpty(); } private Future getK8sContainerId(final int attempt) { final Context context = vertx.getOrCreateContext(); return kubernetesContainerInfoProvider.getContainerId(context) .recover(thr -> { if (thr instanceof IllegalStateException || stopCalled.get()) { return Future.failedFuture(thr); } LOG.info("attempt {} to get K8s container id failed, trying again...", attempt); final Promise containerIdPromise = Promise.promise(); context.runOnContext(action -> getK8sContainerId(attempt + 1).onComplete(containerIdPromise)); return containerIdPromise.future(); }); } @Override public Future stop() { if (!stopCalled.compareAndSet(false, true)) { return Future.succeededFuture(); } final List> futures = internalCommandConsumers.stream() .map(Lifecycle::stop) .toList(); return Future.all(futures).mapEmpty(); } @Override public void registerReadinessChecks(final HealthCheckHandler readinessHandler) { if (!internalCommandConsumers.isEmpty()) { LOG.warn("registerReadinessChecks expected to be called before start()"); internalCommandConsumers.forEach(consumer -> consumer.registerReadinessChecks(readinessHandler)); return; } this.readinessHandler = readinessHandler; readinessHandler.register("command-consumer-factory", 1000, this::checkIfInternalCommandConsumersCreated); } private void checkIfInternalCommandConsumersCreated(final Promise status) { if (internalCommandConsumers.isEmpty() || startFailureMessage != null) { final JsonObject data = new JsonObject(); if (startFailureMessage != null) { LOG.error("failed to start command consumer factory: {}", startFailureMessage); data.put("status", "startup of command consumer factory failed, check logs for details"); } status.tryComplete(Status.KO(data)); } else { status.tryComplete(Status.OK()); } } @Override public final Future createCommandConsumer( final String tenantId, final String deviceId, final boolean sendEvent, final Function> commandHandler, final Duration lifespan, final SpanContext context) { Objects.requireNonNull(tenantId); Objects.requireNonNull(deviceId); Objects.requireNonNull(commandHandler); return doCreateCommandConsumer(tenantId, deviceId, null, sendEvent, commandHandler, lifespan, context); } @Override public final Future createCommandConsumer( final String tenantId, final String deviceId, final String gatewayId, final boolean sendEvent, final Function> commandHandler, final Duration lifespan, final SpanContext context) { Objects.requireNonNull(tenantId); Objects.requireNonNull(deviceId); Objects.requireNonNull(gatewayId); Objects.requireNonNull(commandHandler); return doCreateCommandConsumer(tenantId, deviceId, gatewayId, sendEvent, commandHandler, lifespan, context); } private Future doCreateCommandConsumer( final String tenantId, final String deviceId, final String gatewayId, final boolean sendEvent, final Function> commandHandler, final Duration lifespan, final SpanContext context) { if (adapterInstanceId == null) { return Future.failedFuture("not started yet"); } // lifespan greater than what can be expressed in nanoseconds (i.e. 292 years) is considered unlimited, // preventing ArithmeticExceptions down the road final Duration sanitizedLifespan = lifespan == null || lifespan.isNegative() || lifespan.getSeconds() > (Long.MAX_VALUE / 1000_000_000L) ? Duration.ofSeconds(-1) : lifespan; LOG.trace("create command consumer [tenant-id: {}, device-id: {}, gateway-id: {}]", tenantId, deviceId, gatewayId); // register the command handler // for short-lived command consumers, let the consumer creation span context be used as reference in the command // span final SpanContext consumerCreationContextToUse = !sanitizedLifespan.isNegative() && sanitizedLifespan.toSeconds() <= TenantConstants.DEFAULT_MAX_TTD ? context : null; final CommandHandlerWrapper commandHandlerWrapper = new CommandHandlerWrapper(tenantId, deviceId, gatewayId, commandHandler, Vertx.currentContext(), consumerCreationContextToUse); commandHandlers.putCommandHandler(commandHandlerWrapper); final Instant lifespanStart = Instant.now(); return commandRouterClient .registerCommandConsumer(tenantId, deviceId, sendEvent, adapterInstanceId, sanitizedLifespan, context) .onFailure(thr -> { LOG.info( "error registering consumer with the command router service [tenant: {}, device: {}, sendEvent: {}]", tenantId, deviceId, sendEvent, thr); // handler association failed - unregister the handler commandHandlers.removeCommandHandler(tenantId, deviceId); }) .map(v -> { return new ProtocolAdapterCommandConsumer() { @Override public Future close(final boolean sendEvent, final SpanContext spanContext) { return removeCommandConsumer( commandHandlerWrapper, sendEvent, sanitizedLifespan, lifespanStart, spanContext); } }; }); } private Future removeCommandConsumer( final CommandHandlerWrapper commandHandlerWrapper, final boolean sendEvent, final Duration lifespan, final Instant lifespanStart, final SpanContext onCloseSpanContext) { if (adapterInstanceId == null) { return Future.failedFuture("not started yet"); } final String tenantId = commandHandlerWrapper.getTenantId(); final String deviceId = commandHandlerWrapper.getDeviceId(); LOG.trace("remove command consumer [tenant-id: {}, device-id: {}]", tenantId, deviceId); if (!commandHandlers.removeCommandHandler(commandHandlerWrapper)) { // This case happens when trying to remove a command consumer which has been overwritten since its creation // via a 2nd invocation of 'createCommandConsumer' with the same device/tenant id. Since the 2nd 'createCommandConsumer' // invocation has registered a different 'commandHandlerWrapper' instance (and possibly already removed it), // trying to remove the original object will return false here. // On a more abstract level, this case happens when 2 consecutive command subscription requests from the // same device (with no intermittent disconnect/unsubscribe - possibly because of a broken connection in between) have // reached the *same* adapter instance and verticle, using this CommandConsumerFactory. Invoking 'removeCommandConsumer' // on the 1st (obsolete and overwritten) command subscription shall have no impact. Throwing an explicit exception // here will enable the protocol adapter to detect this case and skip an (incorrect) "disconnectedTtd" event message. LOG.debug("command consumer not removed - handler already replaced or removed [tenant: {}, device: {}]", tenantId, deviceId); return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_PRECON_FAILED, "local command handler already replaced or removed")); } return commandRouterClient.unregisterCommandConsumer( tenantId, deviceId, sendEvent, adapterInstanceId, onCloseSpanContext) .recover(thr -> { if (ServiceInvocationException.extractStatusCode(thr) == HttpURLConnection.HTTP_PRECON_FAILED) { final boolean entryMayHaveExpired = !lifespan.isNegative() && Instant.now().isAfter(lifespanStart.plus(lifespan)); if (entryMayHaveExpired) { LOG.trace("ignoring 412 error when unregistering consumer with the command router service; entry may have already expired [tenant: {}, device: {}]", tenantId, deviceId); return Future.succeededFuture(); } else { // entry wasn't actually removed and entry hasn't expired (yet) // This case happens when 2 consecutive command subscription requests from the same device // (with no intermittent disconnect/unsubscribe - possibly because of a broken connection in between) // have reached *different* protocol adapter instances/verticles. Now calling 'unregisterCommandConsumer' // on the 1st subscription fails because of the non-matching adapterInstanceId parameter. // Throwing an explicit exception here will enable the protocol adapter to detect this case // and skip sending an (incorrect) "disconnectedTtd" event message. LOG.debug("consumer not unregistered - not matched or already removed [tenant: {}, device: {}]", tenantId, deviceId); return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_PRECON_FAILED, "no matching command consumer mapping found to be removed")); } } else { LOG.info("error unregistering consumer with the command router service [tenant: {}, device: {}]", tenantId, deviceId, thr); return Future.failedFuture(thr); } }); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy