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; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.lang.reflect.*; 022import java.time.*; 023import java.util.*; 024import java.util.concurrent.*; 025import java.util.function.*; 026 027import org.apache.juneau.commons.reflect.*; 028import org.apache.juneau.http.tracing.TraceContextCarrier; 029import org.apache.juneau.http.response.*; 030import org.apache.juneau.rest.server.arg.*; 031import org.apache.juneau.rest.server.metrics.*; 032import org.apache.juneau.rest.server.stats.*; 033import org.apache.juneau.rest.server.tracing.*; 034 035/** 036 * A specialized invoker for methods that are called during a servlet request. 037 * 038 * <h5 class='section'>See Also:</h5><ul> 039 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestContext">RestContext</a> 040 * </ul> 041 */ 042@SuppressWarnings({ 043 "java:S3776" // invoke() resolves arguments, sets up metrics/tracing scope, invokes the method and handles errors in a single flow 044}) 045public class RestOpInvoker extends MethodInvoker { 046 047 private final RestOpArg[] opArgs; 048 private final Supplier<Object> resourceSupplier; 049 050 /** 051 * Constructor. 052 * 053 * @param m The method being wrapped. 054 * @param opArgs The parameter resolvers. 055 * @param stats The instrumentor. 056 */ 057 public RestOpInvoker(java.lang.reflect.Method m, RestOpArg[] opArgs, MethodExecStats stats) { 058 this(m, opArgs, stats, null); 059 } 060 061 /** 062 * Constructor. 063 * 064 * @param m The method being wrapped. 065 * @param opArgs The parameter resolvers. 066 * @param stats The instrumentor. 067 * @param resourceSupplier Optional resource supplier. When <jk>null</jk>, falls back to {@link RestSession#getResource()}. 068 */ 069 public RestOpInvoker(java.lang.reflect.Method m, RestOpArg[] opArgs, MethodExecStats stats, Supplier<Object> resourceSupplier) { 070 super(m, stats); 071 this.opArgs = opArgs; 072 this.resourceSupplier = resourceSupplier; 073 } 074 075 /** 076 * Invokes this method from the specified {@link RestOpSession}. 077 * 078 * @param opSession The REST call. Must not be <jk>null</jk>. 079 * @throws Exception If an error occurred during either parameter resolution or method invocation. 080 */ 081 public void invoke(RestOpSession opSession) throws Exception { 082 invoke(opSession, false); 083 } 084 085 /** 086 * Invokes this method as the main {@code @RestOp} handler from the specified {@link RestOpSession}, 087 * firing per-request observability hooks ({@link MetricsRecorder} and {@link TracerHook}) around the 088 * underlying invocation. 089 * 090 * <p> 091 * Called by {@link RestOpSession#run()} for the {@code @RestOp}-annotated handler method. Pre / post 092 * call methods (those invoked via {@link RestContext#preCall(RestOpSession)} / 093 * {@link RestContext#postCall(RestOpSession)}) continue to use {@link #invoke(RestOpSession)} so the 094 * observability boundary stays anchored on the user-facing handler. 095 * 096 * <h5 class='section'>Virtual-thread dispatch</h5> 097 * <p> 098 * When the operation opts into virtual threads via {@code @Rest(virtualThreads=true)} or 099 * {@code @RestOp(virtualThreads=true)} and the runtime is Java 21+, the entire body of this method 100 * (parameter resolution, observability scope opening, handler invocation, observability close) is 101 * submitted to the resource's virtual-thread executor and the request thread blocks on completion. 102 * That keeps observability scoping correct (open and close run on the same thread) while letting 103 * blocking I/O inside the handler park a virtual thread instead of the carrier. 104 * 105 * <h5 class='section'>{@link CompletableFuture} returns</h5> 106 * <p> 107 * When the handler returns a {@link CompletionStage}, the observability close is deferred to the 108 * future's {@code whenComplete} callback so metrics and traces capture the actual completion time 109 * and outcome — not the synchronous "future returned" moment. 110 * 111 * @param opSession The REST call. Must not be <jk>null</jk>. 112 * @throws Exception If an error occurred during either parameter resolution or method invocation. 113 */ 114 @SuppressWarnings({ 115 "java:S1181" // Throwable (incl. Error) is intentionally captured to propagate it across the virtual-thread boundary; the caller rethrows Errors as-is. 116 }) 117 public void invokeOp(RestOpSession opSession) throws Exception { 118 var vtExec = opSession.getContext().isVirtualThreadsEnabled() 119 ? opSession.getRestContext().getVirtualThreadExecutor() 120 : null; 121 122 if (vtExec == null) { 123 invoke(opSession, true); 124 return; 125 } 126 127 var f = new CompletableFuture<Void>(); 128 vtExec.execute(() -> { 129 try { 130 invoke(opSession, true); 131 f.complete(null); 132 } catch (Throwable t) { 133 f.completeExceptionally(t); 134 } 135 }); 136 137 try { 138 f.get(); 139 } catch (ExecutionException ee) { 140 Throwable cause = ee.getCause() == null ? ee : ee.getCause(); 141 if (cause instanceof Exception cause2) 142 throw cause2; 143 if (cause instanceof Error cause2) 144 throw cause2; 145 throw new InternalServerError(cause); 146 } catch (InterruptedException ie) { 147 Thread.currentThread().interrupt(); 148 throw new InternalServerError(ie); 149 } 150 } 151 152 @SuppressWarnings({ 153 "resource" // BeanStore is not owned here; its lifecycle is managed by RestContext. 154 }) 155 private void invoke(RestOpSession opSession, boolean observable) throws Exception { 156 var args = new Object[opArgs.length]; 157 for (var i = 0; i < opArgs.length; i++) { 158 ParameterInfo pi = inner().getParameter(i); 159 try { 160 args[i] = opArgs[i].resolve(opSession); 161 } catch (BasicHttpException e) { 162 throw e; 163 } catch (Exception e) { 164 throw new BadRequest(e, "Could not resolve parameter %s of type '%s' on method '%s'.", i, pi.getParameterType(), getFullName()); 165 } 166 } 167 168 RestRequest req = opSession.getRequest(); 169 RestResponse res = opSession.getResponse(); 170 171 MetricsRecorder recorder = NoOpMetricsRecorder.INSTANCE; 172 Scope tracerScope = NoOpTracerHook.NoOpScope.INSTANCE; 173 long startNanos = 0L; 174 Throwable observed = null; 175 boolean observabilityDeferred = false; 176 boolean scopeStashed = false; 177 var opContext = opSession.getContext(); 178 // observable=true means this is the @RestOp handler (not a pre/post call), AND observability is not 179 // explicitly disabled for this operation via @Rest(observability="false") or @RestOp(observability="false"). 180 boolean effectivelyObservable = observable && opContext.isObservabilityEnabled(); 181 if (effectivelyObservable) { 182 var bs = opSession.getRestContext().getBeanStore(); 183 recorder = bs.getBean(MetricsRecorder.class).orElse(NoOpMetricsRecorder.INSTANCE); 184 var tracer = bs.getBean(TracerHook.class).orElse(NoOpTracerHook.INSTANCE); 185 if (tracer == NoOpTracerHook.INSTANCE) { 186 // No-tracer fast path: resolve neither a TraceContextExtractor nor a carrier/operation, and 187 // touch no request-scoped tracing attribute — matches the pre-existing zero-allocation contract. 188 tracerScope = tracer.startSpan(req); 189 } else { 190 // Active tracer: an optional TraceContextExtractor gets a look at the fully-resolved operation 191 // arguments (parsed request body / params included) before span creation, so it can recognize a 192 // non-HTTP carrier (e.g. an MCP request's params._meta) that HTTP headers alone wouldn't expose. 193 Optional<TraceContextExtractor> extractor = bs.getBean(TraceContextExtractor.class); 194 TraceContextCarrier carrier = null; 195 TraceOperation operation = TraceOperation.DEFAULT; 196 if (extractor.isPresent()) { 197 carrier = extractor.get().extract(req, args).orElse(null); 198 operation = extractor.get().operation(req, args); 199 } 200 tracerScope = tracer.startSpan(req, carrier, operation); 201 // Stash the active Scope as a request-scoped neutral tracing observation so a dated MCP adapter 202 // running inside this handler invocation (and already holding this same RestRequest through its 203 // own request-scoped BeanStore entry) can call Scope#recordRpcError(int, String) before returning 204 // a JSON-RPC error response. Cleared below once this invocation's synchronous/deferred 205 // observability lifecycle closes the scope. 206 req.setAttribute(TraceContextResponseProcessor.ATTR_SCOPE, tracerScope); 207 scopeStashed = true; 208 } 209 startNanos = System.nanoTime(); 210 } 211 212 try { 213 RestSession session = opSession.getRestSession(); 214 215 var target = resourceSupplier == null ? session.getResource() : resourceSupplier.get(); 216 Object output = super.invoke(target, args); 217 218 if (! inner().hasReturnType(Void.TYPE) && (nn(output) || ! res.getOutputStreamCalled())) 219 res.setContent(output); 220 221 if (effectivelyObservable && output instanceof CompletionStage<?> output2) { 222 observabilityDeferred = true; 223 deferObservability(output2, recorder, tracerScope, startNanos, opSession, opContext.getMetricName(), opContext.getMetricTags(), scopeStashed); 224 } 225 226 } catch (IllegalAccessException | IllegalArgumentException e) { 227 observed = e; 228 throw new InternalServerError(e, "Error occurred invoking method '%s'.", inner().getNameFull()); 229 } catch (InvocationTargetException e) { 230 Throwable e2 = e.getTargetException(); 231 observed = e2; 232 res.setStatus(500); // May be overridden later. 233 res.setContent(opSession.getRestContext().convertThrowable(e2)); 234 } finally { 235 if (effectivelyObservable && ! observabilityDeferred) { 236 int status; 237 if (observed != null) 238 status = deriveStatus(observed); 239 else { 240 status = res.getStatus(); 241 if (status == 0) // Defensive: a non-container HttpServletResponse may report 0 when unset. 242 status = 200; 243 } 244 try { 245 tracerScope.setStatusCode(status); 246 if (nn(observed)) 247 tracerScope.setError(observed); 248 } finally { 249 try { 250 tracerScope.close(); 251 } finally { 252 if (scopeStashed) 253 req.setAttribute(TraceContextResponseProcessor.ATTR_SCOPE, null); 254 var elapsed = Duration.ofNanos(System.nanoTime() - startNanos); 255 var pathTemplate = resolveUriTemplate(opSession); 256 recorder.record(getFullName(), req.getMethod(), pathTemplate, status, elapsed, observed, opContext.getMetricName(), opContext.getMetricTags()); 257 } 258 } 259 } 260 } 261 } 262 263 @SuppressWarnings({ 264 "java:S107" // Parameter list is fixed by the framework invocation contract; grouping into a holder would not improve clarity. 265 }) 266 private void deferObservability(CompletionStage<?> stage, MetricsRecorder recorder, Scope tracerScope, 267 long startNanos, RestOpSession opSession, String metricName, String metricTags, boolean scopeStashed) { 268 var fullName = getFullName(); 269 var req = opSession.getRequest(); 270 var httpMethod = req.getMethod(); 271 var pathTemplate = resolveUriTemplate(opSession); 272 stage.whenComplete((value, error) -> { 273 Throwable err = unwrapCompletionError(error); 274 int status = deriveStatus(err); 275 try { 276 tracerScope.setStatusCode(status); 277 if (err != null) 278 tracerScope.setError(err); 279 } finally { 280 try { 281 tracerScope.close(); 282 } finally { 283 if (scopeStashed) 284 req.setAttribute(TraceContextResponseProcessor.ATTR_SCOPE, null); 285 var elapsed = Duration.ofNanos(System.nanoTime() - startNanos); 286 recorder.record(fullName, httpMethod, pathTemplate, status, elapsed, err, metricName, metricTags); 287 } 288 } 289 }); 290 } 291 292 private static Throwable unwrapCompletionError(Throwable t) { 293 if (t instanceof CompletionException && t.getCause() != null) 294 return t.getCause(); 295 return t; 296 } 297 298 private static int deriveStatus(Throwable err) { 299 if (err == null) 300 return 200; 301 if (err instanceof BasicHttpException err2) 302 return err2.getStatusCode(); 303 return 500; 304 } 305 306 private static String resolveUriTemplate(RestOpSession opSession) { 307 try { 308 var pp = opSession.getContext().getPathPattern(); 309 return pp == null ? "" : pp; 310 } catch (RuntimeException e) { 311 // Defensive: getPathPattern() dereferences pathMatchers[0]; protect against any unusual routing setup. 312 return ""; 313 } 314 } 315}