dongjoon-hyun commented on code in PR #918:
URL: 
https://github.com/apache/spark-kubernetes-operator/pull/918#discussion_r4268244084


##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptor.java:
##########
@@ -103,21 +123,36 @@ public void before(BasicBuilder builder, HttpRequest 
request, RequestTags tags)
   }
 
   /**
-   * Called after a non-WebSocket HTTP response is received. The body might or 
might not be already
-   * consumed.
+   * Called after an HTTP response is received. The body might or might not be 
already consumed.
    *
    * <p>Should be used to analyze response codes and headers, original 
response shouldn't be
    * altered.
    *
+   * <p>fabric8 also calls this for a WebSocket upgrade response with a {@code 
null} consumer, and
+   * again for an HTTP request re-sent after another interceptor's {@link
+   * Interceptor#afterFailure(BasicBuilder, HttpResponse, RequestTags)} 
returns {@code true}, e.g.
+   * fabric8's {@code TokenRefreshInterceptor} on a {@code 401}. The two 
values of a request re-sent
+   * this way add up to the time from sending it until the re-sent response 
arrives.
+   *
    * @param request the original request sent to the server.
    * @param response the response received from the server.
+   * @param consumer the consumer of the response body, or {@code null} for a 
WebSocket upgrade.
    */
   @Override
+  @SuppressWarnings("PMD.CompareObjectsWithEquals")
   public void after(
       HttpRequest request,
       HttpResponse<?> response,
       AsyncBody.Consumer<List<ByteBuffer>> consumer) {
-    updateResponseMetrics(response, System.nanoTime());
+    TimedConsumer timedConsumer = consumer == null ? null : 
consumer.unwrap(TimedConsumer.class);
+    while (timedConsumer != null && timedConsumer.owner() != this) {

Review Comment:
   Do we need the `owner` check and this loop? `KubernetesClientFactory` 
registers interceptors with 
`addOrReplaceInterceptor(interceptor.getClass().getName(), ...)`, so two 
instances of the same class replace each other, and `SparkOperator` creates 
only one `KubernetesMetricsInterceptor`. The only way to get two 
`TimedConsumer`s into one chain is an anonymous subclass, as in 
`testResponseLatencyOfTwoInterceptors`. Without that case, a plain 
`consumer.unwrap(TimedConsumer.class)` would be enough, and we could drop the 
`owner` component, the `PMD.CompareObjectsWithEquals` suppression, and that 
test.



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptor.java:
##########
@@ -140,7 +175,8 @@ public CompletableFuture<Boolean> afterFailure(
   /**
    * Called after a connection attempt fails.
    *
-   * <p>This method will be invoked on each failed connection attempt.
+   * <p>fabric8 invokes this method only while the request has a retry left, 
even if it does not

Review Comment:
   This Javadoc correction looks right to me, but it isn't related to the 
latency fix. The same applies to the new doc sentence that a rejected WebSocket 
upgrade is counted several times in `kubernetes.client.http.response` and the 
response code meters. Could we move these to a separate PR, to keep this one 
scoped to SPARK-59928?



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptor.java:
##########
@@ -233,4 +267,26 @@ public Optional<Pair<String, String>> 
parseNamespaceScopedResource(String path)
       return Optional.empty();
     }
   }
+
+  /**
+   * Consumer of a response body which carries the start time of its request, 
restarted at each
+   * response so that the values of a re-sent request add up to its latency. 
It also carries the
+   * interceptor which created it, so that each interceptor on a client uses 
its own start time.
+   */
+  private record TimedConsumer(

Review Comment:
   nit. A `record` is meant to be a shallow-immutable data carrier, but 
`startTimeNanos()` returns an `AtomicLong` that `after(...)` mutates. The 
record also generates `equals`/`hashCode`/`toString` over the whole delegate 
chain and the owner interceptor, and this takes two allocations per request. A 
`private static final class` with an explicit `delegate` and one `volatile 
long` (or `AtomicLong`) field would do the same job in one allocation.



##########
docs/configuration.md:
##########
@@ -264,6 +264,12 @@ has a retry left for it, even if it does not retry it, 
e.g. on a request timeout
 a client which never retries, i.e. a Kubernetes event write or the recording 
of the Kueue
 `PodsReady` condition, is not counted there when it gets no response.
 
+`kubernetes.client.http.response.latency.nanos` does not include the time to 
read the response

Review Comment:
   The start time is taken in `consumer(...)`, which runs before 
`consumeBytesDirect(...)`. So the value also includes client-side time, such as 
waiting for a Vert.x pool connection and connection/TLS setup. For example, 
under many concurrent reconciles, this metric can rise while the API server 
itself is fast. The metrics table describes it as "HTTP response latency from 
the Kubernetes API Server". Could we mention that it is measured from when the 
request is handed to the HTTP client?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to