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

io.servicetalk.http.netty.DefaultHttpServerBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2018-2023 Apple Inc. and the ServiceTalk project authors
 *
 * 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.servicetalk.http.netty;

import io.servicetalk.buffer.api.BufferAllocator;
import io.servicetalk.concurrent.api.Completable;
import io.servicetalk.concurrent.api.Executor;
import io.servicetalk.concurrent.api.Single;
import io.servicetalk.http.api.BlockingHttpService;
import io.servicetalk.http.api.BlockingStreamingHttpService;
import io.servicetalk.http.api.HttpExceptionMapperServiceFilter;
import io.servicetalk.http.api.HttpExecutionContext;
import io.servicetalk.http.api.HttpExecutionStrategies;
import io.servicetalk.http.api.HttpExecutionStrategy;
import io.servicetalk.http.api.HttpExecutionStrategyInfluencer;
import io.servicetalk.http.api.HttpHeaderNames;
import io.servicetalk.http.api.HttpLifecycleObserver;
import io.servicetalk.http.api.HttpProtocolConfig;
import io.servicetalk.http.api.HttpServerBuilder;
import io.servicetalk.http.api.HttpServerContext;
import io.servicetalk.http.api.HttpService;
import io.servicetalk.http.api.HttpServiceContext;
import io.servicetalk.http.api.StreamingHttpRequest;
import io.servicetalk.http.api.StreamingHttpResponse;
import io.servicetalk.http.api.StreamingHttpResponseFactory;
import io.servicetalk.http.api.StreamingHttpService;
import io.servicetalk.http.api.StreamingHttpServiceFilter;
import io.servicetalk.http.api.StreamingHttpServiceFilterFactory;
import io.servicetalk.logging.api.LogLevel;
import io.servicetalk.transport.api.ConnectExecutionStrategy;
import io.servicetalk.transport.api.ConnectionAcceptorFactory;
import io.servicetalk.transport.api.ConnectionInfo;
import io.servicetalk.transport.api.EarlyConnectionAcceptor;
import io.servicetalk.transport.api.ExecutionStrategy;
import io.servicetalk.transport.api.ExecutionStrategyInfluencer;
import io.servicetalk.transport.api.IoExecutor;
import io.servicetalk.transport.api.LateConnectionAcceptor;
import io.servicetalk.transport.api.ServerSslConfig;
import io.servicetalk.transport.api.TransportConfig;
import io.servicetalk.transport.api.TransportObserver;
import io.servicetalk.transport.netty.internal.InfluencerConnectionAcceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.SocketAddress;
import java.net.SocketOption;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.annotation.Nullable;

import static io.servicetalk.concurrent.api.Completable.defer;
import static io.servicetalk.http.api.HttpApiConversions.toStreamingHttpService;
import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy;
import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone;
import static io.servicetalk.http.netty.StrategyInfluencerAwareConversions.toConditionalServiceFilterFactory;
import static io.servicetalk.transport.api.ConnectionAcceptor.ACCEPT_ALL;
import static java.util.Objects.requireNonNull;

final class DefaultHttpServerBuilder implements HttpServerBuilder {

    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpServerBuilder.class);

    private static final HttpExecutionStrategy REQRESP_OFFLOADS = HttpExecutionStrategies.customStrategyBuilder()
            .offloadReceiveMetadata().offloadReceiveData().offloadSend().build();

    @Nullable
    private ConnectionAcceptorFactory connectionAcceptorFactory;
    private final List noOffloadServiceFilters = new ArrayList<>();
    private final List serviceFilters = new ArrayList<>();
    private final List earlyConnectionAcceptors = new ArrayList<>();
    private final List lateConnectionAcceptors = new ArrayList<>();
    private HttpExecutionStrategy strategy = defaultStrategy();
    private boolean drainRequestPayloadBody = true;
    private final HttpServerConfig config = new HttpServerConfig();
    private final HttpExecutionContextBuilder executionContextBuilder = new HttpExecutionContextBuilder();
    private final SocketAddress address;

    // Do not use this ctor directly, HttpServers is the entry point for creating a new builder.
    DefaultHttpServerBuilder(SocketAddress address) {
        appendNonOffloadingServiceFilter(ClearAsyncContextHttpServiceFilter.CLEAR_ASYNC_CONTEXT_HTTP_SERVICE_FILTER);
        this.address = address;
    }

    private static StreamingHttpServiceFilterFactory buildFactory(List filters) {
        return filters.stream()
                .reduce((prev, filter) -> service -> prev.create(filter.create(service)))
                .orElse(StreamingHttpServiceFilter::new); // unfortunate that we need extra layer
    }

    private static StreamingHttpService buildService(Stream filters,
                                                     StreamingHttpService service) {
        return filters
                .reduce((prev, filter) -> svc -> prev.create(filter.create(svc)))
                .map(factory -> (StreamingHttpService) factory.create(service))
                .orElse(service);
    }

    private static HttpExecutionStrategy computeRequiredStrategy(List filters,
                                                                 HttpExecutionStrategy serviceStrategy) {
        HttpExecutionStrategy current = serviceStrategy;
        for (StreamingHttpServiceFilterFactory filter : filters) {
            HttpExecutionStrategy next = current.merge(filter.requiredOffloads());
            if (current != next) {
                LOGGER.debug("{} '{}' changes execution strategy from '{}' to '{}'",
                        StreamingHttpServiceFilterFactory.class, filter, current, next);
                current = next;
            }
        }
        return current;
    }

    private static  T checkNonOffloading(String what, ExecutionStrategy fallbackValue, T obj) {
        ExecutionStrategy requires = obj instanceof ExecutionStrategyInfluencer ?
                ((ExecutionStrategyInfluencer) obj).requiredOffloads() :
                fallbackValue;
        if (requires.hasOffloads()) {
            throw new IllegalArgumentException(what + " '" + obj.getClass().getName() + "' requires offloading: " +
                    requires + ". Therefore, it cannot be used with 'appendNonOffloadingServiceFilter(...)', " +
                    "use 'appendServiceFilter(...)' instead.");
        }
        return obj;
    }

    @Override
    public HttpServerBuilder drainRequestPayloadBody(final boolean enable) {
        this.drainRequestPayloadBody = enable;
        return this;
    }

    @Override
    public HttpServerBuilder appendConnectionAcceptorFilter(final ConnectionAcceptorFactory factory) {
        if (connectionAcceptorFactory == null) {
            connectionAcceptorFactory = factory;
        } else {
            connectionAcceptorFactory = connectionAcceptorFactory.append(factory);
        }
        return this;
    }

    @Override
    public HttpServerBuilder appendEarlyConnectionAcceptor(final EarlyConnectionAcceptor acceptor) {
        earlyConnectionAcceptors.add(requireNonNull(acceptor));
        return this;
    }

    @Override
    public HttpServerBuilder appendLateConnectionAcceptor(final LateConnectionAcceptor acceptor) {
        lateConnectionAcceptors.add(requireNonNull(acceptor));
        return this;
    }

    @Override
    public HttpServerBuilder appendNonOffloadingServiceFilter(final StreamingHttpServiceFilterFactory factory) {
        noOffloadServiceFilters.add(checkNonOffloading("Filter", defaultStrategy(), factory));
        return this;
    }

    @Override
    public HttpServerBuilder appendNonOffloadingServiceFilter(final Predicate predicate,
                                                              final StreamingHttpServiceFilterFactory factory) {
        checkNonOffloading("Predicate", offloadNone(), predicate);
        checkNonOffloading("Filter", defaultStrategy(), factory);
        noOffloadServiceFilters.add(toConditionalServiceFilterFactory(predicate, factory));
        return this;
    }

    @Override
    public HttpServerBuilder appendServiceFilter(final StreamingHttpServiceFilterFactory factory) {
        requireNonNull(factory);
        serviceFilters.add(factory);
        return this;
    }

    @Override
    public HttpServerBuilder appendServiceFilter(final Predicate predicate,
                                                 final StreamingHttpServiceFilterFactory factory) {
        appendServiceFilter(toConditionalServiceFilterFactory(predicate, factory));
        return this;
    }

    @Override
    public HttpServerBuilder protocols(final HttpProtocolConfig... protocols) {
        config.httpConfig().protocols(protocols);
        return this;
    }

    @Override
    public HttpServerBuilder sslConfig(final ServerSslConfig config) {
        this.config.tcpConfig().sslConfig(requireNonNull(config, "config"));
        return this;
    }

    @Override
    public HttpServerBuilder sslConfig(final ServerSslConfig defaultConfig, final Map sniMap) {
        this.config.tcpConfig().sslConfig(requireNonNull(defaultConfig, "defaultConfig"),
                requireNonNull(sniMap, "sniMap"));
        return this;
    }

    @Override
    public HttpServerBuilder sslConfig(final ServerSslConfig defaultConfig, final Map sniMap,
                                       final int maxClientHelloLength, final Duration clientHelloTimeout) {
        this.config.tcpConfig().sslConfig(requireNonNull(defaultConfig, "defaultConfig"),
                requireNonNull(sniMap, "sniMap"), maxClientHelloLength,
                requireNonNull(clientHelloTimeout, "clientHelloTimeout"));
        return this;
    }

    @Override
    public HttpServerBuilder sslConfig(final ServerSslConfig config, final boolean acceptInsecureConnections) {
        this.config.tcpConfig().sslConfig(config, acceptInsecureConnections);
        return this;
    }

    @Override
    public HttpServerBuilder transportConfig(final TransportConfig transportConfig) {
        config.tcpConfig().transportConfig(transportConfig);
        return this;
    }

    @Override
    public  HttpServerBuilder socketOption(final SocketOption option, final T value) {
        config.tcpConfig().socketOption(option, value);
        return this;
    }

    @Override
    public  HttpServerBuilder listenSocketOption(final SocketOption option, final T value) {
        config.tcpConfig().listenSocketOption(option, value);
        return this;
    }

    @Override
    public HttpServerBuilder enableWireLogging(final String loggerName, final LogLevel logLevel,
                                               final BooleanSupplier logUserData) {
        config.tcpConfig().enableWireLogging(loggerName, logLevel, logUserData);
        return this;
    }

    @Override
    public HttpServerBuilder transportObserver(final TransportObserver transportObserver) {
        config.tcpConfig().transportObserver(transportObserver);
        return this;
    }

    @Override
    public HttpServerBuilder lifecycleObserver(final HttpLifecycleObserver lifecycleObserver) {
        config.lifecycleObserver(lifecycleObserver);
        return this;
    }

    @Override
    public HttpServerBuilder allowDropRequestTrailers(final boolean allowDrop) {
        config.httpConfig().allowDropTrailersReadFromTransport(allowDrop);
        return this;
    }

    @Override
    public HttpServerBuilder executor(final Executor executor) {
        executionContextBuilder.executor(executor);
        return this;
    }

    @Override
    public HttpServerBuilder executionStrategy(HttpExecutionStrategy strategy) {
        this.strategy = requireNonNull(strategy);
        return this;
    }

    @Override
    public HttpServerBuilder ioExecutor(final IoExecutor ioExecutor) {
        executionContextBuilder.ioExecutor(ioExecutor);
        return this;
    }

    @Override
    public HttpServerBuilder bufferAllocator(final BufferAllocator allocator) {
        executionContextBuilder.bufferAllocator(allocator);
        return this;
    }

    @Override
    public Single listen(final HttpService service) {
        StreamingHttpService streamingService = toStreamingHttpService(
                computeServiceStrategy(HttpService.class, service), service);
        return listenForService(streamingService, streamingService.requiredOffloads());
    }

    @Override
    public Single listenStreaming(final StreamingHttpService service) {
        return listenForService(service, computeServiceStrategy(StreamingHttpService.class, service));
    }

    @Override
    public Single listenBlocking(final BlockingHttpService service) {
        StreamingHttpService streamingService = toStreamingHttpService(
                computeServiceStrategy(BlockingHttpService.class, service), service);
        return listenForService(streamingService, streamingService.requiredOffloads());
    }

    @Override
    public Single listenBlockingStreaming(final BlockingStreamingHttpService service) {
        StreamingHttpService streamingService = toStreamingHttpService(
                computeServiceStrategy(BlockingStreamingHttpService.class, service), service);
        return listenForService(streamingService, streamingService.requiredOffloads());
    }

    private HttpExecutionContext buildExecutionContext(final HttpExecutionStrategy strategy) {
        executionContextBuilder.executionStrategy(strategy);
        return executionContextBuilder.build();
    }

    /**
     * Starts this server and returns the {@link HttpServerContext} after the server has been successfully started.
     * 

* If the underlying protocol (e.g. TCP) supports it this should result in a socket bind/listen on {@code address}. *

/p> * The execution path for a request will be offloaded from the IO thread as required to ensure safety. The *

*
read side
*
IO thread → request → non-offload filters → offload filters → raw service
*
subscribe/request side
*
IO thread → subscribe/request/cancel → non-offload filters → offload filters → raw service
*
* * @param rawService {@link StreamingHttpService} to use for the server. * @param computedStrategy the computed {@link HttpExecutionStrategy} to use for the service. * @return A {@link Single} that completes when the server is successfully started or terminates with an error if * the server could not be started. */ private Single listenForService(StreamingHttpService rawService, final HttpExecutionStrategy computedStrategy) { InfluencerConnectionAcceptor connectionAcceptor = connectionAcceptorFactory == null ? null : InfluencerConnectionAcceptor.withStrategy(connectionAcceptorFactory.create(ACCEPT_ALL), connectionAcceptorFactory.requiredOffloads()); EarlyConnectionAcceptor earlyConnectionAcceptor = buildEarlyConnectionAcceptor(earlyConnectionAcceptors); LateConnectionAcceptor lateConnectionAcceptor = buildLateConnectionAcceptor(lateConnectionAcceptors); final StreamingHttpService filteredService; final HttpExecutionContext executionContext; // The watchdog sits at the very beginning of the response flow (the end of the filter pipeline) so that any // payload coming from the service is ensured to be tracked before subsequent filters get a chance to drop it // without being accounted for. rawService = HttpMessageDiscardWatchdogServiceFilter.INSTANCE.create(rawService); if (noOffloadServiceFilters.isEmpty()) { filteredService = serviceFilters.isEmpty() ? rawService : buildService(serviceFilters.stream(), rawService); executionContext = buildExecutionContext(computedStrategy); } else { Stream nonOffloadingFilters = noOffloadServiceFilters.stream(); if (computedStrategy.isRequestResponseOffloaded()) { executionContext = buildExecutionContext(REQRESP_OFFLOADS.missing(computedStrategy)); BooleanSupplier shouldOffload = executionContext.ioExecutor().shouldOffloadSupplier(); // We are going to have to offload, even if just to the raw service OffloadingFilter offloadingFilter = new OffloadingFilter(computedStrategy, buildFactory(serviceFilters), shouldOffload); nonOffloadingFilters = Stream.concat(nonOffloadingFilters, Stream.of(offloadingFilter)); } else { // All the filters can be appended. nonOffloadingFilters = Stream.concat(nonOffloadingFilters, serviceFilters.stream()); executionContext = buildExecutionContext(computedStrategy); } filteredService = buildService(nonOffloadingFilters, rawService); } final HttpExecutionStrategy builderStrategy = this.strategy; return doBind(executionContext, connectionAcceptor, filteredService, earlyConnectionAcceptor, lateConnectionAcceptor) .afterOnSuccess(serverContext -> { if (builderStrategy != defaultStrategy() && builderStrategy.missing(computedStrategy) != offloadNone()) { LOGGER.info("Server for address {} created with the builder strategy {} but resulting " + "computed strategy is {}. One of the filters or a final service enforce " + "additional offloading. To find out what filter or service is " + "it, enable debug level logging for {}.", serverContext.listenAddress(), builderStrategy, computedStrategy, DefaultHttpServerBuilder.class); } else if (builderStrategy == computedStrategy) { LOGGER.debug("Server for address {} created with the execution strategy {}.", serverContext.listenAddress(), computedStrategy); } else { LOGGER.debug("Server for address {} created with the builder strategy {}, " + "resulting computed strategy is {}.", serverContext.listenAddress(), builderStrategy, computedStrategy); } }); } private Single doBind(final HttpExecutionContext executionContext, @Nullable final InfluencerConnectionAcceptor connectionAcceptor, final StreamingHttpService service, @Nullable final EarlyConnectionAcceptor earlyConnectionAcceptor, @Nullable final LateConnectionAcceptor lateConnectionAcceptor) { ReadOnlyHttpServerConfig roConfig = config.asReadOnly(); StreamingHttpService filteredService = applyInternalFilters(service, roConfig.lifecycleObserver()); if (roConfig.tcpConfig().sslConfig() != null && roConfig.tcpConfig().acceptInsecureConnections()) { HttpServerConfig configWithoutSsl = new HttpServerConfig(config); configWithoutSsl.tcpConfig().sslConfig(null); if (roConfig.h1Config() != null && roConfig.h2Config() != null) { // For non-SSL, if both H1 and H2 are configured at the same time we force-fallback to H1 configWithoutSsl.httpConfig().protocols(roConfig.h1Config()); } ReadOnlyHttpServerConfig roConfigWithoutSsl = configWithoutSsl.asReadOnly(); return OptionalSslNegotiator.bind(executionContext, roConfig, roConfigWithoutSsl, address, connectionAcceptor, service, drainRequestPayloadBody, earlyConnectionAcceptor, lateConnectionAcceptor); } else if (roConfig.tcpConfig().isAlpnConfigured()) { return DeferredServerChannelBinder.bind(executionContext, roConfig, address, connectionAcceptor, filteredService, drainRequestPayloadBody, false, earlyConnectionAcceptor, lateConnectionAcceptor); } else if (roConfig.tcpConfig().sniMapping() != null) { return DeferredServerChannelBinder.bind(executionContext, roConfig, address, connectionAcceptor, filteredService, drainRequestPayloadBody, true, earlyConnectionAcceptor, lateConnectionAcceptor); } else if (roConfig.isH2PriorKnowledge()) { return H2ServerParentConnectionContext.bind(executionContext, roConfig, address, connectionAcceptor, filteredService, drainRequestPayloadBody, earlyConnectionAcceptor, lateConnectionAcceptor); } return NettyHttpServer.bind(executionContext, roConfig, address, connectionAcceptor, filteredService, drainRequestPayloadBody, earlyConnectionAcceptor, lateConnectionAcceptor); } private HttpExecutionStrategy computeServiceStrategy( final Class clazz, final T service) { final HttpExecutionStrategy serviceStrategy = service.requiredOffloads(); LOGGER.debug("{} '{}' requires {} strategy.", clazz.getSimpleName(), service, serviceStrategy); final HttpExecutionStrategy builderStrategy = this.strategy; final HttpExecutionStrategy computedStrategy = computeRequiredStrategy(serviceFilters, serviceStrategy); return defaultStrategy() == builderStrategy ? computedStrategy : builderStrategy.hasOffloads() ? builderStrategy.merge(computedStrategy) : builderStrategy; } /** * Combines all early acceptors into one by concatenating the callbacks and merging their execution strategies. * * @param acceptors the acceptors to combine into one. * @return the combined acceptor with merged execution strategies. */ @Nullable private static EarlyConnectionAcceptor buildEarlyConnectionAcceptor(final List acceptors) { return acceptors .stream() .reduce((prev, acceptor) -> new EarlyConnectionAcceptor() { @Override public Completable accept(final ConnectionInfo info) { // Defer is required to isolate the context for the individual acceptors. return prev.accept(info).concat(defer(() -> acceptor.accept(info))); } @Override public ConnectExecutionStrategy requiredOffloads() { return prev.requiredOffloads().merge(acceptor.requiredOffloads()); } }) .orElse(null); } /** * Combines all late acceptors into one by concatenating their callbacks and merging their execution strategies. * * @param acceptors the acceptors to combine into one. * @return the combined acceptor with merged execution strategies. */ @Nullable private static LateConnectionAcceptor buildLateConnectionAcceptor(final List acceptors) { return acceptors .stream() .reduce((prev, acceptor) -> new LateConnectionAcceptor() { @Override public Completable accept(final ConnectionInfo info) { // Defer is required to isolate the context for the individual acceptors. return prev.accept(info).concat(defer(() -> acceptor.accept(info))); } @Override public ConnectExecutionStrategy requiredOffloads() { return prev.requiredOffloads().merge(acceptor.requiredOffloads()); } }) .orElse(null); } private static StreamingHttpService applyInternalFilters(StreamingHttpService service, @Nullable final HttpLifecycleObserver lifecycleObserver) { // This filter is placed at the end of the response lifecycle (so beginning of the filter pipeline) to ensure // that any discarded payloads coming from the service are cleaned up. service = HttpMessageDiscardWatchdogServiceFilter.CLEANER.create(service); service = HttpExceptionMapperServiceFilter.INSTANCE.create(service); service = KeepAliveServiceFilter.INSTANCE.create(service); if (lifecycleObserver != null) { service = new HttpLifecycleObserverServiceFilter(lifecycleObserver).create(service); } // TODO: apply ClearAsyncContextHttpServiceFilter here when it's moved to http-netty module by // https://github.com/apple/servicetalk/pull/1820 return service; } /** * Internal filter that correctly sets {@link HttpHeaderNames#CONNECTION} header value based on the requested * keep-alive policy. */ private static final class KeepAliveServiceFilter implements StreamingHttpServiceFilterFactory { static final StreamingHttpServiceFilterFactory INSTANCE = new KeepAliveServiceFilter(); private KeepAliveServiceFilter() { // Singleton } @Override public StreamingHttpServiceFilter create(final StreamingHttpService service) { return new StreamingHttpServiceFilter(service) { @Override public Single handle(final HttpServiceContext ctx, final StreamingHttpRequest request, final StreamingHttpResponseFactory responseFactory) { final HttpKeepAlive keepAlive = HttpKeepAlive.responseKeepAlive(request); // Don't expect any exceptions from delegate because it's already wrapped with // ExceptionMapperServiceFilterFactory return delegate().handle(ctx, request, responseFactory).map(response -> { keepAlive.addConnectionHeaderIfNecessary(response); return response; }); } }; } @Override public HttpExecutionStrategy requiredOffloads() { // no influence since we do not block return offloadNone(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy