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

com.palantir.conjure.java.undertow.runtime.ConjureAsyncRequestProcessing Maven / Gradle / Ivy

There is a newer version: 8.34.0
Show newest version
/*
 * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
 *
 * 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 com.palantir.conjure.java.undertow.runtime;

import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.palantir.conjure.java.api.errors.ErrorType;
import com.palantir.conjure.java.api.errors.ErrorType.Code;
import com.palantir.conjure.java.api.errors.ServiceException;
import com.palantir.conjure.java.undertow.lib.AsyncRequestProcessing;
import com.palantir.conjure.java.undertow.lib.ExceptionHandler;
import com.palantir.conjure.java.undertow.lib.ReturnValueWriter;
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.SafeArg;
import com.palantir.tracing.DeferredTracer;
import io.undertow.server.ExchangeCompletionListener;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.xnio.XnioExecutor;

/**
 *
 *
 * 

Thread Model

* *
    *
  • Any {@link ListenableFuture} may be registered for asynchronous request processing regardless of type, where * it's executed, and what thread completes it. *
  • All serialization and I/O occurs on the server task pool, matching synchronous conjure services. *
* * This requires us to move execution away from {@link ListenableFuture} callbacks as quickly as possible, because * they're controlled by a future created in the service implementation. This way service authors do not need to be * aware of the time it takes to serialize results and write them to clients, which can be time consuming depending on * the network. For example, an endpoint which schedules results on a single-threaded * {@link java.util.concurrent.ScheduledExecutorService} would congest the executor and fail to execute other scheduled * work at the expected time. We use {@link HttpServerExchange#dispatch(HttpHandler)} to put work onto the server task * pool where it is executed as similarly as possible to synchronous conjure endpoints to avoid behavior differences * between the two. */ final class ConjureAsyncRequestProcessing implements AsyncRequestProcessing { private static final Executor DIRECT_EXECUTOR = MoreExecutors.directExecutor(); // Thread interruption can result in unexpected behavior. Most uses of this feature are not based on // running tasks, so the value passed to Future.cancel makes no difference. private static final boolean INTERRUPT_ON_CANCEL = false; private static final AttachmentKey> FUTURE = AttachmentKey.create(ListenableFuture.class); private static final AttachmentKey TIMED_OUT = AttachmentKey.create(Boolean.class); private static final ErrorType ASYNC_REQUEST_PROCESSING_TIMEOUT = ErrorType.create(Code.TIMEOUT, "Conjure:AsyncRequestProcessingTimeout"); // If the request is ended before the future has completed, cancel the future to signal that work // should be stopped. This occurs when clients cancel requests or connections are closed. private static final ExchangeCompletionListener COMPLETION_LISTENER = SafeExchangeCompletionListener.of(exchange -> { ListenableFuture future = exchange.getAttachment(FUTURE); if (future != null) { future.cancel(INTERRUPT_ON_CANCEL); } }); private final Duration timeout; private final ExceptionHandler exceptionHandler; ConjureAsyncRequestProcessing(Duration timeout, ExceptionHandler exceptionHandler) { this.timeout = timeout; this.exceptionHandler = exceptionHandler; } @Override public void register( ListenableFuture future, ReturnValueWriter returnValueWriter, HttpServerExchange exchange) throws IOException { Preconditions.checkNotNull(future, "future"); Preconditions.checkNotNull(returnValueWriter, "returnValueWriter"); Preconditions.checkNotNull(exchange, "exchange"); register(future, returnValueWriter, timeout, exchange); } @Override public void register( ListenableFuture future, ReturnValueWriter returnValueWriter, Duration requestAsyncTimeout, HttpServerExchange exchange) throws IOException { Preconditions.checkNotNull(future, "future"); Preconditions.checkNotNull(returnValueWriter, "returnValueWriter"); Preconditions.checkNotNull(requestAsyncTimeout, "timeout"); Preconditions.checkNotNull(exchange, "exchange"); if (future.isDone()) { // Optimization: write the completed result immediately without dispatching across threads. writeCompleteFuture(future, returnValueWriter, exchange); } else { registerCallback(future, returnValueWriter, requestAsyncTimeout, exchange); } } private void writeCompleteFuture( ListenableFuture future, ReturnValueWriter returnValueWriter, HttpServerExchange exchange) throws IOException { try { T result = Futures.getDone(future); returnValueWriter.write(result, exchange); } catch (ExecutionException e) { exceptionHandler.handle(exchange, e.getCause()); } catch (RuntimeException e) { exceptionHandler.handle(exchange, e); } } private void registerCallback( ListenableFuture future, ReturnValueWriter returnValueWriter, Duration requestAsyncTimeout, HttpServerExchange exchange) { // Attach data to the exchange in order to reuse a stateless listener exchange.putAttachment(FUTURE, future); // If the exchange is complete we still follow this path to provide standard logging. if (exchange.isComplete()) { // If the exchange has already completed (http/2 rst_stream while processing). // Calling addExchangeCompleteListener will result in an IllegalStateException in this case. future.cancel(INTERRUPT_ON_CANCEL); } else { // Cancel the future if the exchange is completed before the future is done exchange.addExchangeCompleteListener(COMPLETION_LISTENER); } XnioExecutor.Key timeoutKey = exchange.getIoThread() .executeAfter( () -> { // TIMED_OUT must be set prior to future.cancel, otherwise the FutureCallback // may be invoked before TIMED_OUT is set/visible. exchange.putAttachment(TIMED_OUT, Boolean.TRUE); future.cancel(INTERRUPT_ON_CANCEL); }, requestAsyncTimeout.toMillis(), TimeUnit.MILLISECONDS); future.addListener(timeoutKey::remove, DIRECT_EXECUTOR); // Dispatch the registration task, this accomplishes two things: // 1. Puts the exchange into a 'dispatched' state, otherwise the request will be terminated when // the endpoint HttpHandler returns. See Connectors.executeRootHandler for more information. // 2. Avoids racing completion with the current handler chain. The dispatched task will be executed // after this Endpoints HttpHandler returns. Otherwise an unlucky future could race // HttpServerExchange.isInCall transitioning from true -> false, causing hte dispatch task not // to execute. DeferredTracer tracer = new DeferredTracer("Undertow: Async Result"); exchange.dispatch(() -> Futures.addCallback( future, new FutureCallback() { @Override public void onSuccess(@Nullable T result) { exchange.dispatch( wrapCallback(_serverExchange -> returnValueWriter.write(result, exchange), tracer)); } @Override public void onFailure(Throwable throwable) { exchange.dispatch(wrapCallback( serverExchange -> exceptionHandler.handle( serverExchange, getThrowable(serverExchange, throwable, requestAsyncTimeout)), tracer)); } }, DIRECT_EXECUTOR)); } /** * Returns the provided {@link Throwable} unless the task has been canceled, * in which case, additional information is provided. */ private static Throwable getThrowable(HttpServerExchange exchange, Throwable failure, Duration timeout) { if (failure instanceof CancellationException && Boolean.TRUE.equals(exchange.getAttachment(TIMED_OUT))) { return new ServiceException( ASYNC_REQUEST_PROCESSING_TIMEOUT, failure, SafeArg.of("timeoutSeconds", timeout.getSeconds())); } return failure; } private HttpHandler wrapCallback(HttpHandler action, DeferredTracer tracer) { return exchange -> tracer.withTrace(() -> { try { action.handleRequest(exchange); } catch (Throwable t) { exceptionHandler.handle(exchange, t); } return null; }); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy