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


##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptor.java:
##########
@@ -103,21 +125,38 @@ 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} or {@code
+   * BackwardsCompatibilityInterceptor} for a legacy API version. The two 
values of a request

Review Comment:
   Sorry, my comment in the previous round was wrong. fabric8 installs 
`BackwardsCompatibilityInterceptor` only when 
`kubernetes.backwardsCompatibilityInterceptor.disable` is set to `false`, 
because its default is `"true"` (fabric8 7.8.0 `HttpClientUtils.java` L75 and 
L117-122, the same in 7.9.0 and 8.0.0). So with the default interceptors, 
`TokenRefreshInterceptor` is the only one which re-sends a request. Could you 
drop `or BackwardsCompatibilityInterceptor for a legacy API version` here, and 
the same example from `What changes were proposed in this pull request?` of the 
PR description, which becomes the commit message?
   
   FYI, when it is enabled, a request can be re-sent twice, e.g. `401` -> `404` 
-> `200`, and is then recorded three times. The values still add up, so 
`recorded twice` in the docs is fine for the default interceptors.
   
   nit: For an HTTP request, fabric8 calls the 
`afterFailure(HttpRequest.Builder, HttpResponse, RequestTags)` overload 
(`StandardHttpClient.java` L124), which delegates to the `BasicBuilder` one by 
default. So linking that overload would be slightly more precise. The current 
link came from my suggestion, so either is fine.
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptor.java:
##########
@@ -103,21 +125,38 @@ 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} or {@code
+   * BackwardsCompatibilityInterceptor} for a legacy API version. 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
   public void after(
       HttpRequest request,
       HttpResponse<?> response,
       AsyncBody.Consumer<List<ByteBuffer>> consumer) {
-    updateResponseMetrics(response, System.nanoTime());
+    Objects.requireNonNull(response);
+    TimedConsumer timedConsumer = consumer == null ? null : 
consumer.unwrap(TimedConsumer.class);

Review Comment:
   Now that the `owner` lookup is gone, as I suggested earlier, the corner case 
of my earlier comment is back. `unwrap(TimedConsumer.class)` returns the 
outermost `TimedConsumer` for every `KubernetesMetricsInterceptor` on a client, 
and `restart()` resets its start time. So a second instance, e.g. of a subclass 
added through `SparkOperator#getClientInterceptors`, records nearly zero for 
every response, without a warning. In my test with two instances on one client, 
the first recorded ~205 ms and the second 47-58 µs for the same response.
   
   Since this can't happen with the operator's own client, mentioning it in the 
Javadoc would be enough, e.g. in `consumer(...)` (L100-104):
   
   > Only one instance per client is supported, because every instance restarts 
the outermost {@code TimedConsumer}.
   



##########
docs/configuration.md:
##########
@@ -264,6 +264,13 @@ 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` is measured from when a 
request is handed to the
+HTTP client, so it also includes waiting for a connection. It does not include 
the time to read
+the response body. A request re-sent by another interceptor, e.g. after a 
`401`, is recorded
+twice, and the two values add up to the time from sending it until the re-sent 
response arrives.

Review Comment:
   This holds for a `401` which is re-sent. But when the token refresh fails or 
gets no new token, e.g. with an `OAuthTokenProvider` or an exec credential 
plugin, the `401` is not re-sent and has only one value. Since `after(...)` 
runs after the synchronous `afterFailure(...)` work, that value includes the 
whole refresh. In my test with an `OAuthTokenProvider` which blocked for 500 ms 
and returned `null`, a `401` which the server returned after ~18 ms was 
recorded as ~529 ms.
   
   How about adding a sentence, e.g. `A 401 which is not re-sent, e.g. when the 
token refresh fails, can include the time of the refresh.`? This does not 
happen in-cluster, where the service account token file always gives a token, 
so it is minor.
   



##########
docs/migration_guide.md:
##########
@@ -144,6 +144,12 @@ affect building the project, CI, tests or examples.
   operator also redacts the configuration properties whose keys or values 
contain `credential` in
   its logs. To restore the behavior before 1.1.0, set it to
   `(?i)secret|password|token|access[.]?key`.
+- Since 1.1.0, `kubernetes.client.http.response.latency.nanos` records the 
latency of HTTP
+  responses, not including the time to read the response body. 1.0 recorded 
nearly zero
+  regardless of the latency. A request re-sent by another interceptor, e.g. 
after a `401`, is
+  recorded twice, and the two values add up to the time from sending it until 
the re-sent response
+  arrives, e.g. including the token refresh. It no longer records a WebSocket 
upgrade response,
+  e.g. of a watch, so its count can be lower than that of 
`kubernetes.client.http.response`.

Review Comment:
   nit (optional): The count can also stay at zero for an embedder. 1.0 
recorded a value at every `after(...)`, while this records one only when 
`unwrap(...)` finds the `TimedConsumer`. So if a subclass of `SparkOperator` 
adds an interceptor through `getClientInterceptors(...)` which wraps the 
consumer after this one without delegating `unwrap(...)`, e.g. with a lambda, 
the histogram stays empty after one warning. The Javadoc and the warning 
explain it, so this is minor, but a line under `### Java API` could mention it.
   



##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptorTest.java:
##########
@@ -171,6 +194,145 @@ void testResponseCodeGroups() {
     Assertions.assertEquals(1, meters.get("5xx").getCount());
   }
 
+  @Test
+  @Order(5)
+  void testResponseLatency() {
+    KubernetesMetricsInterceptor metricsInterceptor = new 
KubernetesMetricsInterceptor();
+    List<Interceptor> interceptors = List.of(metricsInterceptor);
+    try (KubernetesClient client =
+        KubernetesClientFactory.buildKubernetesClient(
+            interceptors, kubernetesClient.getConfiguration())) {
+      ConfigMap configMap = createConfigMap();
+      mockServer
+          .expect()
+          .get()
+          .delay(50)
+          .withPath(CONFIG_MAP_PATH)
+          .andReturn(200, configMap)
+          .once();
+      long startTime = System.nanoTime();
+      client.resource(configMap).get();
+      long elapsed = System.nanoTime() - startTime;
+
+      Histogram latency = histogram(metricsInterceptor.metricRegistry(), 
LATENCY);
+      Assertions.assertEquals(1, latency.getCount());
+      Snapshot snapshot = latency.getSnapshot();
+      Assertions.assertTrue(snapshot.getMax() >= 50_000_000L, () -> "max: " + 
snapshot.getMax());
+      Assertions.assertTrue(
+          snapshot.getMax() <= elapsed,
+          () -> "max: " + snapshot.getMax() + ", elapsed: " + elapsed);
+    }
+  }
+
+  @Test
+  @Order(6)
+  void testResponseLatencyOfResentRequest() {
+    KubernetesMetricsInterceptor metricsInterceptor = new 
KubernetesMetricsInterceptor();
+    MetricRegistry metrics = metricsInterceptor.metricRegistry();
+    List<Interceptor> interceptors = List.of(metricsInterceptor);
+    Config config =
+        new ConfigBuilder(kubernetesClient.getConfiguration())
+            .withOauthTokenProvider(() -> "token")
+            .build();
+    try (KubernetesClient client =
+        KubernetesClientFactory.buildKubernetesClient(interceptors, config)) {
+      ConfigMap configMap = createConfigMap();
+      mockServer
+          .expect()
+          .get()
+          .delay(200)
+          .withPath(CONFIG_MAP_PATH)
+          .andReturn(HTTP_UNAUTHORIZED, new 
StatusBuilder().withCode(HTTP_UNAUTHORIZED).build())
+          .once();
+      mockServer.expect().get().withPath(CONFIG_MAP_PATH).andReturn(200, 
configMap).once();
+      long startTime = System.nanoTime();
+      client.resource(configMap).get();
+      long elapsed = System.nanoTime() - startTime;
+
+      Assertions.assertEquals(2, meterCount(metrics, "http.response"));
+      Assertions.assertEquals(1, meterCount(metrics, "http.response.401"));
+      Assertions.assertEquals(0, meterCount(metrics, "failed"));
+      Histogram latency = histogram(metrics, LATENCY);
+      Assertions.assertEquals(2, latency.getCount());
+      Snapshot snapshot = latency.getSnapshot();
+      Assertions.assertTrue(snapshot.getMax() >= 200_000_000L, () -> "max: " + 
snapshot.getMax());
+      Assertions.assertTrue(snapshot.getMin() < 200_000_000L, () -> "min: " + 
snapshot.getMin());
+      long sum = Assertions.assertInstanceOf(SummingHistogram.class, 
latency).getSum();
+      Assertions.assertTrue(sum >= 200_000_000L, () -> "sum: " + sum);
+      Assertions.assertTrue(sum <= elapsed, () -> "sum: " + sum + ", elapsed: 
" + elapsed);
+    }
+  }
+
+  @Test
+  @Order(7)
+  void testResponseLatencyOfWebSocketUpgrade() {
+    KubernetesMetricsInterceptor metricsInterceptor = new 
KubernetesMetricsInterceptor();
+    MetricRegistry metrics = metricsInterceptor.metricRegistry();
+    List<Interceptor> interceptors = List.of(metricsInterceptor);
+    try (KubernetesClient client =
+            KubernetesClientFactory.buildKubernetesClient(
+                interceptors, kubernetesClient.getConfiguration());
+        SharedIndexInformer<ConfigMap> informer =
+            client.configMaps().inNamespace("spark-system").inform()) {
+      await()
+          .pollDelay(Duration.ZERO)
+          .untilAsserted(
+              () -> {
+                Assertions.assertEquals(2, meterCount(metrics, 
"http.response"));
+                Assertions.assertEquals(1, meterCount(metrics, 
"http.response.101"));
+                Assertions.assertEquals(1, histogram(metrics, 
LATENCY).getCount());
+              });
+      Assertions.assertTrue(informer.isWatching());
+    }
+  }
+
+  @Test
+  @Order(8)
+  void testConsumerUnwrapsDelegate() {
+    AsyncBody.Consumer<List<ByteBuffer>> delegate = (value, asyncBody) -> {};
+    AsyncBody.Consumer<List<ByteBuffer>> consumer =
+        new KubernetesMetricsInterceptor().consumer(delegate, null);
+    Assertions.assertSame(delegate, consumer.unwrap(delegate.getClass()));
+  }
+
+  @Test
+  @Order(9)
+  void testResponseLatencyWithHttpLogging() {
+    Logger logger = mock(Logger.class);
+    when(logger.isTraceEnabled()).thenReturn(true);
+    KubernetesMetricsInterceptor metricsInterceptor = new 
KubernetesMetricsInterceptor();
+    // fabric8 installs HttpLoggingInterceptor first at the TRACE level, so it 
is wrapped inside.
+    List<Interceptor> interceptors =
+        List.of(new HttpLoggingInterceptor(logger), metricsInterceptor);
+    try (KubernetesClient client =
+        KubernetesClientFactory.buildKubernetesClient(
+            interceptors, kubernetesClient.getConfiguration())) {
+      ConfigMap configMap = createConfigMap();
+      mockServer.expect().get().withPath(CONFIG_MAP_PATH).andReturn(200, 
configMap).once();
+      client.resource(configMap).get();
+
+      Assertions.assertEquals(
+          1, histogram(metricsInterceptor.metricRegistry(), 
LATENCY).getCount());
+      // The response body is logged only if HttpLoggingInterceptor finds its 
own consumer.
+      String body = "\"kind\":\"ConfigMap\"";
+      verify(logger, timeout(10_000)).trace(argThat((String m) -> m != null && 
m.contains(body)));
+    }
+  }
+
+  @Test
+  @Order(10)
+  void testResponseLatencyWithoutTimedConsumer() {
+    KubernetesMetricsInterceptor metricsInterceptor = new 
KubernetesMetricsInterceptor();
+    MetricRegistry metrics = metricsInterceptor.metricRegistry();
+    AsyncBody.Consumer<List<ByteBuffer>> consumer = (value, asyncBody) -> {};
+    HttpResponse<?> response = mock(HttpResponse.class);
+    when(response.code()).thenReturn(200);
+    metricsInterceptor.after(null, response, consumer);
+    metricsInterceptor.after(null, response, consumer);
+    Assertions.assertEquals(2, meterCount(metrics, "http.response"));
+    Assertions.assertEquals(0, histogram(metrics, LATENCY).getCount());

Review Comment:
   nit: The Javadoc of `consumer(...)` and the PR description say that a 
warning is logged once, but this test checks only the meters. So it would still 
pass if the warning were never logged, or logged for every response on the 
event loop. How about also asserting the log messages, e.g. with a 
`TestLogAppender` like `ConfigOptionTest#testLogEachInvalidValueOnce`? The two 
`after(...)` calls here already cover the "once" part.
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptor.java:
##########
@@ -233,4 +269,38 @@ 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.
+   */
+  private static final class TimedConsumer implements 
AsyncBody.Consumer<List<ByteBuffer>> {
+    private static final AtomicLongFieldUpdater<TimedConsumer> 
START_TIME_NANOS =
+        AtomicLongFieldUpdater.newUpdater(TimedConsumer.class, 
"startTimeNanos");
+
+    private final AsyncBody.Consumer<List<ByteBuffer>> delegate;
+    // Updated through START_TIME_NANOS, so that each request needs only one 
allocation.
+    @SuppressWarnings({"PMD.AvoidUsingVolatile", "PMD.UnusedPrivateField"})
+    private volatile long startTimeNanos = System.nanoTime();

Review Comment:
   nit (optional): I suggested this updater in the previous round, but on 
second thought, it isn't worth it. It saves one small `AtomicLong` per request, 
which is negligible next to the other allocations of an HTTP exchange, while it 
needs a `volatile` field, two PMD suppressions, and the field name in a string, 
which fails only at runtime if the field is renamed. A plain field does the 
same:
   
   ```java
   private final AtomicLong startTimeNanos = new AtomicLong(System.nanoTime());
   
   long restart() {
     long now = System.nanoTime();
     return now - startTimeNanos.getAndSet(now);
   }
   ```
   
   Either is fine with me. Sorry for the churn.
   



##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptorTest.java:
##########
@@ -171,6 +194,145 @@ void testResponseCodeGroups() {
     Assertions.assertEquals(1, meters.get("5xx").getCount());
   }
 
+  @Test
+  @Order(5)
+  void testResponseLatency() {
+    KubernetesMetricsInterceptor metricsInterceptor = new 
KubernetesMetricsInterceptor();
+    List<Interceptor> interceptors = List.of(metricsInterceptor);
+    try (KubernetesClient client =
+        KubernetesClientFactory.buildKubernetesClient(
+            interceptors, kubernetesClient.getConfiguration())) {
+      ConfigMap configMap = createConfigMap();
+      mockServer
+          .expect()
+          .get()
+          .delay(50)
+          .withPath(CONFIG_MAP_PATH)
+          .andReturn(200, configMap)
+          .once();
+      long startTime = System.nanoTime();
+      client.resource(configMap).get();
+      long elapsed = System.nanoTime() - startTime;
+
+      Histogram latency = histogram(metricsInterceptor.metricRegistry(), 
LATENCY);
+      Assertions.assertEquals(1, latency.getCount());
+      Snapshot snapshot = latency.getSnapshot();
+      Assertions.assertTrue(snapshot.getMax() >= 50_000_000L, () -> "max: " + 
snapshot.getMax());
+      Assertions.assertTrue(
+          snapshot.getMax() <= elapsed,
+          () -> "max: " + snapshot.getMax() + ", elapsed: " + elapsed);
+    }
+  }
+
+  @Test
+  @Order(6)
+  void testResponseLatencyOfResentRequest() {
+    KubernetesMetricsInterceptor metricsInterceptor = new 
KubernetesMetricsInterceptor();
+    MetricRegistry metrics = metricsInterceptor.metricRegistry();
+    List<Interceptor> interceptors = List.of(metricsInterceptor);
+    Config config =
+        new ConfigBuilder(kubernetesClient.getConfiguration())
+            .withOauthTokenProvider(() -> "token")
+            .build();
+    try (KubernetesClient client =
+        KubernetesClientFactory.buildKubernetesClient(interceptors, config)) {
+      ConfigMap configMap = createConfigMap();
+      mockServer
+          .expect()
+          .get()
+          .delay(200)
+          .withPath(CONFIG_MAP_PATH)
+          .andReturn(HTTP_UNAUTHORIZED, new 
StatusBuilder().withCode(HTTP_UNAUTHORIZED).build())
+          .once();
+      mockServer.expect().get().withPath(CONFIG_MAP_PATH).andReturn(200, 
configMap).once();
+      long startTime = System.nanoTime();
+      client.resource(configMap).get();
+      long elapsed = System.nanoTime() - startTime;
+
+      Assertions.assertEquals(2, meterCount(metrics, "http.response"));
+      Assertions.assertEquals(1, meterCount(metrics, "http.response.401"));
+      Assertions.assertEquals(0, meterCount(metrics, "failed"));
+      Histogram latency = histogram(metrics, LATENCY);
+      Assertions.assertEquals(2, latency.getCount());
+      Snapshot snapshot = latency.getSnapshot();
+      Assertions.assertTrue(snapshot.getMax() >= 200_000_000L, () -> "max: " + 
snapshot.getMax());
+      Assertions.assertTrue(snapshot.getMin() < 200_000_000L, () -> "min: " + 
snapshot.getMin());

Review Comment:
   nit: Now that L262 bounds the sum by the wall-clock time, this check, which 
I suggested in an earlier round, no longer adds coverage. If `restart()` did 
not reset the start time, the second value would include the first one, and the 
sum would exceed `elapsed`. But this check still assumes that the re-sent 
request completes within 200 ms, so a long GC pause on a busy CI runner could 
fail it. How about dropping it? `How was this patch tested?` in the PR 
description (`one of at least 200 ms and one below 200 ms`) would need the same 
update.
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptor.java:
##########
@@ -103,21 +125,38 @@ 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

Review Comment:
   FYI, no change is needed in this PR, since this is in your list of 
follow-ups (the upstream fabric8 issue). A rejected upgrade still reaches 
`after(...)` 1+N times (`StandardHttpClient.java` L314-315 and L324-326), e.g. 
five times for a `403` with the default interceptors, which marks 
`http.response`, its code meter and `4xx` each time. In addition, fabric8 
retries an upgrade rejected with a `429` or a `5xx` with backoff 
(`buildWebSocket` -> `retryWithExponentialBackoff`, L285-289), so this repeats 
for each attempt. The latency histogram is no longer affected, since the 
consumer is `null` there.
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/KubernetesMetricsInterceptor.java:
##########
@@ -103,21 +125,38 @@ 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} or {@code
+   * BackwardsCompatibilityInterceptor} for a legacy API version. 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
   public void after(
       HttpRequest request,
       HttpResponse<?> response,
       AsyncBody.Consumer<List<ByteBuffer>> consumer) {
-    updateResponseMetrics(response, System.nanoTime());
+    Objects.requireNonNull(response);
+    TimedConsumer timedConsumer = consumer == null ? null : 
consumer.unwrap(TimedConsumer.class);
+    if (timedConsumer != null) {
+      responseLatency.update(timedConsumer.restart());
+    } else if (consumer != null && 
missingTimedConsumerLogged.compareAndSet(false, true)) {
+      log.warn(
+          "No response latency is recorded, because an interceptor added after 
{} wraps the "
+              + "response body consumer without delegating 
AsyncBody.Consumer#unwrap(...).",
+          KubernetesMetricsInterceptor.class.getSimpleName());
+    }
+    updateResponseMetrics(response);

Review Comment:
   nit (optional): `consumer.unwrap(...)` at L150 runs the `unwrap(...)` of the 
consumers which wrap `TimedConsumer`, i.e. code of other interceptors, before 
this line. If one of them throws, e.g. a wrapper whose `unwrap(...)` returns 
`(U) this` for any class, which fails the cast at L150, fabric8 drops the 
exception of its `thenAccept(...)` stage (`StandardHttpClient.java` L118-119). 
Then `http.response` and the code meters miss the response without a log, and 
the interceptors after this one don't get `after(...)` either. 1.0 marked the 
meters unconditionally.
   
   This can't happen with the default interceptors, where `TimedConsumer` is 
the outermost consumer, so feel free to leave it. Otherwise, calling 
`updateResponseMetrics(response)` right after 
`Objects.requireNonNull(response)` would keep the meters independent of other 
interceptors, and the tests don't depend on the order. I know the current order 
came from my earlier suggestion.
   



-- 
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