001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.processor;
018
019import static jakarta.servlet.http.HttpServletResponse.*;
020import static org.apache.juneau.commons.utils.ObjectUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022
023import java.io.*;
024import java.util.concurrent.*;
025import java.util.concurrent.atomic.*;
026import java.util.logging.*;
027
028import org.apache.juneau.http.response.*;
029import org.apache.juneau.rest.server.*;
030import org.apache.juneau.rest.server.logging.*;
031
032import jakarta.servlet.*;
033import jakarta.servlet.http.*;
034
035/**
036 * Response processor that unwraps {@link CompletableFuture} / {@link CompletionStage} return values from
037 * {@code @RestOp}-annotated methods and bridges them to the servlet container's
038 * {@link AsyncContext asynchronous} request lifecycle.
039 *
040 * <p>
041 * When a handler returns a future, this processor is the FIRST processor in the default chain
042 * (see {@link org.apache.juneau.rest.server.config.DefaultConfig}). It detects the {@link CompletionStage}
043 * content, calls {@link HttpServletRequest#startAsync()} on the underlying servlet request, and
044 * registers a {@link CompletionStage#whenComplete(java.util.function.BiConsumer) whenComplete}
045 * callback that:
046 *
047 * <ul>
048 *    <li>On success — installs the unwrapped value via {@link RestResponse#setContent(Object)}, re-runs
049 *       {@link RestContext#processResponse(RestOpSession)} (this processor sees a non-future and falls
050 *       through to {@code SerializedPojoProcessor} et al.), flushes the response, and
051 *       calls {@link AsyncContext#complete()}.
052 *    <li>On failure — runs the throwable through {@link RestContext#convertThrowable(Throwable)} so
053 *       the existing error pipeline (including {@link ThrowableProcessor} and
054 *       {@link ProblemDetailsProcessor}) handles it, then completes the {@code AsyncContext}.
055 * </ul>
056 *
057 * <h5 class='section'>Synchronous fallback</h5>
058 * <p>
059 * In environments where {@link HttpServletRequest#startAsync()} is unsupported (most notably
060 * Juneau's {@code MockServletRequest}, which returns {@code null}), this processor falls back to a
061 * blocking {@link CompletableFuture#get(long, TimeUnit) get(timeout)} on the future and then
062 * returns {@link #RESTART} so the rest of the chain runs synchronously on the unwrapped value.
063 * This keeps the unit-test surface working without requiring a real servlet container while
064 * preserving full {@code AsyncContext} semantics in production.
065 *
066 * <h5 class='section'>Timeout</h5>
067 * <p>
068 * The async timeout is configurable via {@code @Rest(asyncTimeoutMillis)} /
069 * {@code @RestOp(asyncTimeoutMillis)} (default 30s). On timeout the processor cancels the future
070 * with {@code mayInterruptIfRunning=true} and writes a {@link HttpServletResponse#SC_GATEWAY_TIMEOUT}
071 * response.
072 *
073 * <h5 class='section'>Bare {@link Future} rejection</h5>
074 * <p>
075 * Bare {@link Future} return types (anything that is a {@code Future} but not a
076 * {@link CompletionStage}) are rejected with an {@link InternalServerError} —
077 * polling a {@code Future} would block the request thread without any of the cancellation /
078 * cooperation guarantees that {@link CompletableFuture} provides. Handlers that need async dispatch
079 * must return {@link CompletableFuture} or {@link CompletionStage}.
080 *
081 * <h5 class='section'>Completion executor</h5>
082 * <p>
083 * When {@code @Rest(asyncCompletionExecutor="poolName")} or the per-op equivalent is set, this processor
084 * switches from {@code future.whenComplete(callback)} to {@code future.whenCompleteAsync(callback, executor)},
085 * routing the response-handler work through the named {@link Executor} bean. The MDC
086 * snapshot (see below) is taken on the request thread <em>before</em> the executor routing so the MDC context
087 * is always available regardless of which thread the executor picks.
088 *
089 * <h5 class='section'>Thread-local caveats</h5>
090 * <p>
091 * Anything carried via {@link ThreadLocal} (SLF4J MDC, security contexts) does NOT survive the
092 * async boundary — the {@code whenComplete} callback typically runs on a different thread than the
093 * one that produced the future. {@code RequestAttributes}, {@link org.apache.juneau.commons.svl.VarResolverSession},
094 * and {@link java.util.Locale} are request-scoped and survive correctly.
095 *
096 * <h5 class='section'>See Also:</h5><ul>
097 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerAsyncDispatch">Async Returns + Virtual-Thread Dispatch</a>
098 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ResponseProcessors">Response Processors</a>
099 * </ul>
100 *
101 * @since 10.0.0
102 */
103@SuppressWarnings({
104   "java:S1141", // finalizeAsync() nests try/finally blocks so each completion step (debug emit, request close, async-context complete) runs even if an earlier one fails
105   "java:S3776" // finalizeAsync() combines timeout, error and success handling with layered fallback and cleanup in one method
106})
107public class AsyncResponseProcessor implements ResponseProcessor {
108
109   private static final Logger LOG = Logger.getLogger(AsyncResponseProcessor.class.getName());
110
111   /**
112    * Request attribute set by this processor when the {@code AsyncContext} path is taken so
113    * downstream lifecycle code (specifically {@link RestOpSession#finish()} and
114    * {@link RestSession#finish()}) can skip the synchronous {@code flushBuffer()} / {@code req.close()}
115    * — the {@code AsyncContext.complete()} call inside the {@code whenComplete} callback is
116    * responsible for committing the response.
117    *
118    * @since 10.0.0
119    */
120   public static final String ATTR_ASYNC_DISPATCH_OWNED = "org.apache.juneau.rest.server.async.dispatchOwned";
121
122   /** Default async timeout in milliseconds when no annotation override is supplied. */
123   public static final long DEFAULT_ASYNC_TIMEOUT_MILLIS = 30_000L;
124
125   @Override /* Overridden from ResponseProcessor */
126   public int process(RestOpSession opSession) throws IOException, BasicHttpException {
127      var res = opSession.getResponse();
128      var content = res.getContent().orElse(null);
129
130      if (content == null)
131         return NEXT;
132
133      if (content instanceof CompletionStage<?> stage)
134         return processAsync(opSession, stage);
135
136      if (content instanceof Future<?>) {
137         throw new InternalServerError(
138            "Bare java.util.concurrent.Future is not supported as a return type from @RestOp methods. "
139               + "Return CompletableFuture or CompletionStage instead, or block on the result yourself before returning."
140         );
141      }
142
143      return NEXT;
144   }
145
146   private int processAsync(RestOpSession opSession, CompletionStage<?> stage) throws BasicHttpException {
147      var timeoutMs = resolveTimeoutMillis(opSession);
148      var cf = stage.toCompletableFuture();
149      var req = opSession.getRequest().getHttpServletRequest();
150
151      // True async path requires both isAsyncSupported() and a non-null AsyncContext from startAsync().
152      AsyncContext asyncCtx = null;
153      if (req.isAsyncSupported()) {
154         try {
155            asyncCtx = req.startAsync();
156            if (asyncCtx != null && timeoutMs > 0)
157               asyncCtx.setTimeout(timeoutMs);
158         } catch (IllegalStateException e) {
159            asyncCtx = null;  // Already committed or async not actually supported on this request.
160         }
161      }
162
163      if (asyncCtx == null)
164         return processSyncFallback(opSession, cf, timeoutMs);
165
166      dispatchAsync(opSession, cf, asyncCtx, timeoutMs);
167      return FINISHED;
168   }
169
170   @SuppressWarnings({
171      "java:S1166", // CancellationException is intentionally swallowed; we surface it as a 500 via convertThrowable.
172      "java:S2142", // We re-set the interrupt flag immediately and surface as a 500.
173      "java:S3516" // Always returns RESTART by design: every outcome (success or error) re-runs the chain on the now-synchronous content.
174   })
175   private int processSyncFallback(RestOpSession opSession, CompletableFuture<?> cf, long timeoutMs) {
176      var res = opSession.getResponse();
177      try {
178         Object value = (timeoutMs > 0) ? cf.get(timeoutMs, TimeUnit.MILLISECONDS) : cf.get();
179         res.setContent(value);
180         return RESTART;
181      } catch (TimeoutException te) {
182         cf.cancel(true);
183         res.setStatus(SC_GATEWAY_TIMEOUT);
184         res.setContent(opSession.getRestContext().convertThrowable(
185            new GatewayTimeout("Async response timed out after " + timeoutMs + "ms.")
186         ));
187         return RESTART;
188      } catch (CancellationException ce) {
189         res.setStatus(SC_INTERNAL_SERVER_ERROR);
190         res.setContent(opSession.getRestContext().convertThrowable(ce));
191         return RESTART;
192      } catch (ExecutionException ee) {
193         Throwable cause = or(ee.getCause(), ee);
194         res.setStatus(SC_INTERNAL_SERVER_ERROR);
195         res.setContent(opSession.getRestContext().convertThrowable(cause));
196         return RESTART;
197      } catch (InterruptedException ie) {
198         Thread.currentThread().interrupt();
199         cf.cancel(true);
200         res.setStatus(SC_INTERNAL_SERVER_ERROR);
201         res.setContent(opSession.getRestContext().convertThrowable(ie));
202         return RESTART;
203      }
204   }
205
206   private void dispatchAsync(RestOpSession opSession, CompletableFuture<?> cf, AsyncContext asyncCtx, long timeoutMs) {
207      // Publish async ownership + the resolved debug snapshot on the request thread BEFORE registering any completion
208      // callback, so an already-completed future (whose whenComplete runs inline) never observes a null snapshot and
209      // finish() reliably observes async ownership and skips synchronous flush/emit.
210      markAsyncOwnedAndSnapshot(opSession);
211
212      var done = new AtomicBoolean(false);
213
214      asyncCtx.addListener(new AsyncListener() {
215         @Override public void onComplete(AsyncEvent ev) { /* no-op */ }
216         @Override public void onStartAsync(AsyncEvent ev) { /* no-op */ }
217         @Override public void onError(AsyncEvent ev) {
218            finalizeAsync(opSession, cf, asyncCtx, ev.getThrowable(), done, /* timeout */ false, timeoutMs);
219         }
220         @Override public void onTimeout(AsyncEvent ev) {
221            finalizeAsync(opSession, cf, asyncCtx, null, done, /* timeout */ true, timeoutMs);
222         }
223      });
224
225      // MDC bridge: snapshot request-thread MDC at dispatch time, then restore it on the
226      // completion thread so log statements inside the whenComplete callback see the same diagnostic
227      // context (requestId, userId, etc.) that was set by upstream filters.
228      // NOTE: the MDC wrap happens BEFORE the executor routing so the snapshot is always
229      // available regardless of which thread the executor picks.
230      var mdcSnapshot = opSession.getRestContext().isMdcAsyncPropagation()
231         ? MdcAsyncListener.snapshot()
232         : null;
233
234      var callback = MdcAsyncListener.wrap(
235         (value, error) -> finalizeAsync(opSession, cf, asyncCtx, error, done, /* timeout */ false, timeoutMs, value),
236         mdcSnapshot
237      );
238
239      // If a per-op or resource-level completion executor is configured, route the callback
240      // through it via whenCompleteAsync; otherwise use the natural completion thread.
241      var executor = opSession.getContext().getAsyncCompletionExecutor();
242      if (executor != null)
243         cf.whenCompleteAsync(callback, executor);
244      else
245         cf.whenComplete(callback);
246   }
247
248   private void finalizeAsync(RestOpSession opSession, CompletableFuture<?> cf, AsyncContext asyncCtx,
249         Throwable error, AtomicBoolean done, boolean timeout, long timeoutMs) {
250      finalizeAsync(opSession, cf, asyncCtx, error, done, timeout, timeoutMs, null);
251   }
252
253   @SuppressWarnings({
254      "java:S107" // The 8 parameters are the internal async-finalization context (session, future, async ctx, error, done-latch, timeout flag, timeout ms, resolved value); a holder object would obscure this hot-path call and its sibling overload.
255   })
256   private void finalizeAsync(RestOpSession opSession, CompletableFuture<?> cf, AsyncContext asyncCtx,
257         Throwable error, AtomicBoolean done, boolean timeout, long timeoutMs, Object value) {
258      if (! done.compareAndSet(false, true))
259         return;
260
261      var res = opSession.getResponse();
262      var session = opSession.getRestSession();
263
264      try {
265         if (timeout) {
266            cf.cancel(true);
267            res.setStatus(SC_GATEWAY_TIMEOUT);
268            var cause = new GatewayTimeout("Async response timed out after " + timeoutMs + "ms.");
269            session.exception(cause);
270            res.setContent(opSession.getRestContext().convertThrowable(cause));
271         } else if (error != null) {
272            Throwable cause = unwrap(error);
273            res.setStatus(SC_INTERNAL_SERVER_ERROR);
274            session.exception(cause);
275            res.setContent(opSession.getRestContext().convertThrowable(cause));
276         } else {
277            res.setContent(value);
278         }
279
280         opSession.getRestContext().processResponse(opSession);
281         res.flushBuffer();
282      } catch (Exception e) {
283         LOG.log(Level.WARNING, e, () -> "Async response finalization failed: " + e.getMessage());
284         session.exception(e);
285         try {
286            if (! res.getHttpServletResponse().isCommitted())
287               res.getHttpServletResponse().sendError(SC_INTERNAL_SERVER_ERROR);
288         } catch (Exception inner) {
289            LOG.log(Level.FINEST, inner, () -> "Async response error-fallback also failed: " + inner.getMessage());
290         }
291      } finally {
292         // Completion-path emission runs AFTER the body/headers are written, then request close, then async-context
293         // completion. Nested try/finally isolates each step: a debug-emit failure cannot suppress request close,
294         // and a request-close failure cannot suppress async-context completion.
295         try {
296            RestDebugPipeline.emitOnCompletion(session);
297         } finally {
298            try {
299               closeRequestQuietly(opSession);
300            } finally {
301               completeQuietly(asyncCtx);
302            }
303         }
304      }
305   }
306
307   /**
308    * Publishes async ownership and the resolved debug snapshot on the request thread, before any completion callback or
309    * streaming subscription is registered.
310    *
311    * <p>
312    * Shared by {@link AsyncResponseProcessor} and the reactive streaming processor so both async-owned families use the
313    * same race-safe handoff: the ownership flag makes {@link RestSession#finish()} skip synchronous flush/emit, and the
314    * snapshot carries the resolved logger/formatter/detail-tier across the async hop.
315    *
316    * @param opSession The operation session being handed off to async dispatch. Must not be <jk>null</jk>.
317    */
318   public static void markAsyncOwnedAndSnapshot(RestOpSession opSession) {
319      opSession.getRequest().getHttpServletRequest().setAttribute(ATTR_ASYNC_DISPATCH_OWNED, Boolean.TRUE);
320      RestDebugPipeline.captureAsyncSnapshot(opSession.getRestSession());
321   }
322
323   private static void closeRequestQuietly(RestOpSession opSession) {
324      try {
325         opSession.closeRequest();
326      } catch (RuntimeException e) {
327         LOG.log(Level.FINEST, e, () -> "Async request close failed: " + e.getMessage());
328      }
329   }
330
331   private static void completeQuietly(AsyncContext asyncCtx) {
332      try {
333         asyncCtx.complete();
334      } catch (IllegalStateException ise) {
335         LOG.log(Level.FINEST, ise, () -> "AsyncContext.complete() raced with the container: " + ise.getMessage());
336      }
337   }
338
339   private static Throwable unwrap(Throwable t) {
340      if (t instanceof CompletionException && t.getCause() != null)
341         return t.getCause();
342      if (t instanceof ExecutionException && t.getCause() != null)
343         return t.getCause();
344      return t;
345   }
346
347   private static long resolveTimeoutMillis(RestOpSession opSession) {
348      var op = opSession.getContext().getAsyncTimeoutMillis();
349      if (op > 0)
350         return op;
351      var ctx = opSession.getRestContext().getAsyncTimeoutMillis();
352      return ctx > 0 ? ctx : DEFAULT_ASYNC_TIMEOUT_MILLIS;
353   }
354
355   /**
356    * Returns whether the given session has been handed off to async dispatch. Inspected by
357    * {@link RestOpSession#finish()} / {@link RestSession#finish()} to skip the synchronous
358    * {@code flushBuffer()} call when the {@code AsyncContext.complete()} path will commit the
359    * response itself.
360    *
361    * @param opSession The session to check.
362    *    <br>Can be <jk>null</jk> (returns <jk>false</jk>).
363    * @return {@code true} if {@link #ATTR_ASYNC_DISPATCH_OWNED} has been set.
364    */
365   public static boolean isAsyncDispatchOwned(RestOpSession opSession) {
366      if (opSession == null)
367         return false;
368      return isAsyncDispatchOwned(opSession.getRequest().getHttpServletRequest());
369   }
370
371   /**
372    * Variant of {@link #isAsyncDispatchOwned(RestOpSession)} that operates directly on the
373    * underlying servlet request — used by {@link RestSession#finish()} where the
374    * {@link RestOpSession} may not be available.
375    *
376    * @param req The servlet request.
377    *    <br>Can be <jk>null</jk> (returns <jk>false</jk>).
378    * @return {@code true} if {@link #ATTR_ASYNC_DISPATCH_OWNED} has been set.
379    */
380   public static boolean isAsyncDispatchOwned(HttpServletRequest req) {
381      if (req == null)
382         return false;
383      return isTrue(req.getAttribute(ATTR_ASYNC_DISPATCH_OWNED));
384   }
385}