This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new efe29de2e4f7 CAMEL-26827: Add health probe deadline regressions 
(#29743)
efe29de2e4f7 is described below

commit efe29de2e4f7fd60e0ec7f33dc91ca6de982dcb9
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Nov 12 21:45:15 2026 +0100

    CAMEL-26827: Add health probe deadline regressions (#29743)
    
    Add real HTTP-server regressions for OPA and OpenFGA health probes that 
stall after sending response headers. The tests prove the complete-response 
deadline, peer disconnection after cancellation, and interrupt-flag 
preservation. Component documentation and catalog mirrors describe the 
five-second budget.
    
    Follow-up coverage for #29630.
    
    Codex on behalf of oscerd.
    
    Co-authored-by: Codex <[email protected]>
---
 .../apache/camel/catalog/docs/opa-component.adoc   |   4 +
 .../camel/catalog/docs/openfga-component.adoc      |   4 +
 .../camel-opa/src/main/docs/opa-component.adoc     |   4 +
 .../component/opa/OpaHealthProbeDeadlineTest.java  | 136 +++++++++++++++++++++
 .../src/main/docs/openfga-component.adoc           |   4 +
 .../openfga/OpenFgaHealthProbeDeadlineTest.java    | 136 +++++++++++++++++++++
 6 files changed, 288 insertions(+)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
index f05907d8b19a..6c5f3077c2b5 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
@@ -653,6 +653,10 @@ so the policy always returns a verdict.
 
 == Health check
 
+Each check owns its HTTP client and releases it when the check stops. The 
five-second
+health deadline includes the complete response body; a stalled body reports 
DOWN and
+the outstanding request is cancelled.
+
 Because the component fails closed, an OPA server that cannot be reached fails 
*every* exchange through the route.
 The producer registers a readiness check that probes the server's `/health` 
endpoint, so an unavailable policy
 decision point shows up in the health registry rather than only in the error 
logs once traffic starts failing.
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openfga-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openfga-component.adoc
index 5bd6f2d3d8ce..bb37a914371d 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openfga-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openfga-component.adoc
@@ -446,6 +446,10 @@ none, leaving the connect phase bounded only by the 
operating system.
 
 == Health checks
 
+Each check owns its HTTP client and releases it when the check stops. The 
five-second
+health deadline includes the complete response body; a stalled body reports 
DOWN and
+the outstanding request is cancelled.
+
 Both the producer and `OpenFgaSecurityPolicy` register a readiness check that 
probes OpenFGA's `/healthz`
 endpoint. Because the component fails closed, a route that is up but cannot 
reach OpenFGA fails every message, so
 it is not ready — and this makes that visible before traffic arrives rather 
than only in the error logs afterwards.
diff --git a/components/camel-opa/src/main/docs/opa-component.adoc 
b/components/camel-opa/src/main/docs/opa-component.adoc
index f05907d8b19a..6c5f3077c2b5 100644
--- a/components/camel-opa/src/main/docs/opa-component.adoc
+++ b/components/camel-opa/src/main/docs/opa-component.adoc
@@ -653,6 +653,10 @@ so the policy always returns a verdict.
 
 == Health check
 
+Each check owns its HTTP client and releases it when the check stops. The 
five-second
+health deadline includes the complete response body; a stalled body reports 
DOWN and
+the outstanding request is cancelled.
+
 Because the component fails closed, an OPA server that cannot be reached fails 
*every* exchange through the route.
 The producer registers a readiness check that probes the server's `/health` 
endpoint, so an unavailable policy
 decision point shows up in the health registry rather than only in the error 
logs once traffic starts failing.
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaHealthProbeDeadlineTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaHealthProbeDeadlineTest.java
new file mode 100644
index 000000000000..4748a61a0bb5
--- /dev/null
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaHealthProbeDeadlineTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.opa;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.http.HttpTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.health.HealthCheck;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OpaHealthProbeDeadlineTest {
+
+    @Test
+    @Timeout(15)
+    void reportsDownAndCancelsWhenTheHealthBodyStallsAfterHeaders() throws 
Exception {
+        try (StalledHealthEndpoint server = new StalledHealthEndpoint();
+             OpaProducerHealthCheck check = new 
OpaProducerHealthCheck(server.url(), null, "scope", "endpoint", null)) {
+            check.setEnabled(true);
+
+            HealthCheck.Result result = check.call();
+
+            assertThat(server.headersSent.getCount()).isZero();
+            assertThat(result.getState()).isEqualTo(HealthCheck.State.DOWN);
+            
assertThat(result.getError()).isPresent().get().isInstanceOf(HttpTimeoutException.class);
+            assertThat(server.disconnected.await(5, 
TimeUnit.SECONDS)).isTrue();
+        }
+    }
+
+    @Test
+    @Timeout(10)
+    void interruptionCancelsTheHealthRequestAndPreservesTheInterruptFlag() 
throws Exception {
+        try (StalledHealthEndpoint server = new StalledHealthEndpoint();
+             OpaProducerHealthCheck check = new 
OpaProducerHealthCheck(server.url(), null, "scope", "endpoint", null);
+             var executor = Executors.newSingleThreadExecutor()) {
+            check.setEnabled(true);
+            AtomicReference<Thread> checking = new AtomicReference<>();
+            AtomicBoolean interruptPreserved = new AtomicBoolean();
+            var result = executor.submit(() -> {
+                checking.set(Thread.currentThread());
+                HealthCheck.Result outcome = check.call();
+                interruptPreserved.set(Thread.currentThread().isInterrupted());
+                return outcome;
+            });
+            assertThat(server.headersSent.await(3, TimeUnit.SECONDS)).isTrue();
+
+            checking.get().interrupt();
+
+            HealthCheck.Result outcome = result.get(3, TimeUnit.SECONDS);
+            assertThat(outcome.getState()).isEqualTo(HealthCheck.State.DOWN);
+            
assertThat(outcome.getError()).isPresent().get().isInstanceOf(InterruptedException.class);
+            assertThat(interruptPreserved).isTrue();
+            assertThat(server.disconnected.await(3, 
TimeUnit.SECONDS)).isTrue();
+        }
+    }
+
+    /** Sends a partial HTTP body, then observes the peer closing the 
cancelled request. */
+    private static final class StalledHealthEndpoint implements AutoCloseable {
+        private final ServerSocket listener = new ServerSocket(0);
+        private final CountDownLatch headersSent = new CountDownLatch(1);
+        private final CountDownLatch disconnected = new CountDownLatch(1);
+        private final Thread server;
+        private volatile Socket peer;
+
+        private StalledHealthEndpoint() throws IOException {
+            server = new Thread(this::serve, "opa-stalled-health");
+            server.setDaemon(true);
+            server.start();
+        }
+
+        private String url() {
+            return "http://localhost:"; + listener.getLocalPort();
+        }
+
+        private void serve() {
+            try (Socket accepted = listener.accept()) {
+                peer = accepted;
+                BufferedReader request = new BufferedReader(
+                        new InputStreamReader(accepted.getInputStream(), 
StandardCharsets.US_ASCII));
+                String line;
+                while ((line = request.readLine()) != null && !line.isEmpty()) 
{
+                    // Consume the request headers before looking for 
cancellation on the connection.
+                }
+                accepted.getOutputStream().write(
+                        ("HTTP/1.1 200 OK\r\nContent-Type: 
application/json\r\nContent-Length: 100\r\n\r\n{")
+                                .getBytes(StandardCharsets.US_ASCII));
+                accepted.getOutputStream().flush();
+                headersSent.countDown();
+                if (accepted.getInputStream().read() == -1) {
+                    disconnected.countDown();
+                }
+            } catch (IOException e) {
+                if (headersSent.getCount() == 0) {
+                    // Cancellation may reset the connection instead of 
closing it cleanly.
+                    disconnected.countDown();
+                }
+            }
+        }
+
+        @Override
+        public void close() throws Exception {
+            listener.close();
+            Socket accepted = peer;
+            if (accepted != null) {
+                accepted.close();
+            }
+            server.join(TimeUnit.SECONDS.toMillis(3));
+        }
+    }
+}
diff --git a/components/camel-openfga/src/main/docs/openfga-component.adoc 
b/components/camel-openfga/src/main/docs/openfga-component.adoc
index 5bd6f2d3d8ce..bb37a914371d 100644
--- a/components/camel-openfga/src/main/docs/openfga-component.adoc
+++ b/components/camel-openfga/src/main/docs/openfga-component.adoc
@@ -446,6 +446,10 @@ none, leaving the connect phase bounded only by the 
operating system.
 
 == Health checks
 
+Each check owns its HTTP client and releases it when the check stops. The 
five-second
+health deadline includes the complete response body; a stalled body reports 
DOWN and
+the outstanding request is cancelled.
+
 Both the producer and `OpenFgaSecurityPolicy` register a readiness check that 
probes OpenFGA's `/healthz`
 endpoint. Because the component fails closed, a route that is up but cannot 
reach OpenFGA fails every message, so
 it is not ready — and this makes that visible before traffic arrives rather 
than only in the error logs afterwards.
diff --git 
a/components/camel-openfga/src/test/java/org/apache/camel/component/openfga/OpenFgaHealthProbeDeadlineTest.java
 
b/components/camel-openfga/src/test/java/org/apache/camel/component/openfga/OpenFgaHealthProbeDeadlineTest.java
new file mode 100644
index 000000000000..e6a74a5675ef
--- /dev/null
+++ 
b/components/camel-openfga/src/test/java/org/apache/camel/component/openfga/OpenFgaHealthProbeDeadlineTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfga;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.http.HttpTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.health.HealthCheck;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OpenFgaHealthProbeDeadlineTest {
+
+    @Test
+    @Timeout(15)
+    void reportsDownAndCancelsWhenTheHealthBodyStallsAfterHeaders() throws 
Exception {
+        try (StalledHealthEndpoint server = new StalledHealthEndpoint();
+             OpenFgaProducerHealthCheck check = new 
OpenFgaProducerHealthCheck(server.url(), null, "store", "endpoint", null)) {
+            check.setEnabled(true);
+
+            HealthCheck.Result result = check.call();
+
+            assertThat(server.headersSent.getCount()).isZero();
+            assertThat(result.getState()).isEqualTo(HealthCheck.State.DOWN);
+            
assertThat(result.getError()).isPresent().get().isInstanceOf(HttpTimeoutException.class);
+            assertThat(server.disconnected.await(5, 
TimeUnit.SECONDS)).isTrue();
+        }
+    }
+
+    @Test
+    @Timeout(10)
+    void interruptionCancelsTheHealthRequestAndPreservesTheInterruptFlag() 
throws Exception {
+        try (StalledHealthEndpoint server = new StalledHealthEndpoint();
+             OpenFgaProducerHealthCheck check = new 
OpenFgaProducerHealthCheck(server.url(), null, "store", "endpoint", null);
+             var executor = Executors.newSingleThreadExecutor()) {
+            check.setEnabled(true);
+            AtomicReference<Thread> checking = new AtomicReference<>();
+            AtomicBoolean interruptPreserved = new AtomicBoolean();
+            var result = executor.submit(() -> {
+                checking.set(Thread.currentThread());
+                HealthCheck.Result outcome = check.call();
+                interruptPreserved.set(Thread.currentThread().isInterrupted());
+                return outcome;
+            });
+            assertThat(server.headersSent.await(3, TimeUnit.SECONDS)).isTrue();
+
+            checking.get().interrupt();
+
+            HealthCheck.Result outcome = result.get(3, TimeUnit.SECONDS);
+            assertThat(outcome.getState()).isEqualTo(HealthCheck.State.DOWN);
+            
assertThat(outcome.getError()).isPresent().get().isInstanceOf(InterruptedException.class);
+            assertThat(interruptPreserved).isTrue();
+            assertThat(server.disconnected.await(3, 
TimeUnit.SECONDS)).isTrue();
+        }
+    }
+
+    /** Sends a partial HTTP body, then observes the peer closing the 
cancelled request. */
+    private static final class StalledHealthEndpoint implements AutoCloseable {
+        private final ServerSocket listener = new ServerSocket(0);
+        private final CountDownLatch headersSent = new CountDownLatch(1);
+        private final CountDownLatch disconnected = new CountDownLatch(1);
+        private final Thread server;
+        private volatile Socket peer;
+
+        private StalledHealthEndpoint() throws IOException {
+            server = new Thread(this::serve, "openfga-stalled-health");
+            server.setDaemon(true);
+            server.start();
+        }
+
+        private String url() {
+            return "http://localhost:"; + listener.getLocalPort();
+        }
+
+        private void serve() {
+            try (Socket accepted = listener.accept()) {
+                peer = accepted;
+                BufferedReader request = new BufferedReader(
+                        new InputStreamReader(accepted.getInputStream(), 
StandardCharsets.US_ASCII));
+                String line;
+                while ((line = request.readLine()) != null && !line.isEmpty()) 
{
+                    // Consume the request headers before looking for 
cancellation on the connection.
+                }
+                accepted.getOutputStream().write(
+                        ("HTTP/1.1 200 OK\r\nContent-Type: 
application/json\r\nContent-Length: 100\r\n\r\n{")
+                                .getBytes(StandardCharsets.US_ASCII));
+                accepted.getOutputStream().flush();
+                headersSent.countDown();
+                if (accepted.getInputStream().read() == -1) {
+                    disconnected.countDown();
+                }
+            } catch (IOException e) {
+                if (headersSent.getCount() == 0) {
+                    // Cancellation may reset the connection instead of 
closing it cleanly.
+                    disconnected.countDown();
+                }
+            }
+        }
+
+        @Override
+        public void close() throws Exception {
+            listener.close();
+            Socket accepted = peer;
+            if (accepted != null) {
+                accepted.close();
+            }
+            server.join(TimeUnit.SECONDS.toMillis(3));
+        }
+    }
+}

Reply via email to