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}