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

io.gravitee.gateway.reactive.handlers.api.v4.TcpApiReactor Maven / Gradle / Ivy

There is a newer version: 4.5.5
Show newest version
/*
 * Copyright © 2015 The Gravitee team (http://gravitee.io)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.gravitee.gateway.reactive.handlers.api.v4;

import static io.gravitee.gateway.handlers.api.ApiReactorHandlerFactory.REPORTERS_LOGGING_EXCLUDED_RESPONSE_TYPES_PROPERTY;
import static io.gravitee.gateway.handlers.api.ApiReactorHandlerFactory.REPORTERS_LOGGING_MAX_SIZE_PROPERTY;
import static io.gravitee.gateway.reactive.api.context.InternalContextAttributes.*;
import static io.gravitee.gateway.reactive.handlers.api.v4.DefaultApiReactor.*;
import static io.reactivex.rxjava3.core.Completable.defer;
import static io.reactivex.rxjava3.core.Observable.interval;

import io.gravitee.common.component.AbstractLifecycleComponent;
import io.gravitee.common.component.Lifecycle;
import io.gravitee.common.http.HttpStatusCode;
import io.gravitee.definition.model.v4.listener.tcp.TcpListener;
import io.gravitee.gateway.env.RequestTimeoutConfiguration;
import io.gravitee.gateway.reactive.api.ExecutionFailure;
import io.gravitee.gateway.reactive.api.connector.entrypoint.EntrypointConnector;
import io.gravitee.gateway.reactive.api.context.ContextAttributes;
import io.gravitee.gateway.reactive.api.context.DeploymentContext;
import io.gravitee.gateway.reactive.api.context.InternalContextAttributes;
import io.gravitee.gateway.reactive.api.hook.InvokerHook;
import io.gravitee.gateway.reactive.api.invoker.Invoker;
import io.gravitee.gateway.reactive.core.context.MutableExecutionContext;
import io.gravitee.gateway.reactive.core.hook.HookHelper;
import io.gravitee.gateway.reactive.core.tracing.TracingHook;
import io.gravitee.gateway.reactive.core.v4.analytics.AnalyticsContext;
import io.gravitee.gateway.reactive.core.v4.endpoint.EndpointManager;
import io.gravitee.gateway.reactive.core.v4.entrypoint.DefaultEntrypointConnectorResolver;
import io.gravitee.gateway.reactive.core.v4.invoker.EndpointInvoker;
import io.gravitee.gateway.reactive.handlers.api.v4.analytics.logging.LoggingHook;
import io.gravitee.gateway.reactive.reactor.ApiReactor;
import io.gravitee.gateway.reactor.handler.Acceptor;
import io.gravitee.gateway.reactor.handler.DefaultTcpAcceptor;
import io.gravitee.gateway.reactor.handler.ReactorHandler;
import io.gravitee.node.api.Node;
import io.gravitee.node.api.configuration.Configuration;
import io.gravitee.plugin.entrypoint.EntrypointConnectorPluginManager;
import io.reactivex.rxjava3.core.Completable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;

/**
 * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
 * @author GraviteeSource Team
 */

@Slf4j
public class TcpApiReactor extends AbstractLifecycleComponent implements ApiReactor {

    private final Api api;
    private final Node node;
    private final DefaultEntrypointConnectorResolver entrypointConnectorResolver;
    private final EndpointManager endpointManager;
    private final EndpointInvoker defaultInvoker;

    private final RequestTimeoutConfiguration requestTimeoutConfiguration;
    private final String loggingExcludedResponseType;
    private final String loggingMaxSize;
    private Lifecycle.State lifecycleState;
    private final AtomicLong pendingRequests = new AtomicLong(0);

    private final List invokerHooks = new ArrayList<>();
    private boolean tracingEnabled;
    private long pendingRequestsTimeout;

    public TcpApiReactor(
        Api api,
        Node node,
        Configuration configuration,
        DeploymentContext deploymentContext,
        EntrypointConnectorPluginManager entrypointConnectorPluginManager,
        EndpointManager endpointManager,
        RequestTimeoutConfiguration requestTimeoutConfiguration
    ) {
        this.api = api;
        this.node = node;
        this.requestTimeoutConfiguration = requestTimeoutConfiguration;
        this.entrypointConnectorResolver =
            new DefaultEntrypointConnectorResolver(api.getDefinition(), deploymentContext, entrypointConnectorPluginManager);
        this.endpointManager = endpointManager;
        this.defaultInvoker = new EndpointInvoker(endpointManager);
        this.tracingEnabled = configuration.getProperty(SERVICES_TRACING_ENABLED_PROPERTY, Boolean.class, false);
        this.pendingRequestsTimeout = configuration.getProperty(PENDING_REQUESTS_TIMEOUT_PROPERTY, Long.class, 10_000L);
        this.lifecycleState = Lifecycle.State.INITIALIZED;
        this.loggingExcludedResponseType =
            configuration.getProperty(REPORTERS_LOGGING_EXCLUDED_RESPONSE_TYPES_PROPERTY, String.class, null);
        this.loggingMaxSize = configuration.getProperty(REPORTERS_LOGGING_MAX_SIZE_PROPERTY, String.class, null);
    }

    @Override
    public Api api() {
        return this.api;
    }

    @Override
    public Completable handle(MutableExecutionContext ctx) {
        // TODO: might need a bit of refactoring (duplication)
        ctx.setAttribute(ContextAttributes.ATTR_API, api.getId());
        ctx.setAttribute(ContextAttributes.ATTR_API_DEPLOYED_AT, api.getDeployedAt().getTime());
        ctx.setAttribute(ContextAttributes.ATTR_ORGANIZATION, api.getOrganizationId());
        ctx.setAttribute(ContextAttributes.ATTR_ENVIRONMENT, api.getEnvironmentId());
        ctx.setInternalAttribute(InternalContextAttributes.ATTR_INTERNAL_INVOKER, defaultInvoker);

        // TODO specific Tcp API Request processor chain factory that contains SubscriptionProcessor in beforeApi chain
        return new CompletableReactorChain(handleEntrypointRequest(ctx))
            // configure backend call
            .chainWith(invokeBackend(ctx))
            // setup timeout
            .chainWith(upstream -> timeout(upstream, ctx))
            // handle response
            .chainWith(handleEntrypointResponse(ctx))
            // hook everything up and start piping data
            .chainWith(ctx.response().end(ctx))
            .doOnSubscribe(disposable -> pendingRequests.incrementAndGet())
            .doFinally(pendingRequests::decrementAndGet);
    }

    protected Completable handleEntrypointRequest(final MutableExecutionContext ctx) {
        // TODO: code is duplicated with ApiReactor
        return Completable.defer(() -> {
            final EntrypointConnector entrypointConnector = entrypointConnectorResolver.resolve(ctx);
            if (entrypointConnector == null) {
                return ctx.interruptWith(new ExecutionFailure().message("No entrypoint matches the incoming request"));
            }
            // Add the resolved entrypoint connector into the internal attributes, so it can be used later (ex: for endpoint connector resolution).
            ctx.setInternalAttribute(ATTR_INTERNAL_ENTRYPOINT_CONNECTOR, entrypointConnector);

            return entrypointConnector.handleRequest(ctx);
        });
    }

    protected Completable invokeBackend(final MutableExecutionContext ctx) {
        return defer(() ->
            getInvoker(ctx)
                .map(invoker -> HookHelper.hook(() -> invoker.invoke(ctx), invoker.getId(), invokerHooks, ctx, null))
                .orElse(Completable.complete())
        );
    }

    private Optional getInvoker(final MutableExecutionContext ctx) {
        if (ctx.getInternalAttribute(ATTR_INTERNAL_INVOKER) instanceof Invoker invoker) {
            return Optional.of(invoker);
        }
        return Optional.empty();
    }

    protected Completable handleEntrypointResponse(final MutableExecutionContext ctx) {
        // TODO: code is duplicated with ApiReactor
        return Completable
            .defer(() -> {
                if (ctx.getInternalAttribute(ATTR_INTERNAL_EXECUTION_FAILURE) == null) {
                    final EntrypointConnector entrypointConnector = ctx.getInternalAttribute(ATTR_INTERNAL_ENTRYPOINT_CONNECTOR);
                    if (entrypointConnector != null) {
                        return entrypointConnector.handleResponse(ctx);
                    }
                }
                return Completable.complete();
            })
            .compose(upstream -> timeout(upstream, ctx));
    }

    protected Completable timeout(final Completable upstream, MutableExecutionContext ctx) {
        // TODO: code is duplicated with ApiReactor
        // When timeout is configured with 0 or less, consider it as infinity: no timeout operator to use in the chain.
        if (requestTimeoutConfiguration.getRequestTimeout() <= 0) {
            return upstream;
        }

        return Completable.defer(() ->
            upstream.timeout(
                Math.max(
                    requestTimeoutConfiguration.getRequestTimeoutGraceDelay(),
                    requestTimeoutConfiguration.getRequestTimeout() - (System.currentTimeMillis() - ctx.request().timestamp())
                ),
                TimeUnit.MILLISECONDS,
                ctx.interruptWith(new ExecutionFailure().key(REQUEST_TIMEOUT_KEY).message("Request timeout"))
            )
        );
    }

    @Override
    public Lifecycle.State lifecycleState() {
        return lifecycleState;
    }

    @Override
    protected void doStart() throws Exception {
        long startTime = System.currentTimeMillis();
        endpointManager.start();
        if (tracingEnabled) {
            invokerHooks.add(new TracingHook("invoker"));
        }

        var analyticsContext = analyticsContext();
        if (analyticsContext.isEnabled() && (analyticsContext.isLoggingEnabled())) {
            invokerHooks.add(new LoggingHook());
        }

        lifecycleState = Lifecycle.State.STARTED;
        long endTime = System.currentTimeMillis(); // Get the end Time
        log.debug("TCP API reactor started in {} ms", (endTime - startTime));
        dumpAcceptors();
    }

    protected void doStop() throws Exception {
        lifecycleState = Lifecycle.State.STOPPING;

        try {
            entrypointConnectorResolver.preStop();
            endpointManager.preStop();

            if (!node.lifecycleState().equals(Lifecycle.State.STARTED)) {
                log.debug("Current node is not started, API handler will be stopped immediately");
                stopNow();
            } else {
                log.debug("Current node is started, API handler will wait for pending requests before stopping");
                stopUntil().onErrorComplete().subscribe();
            }
        } catch (Exception e) {
            log.warn("An error occurred when trying to stop the TCP API reactor {}", this);
        }
    }

    private Completable stopUntil() {
        // TODO: code is duplicated with ApiReactor
        return interval(STOP_UNTIL_INTERVAL_PERIOD_MS, TimeUnit.MILLISECONDS)
            .timestamp()
            .takeWhile(t -> pendingRequests.get() > 0 && (t.value() + 1) * STOP_UNTIL_INTERVAL_PERIOD_MS < pendingRequestsTimeout)
            .ignoreElements()
            .onErrorComplete()
            .doFinally(this::stopNow);
    }

    private void stopNow() throws Exception {
        log.debug("TCP API reactor is now stopping, closing context for {} ...", this);

        entrypointConnectorResolver.stop();
        endpointManager.stop();

        lifecycleState = Lifecycle.State.STOPPED;

        log.debug("TCP API reactor is now stopped: {}", this);
    }

    @Override
    public ReactorHandler stop() throws Exception {
        return this;
    }

    @Override
    @SuppressWarnings("java:S6204") // no using toList() as it messes with generics
    public List> acceptors() {
        return api
            .getDefinition()
            .getListeners()
            .stream()
            .filter(TcpListener.class::isInstance)
            .map(l -> (TcpListener) l)
            .flatMap(listener -> listener.getHosts().stream().map(host -> new DefaultTcpAcceptor(this, host, listener.getServers())))
            .collect(Collectors.>toList());
    }

    protected AnalyticsContext analyticsContext() {
        return new AnalyticsContext(api.getDefinition().getAnalytics(), loggingMaxSize, loggingExcludedResponseType);
    }

    protected void dumpAcceptors() {
        // TODO: code is duplicated with ApiReactor
        List> acceptors = acceptors();
        log.debug("{} ready to accept buffers on:", this);
        acceptors.forEach(acceptor -> log.debug("\t{}", acceptor));
    }

    @Override
    public String toString() {
        return "TcpApiReactor API id[" + api.getId() + "] name[" + api.getName() + "] version[" + api.getApiVersion() + ']';
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy