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 411e25ec05f7 CAMEL-26408: camel-openfga - scale the batchCheck await 
timeout by the fan-out (#28703)
411e25ec05f7 is described below

commit 411e25ec05f7fb476e4feefe7f01ea30f41af29f
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Oct 29 22:38:00 2026 +0100

    CAMEL-26408: camel-openfga - scale the batchCheck await timeout by the 
fan-out (#28703)
    
    batchCheck issues one check per object, maxParallelRequests at a time, so
    ceil(n / maxParallelRequests) waves share a single future. It was bounded by
    the single-request await timeout (connect+read, per attempt, across the 
retries,
    plus a margin), so a large batch against a slow server could be cancelled
    mid-flight and surfaced as a spurious "Timed out after ...". Scale the 
bound by
    the number of waves - the safety margin still added only once - and report 
the
    actual bound in the timeout message.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../camel/component/openfga/OpenFgaAuthorizer.java | 39 +++++++++++---
 .../camel/component/openfga/OpenFgaProducer.java   |  8 ++-
 .../component/openfga/OpenFgaAuthorizerTest.java   | 59 ++++++++++++++++++++++
 3 files changed, 97 insertions(+), 9 deletions(-)

diff --git 
a/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaAuthorizer.java
 
b/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaAuthorizer.java
index 25a97ee59c55..065354464831 100644
--- 
a/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaAuthorizer.java
+++ 
b/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaAuthorizer.java
@@ -74,6 +74,7 @@ public class OpenFgaAuthorizer {
     private final Expression continuationToken;
     private final ConsistencyPreference consistency;
     private final boolean failOpen;
+    private final long perRequestTimeoutMillis;
     private final long awaitTimeoutMillis;
 
     public OpenFgaAuthorizer(OpenFgaClient client, OpenFgaConfiguration 
configuration, CamelContext camelContext) {
@@ -88,7 +89,8 @@ public class OpenFgaAuthorizer {
         this.continuationToken = compile(camelContext, 
configuration.getContinuationToken());
         this.consistency = parseConsistency(configuration.getConsistency());
         this.failOpen = configuration.isFailOpen();
-        this.awaitTimeoutMillis = awaitTimeout(configuration);
+        this.perRequestTimeoutMillis = perRequestTimeout(configuration);
+        this.awaitTimeoutMillis = perRequestTimeoutMillis + 
AWAIT_SAFETY_MARGIN_MILLIS;
     }
 
     /**
@@ -157,13 +159,27 @@ public class OpenFgaAuthorizer {
     }
 
     /**
-     * The longest one exchange may wait for an answer: what the SDK itself 
allows for - a connect and a request per
-     * attempt, across the first attempt and every retry - plus a margin.
+     * What the SDK itself allows for one request - a connect and a request 
per attempt, across the first attempt and
+     * every retry. The safety margin is not included here: {@link 
#awaitTimeoutMillis} adds it once for a single call,
+     * and {@link #batchAwaitTimeoutMillis} adds it once on top of the whole 
fan-out.
      */
-    private static long awaitTimeout(OpenFgaConfiguration configuration) {
+    private static long perRequestTimeout(OpenFgaConfiguration configuration) {
         long perAttempt = configuration.getConnectTimeout() + 
configuration.getReadTimeout();
         long attempts = Math.max(0, configuration.getMaxRetries()) + 1L;
-        return perAttempt * attempts + AWAIT_SAFETY_MARGIN_MILLIS;
+        return perAttempt * attempts;
+    }
+
+    /**
+     * The longest a {@code batchCheck} of {@code objectCount} objects may 
wait. The SDK issues the batch as one request
+     * per object with at most {@code maxParallelRequests} in flight, so the 
fan-out runs in
+     * {@code ceil(objectCount / maxParallelRequests)} serialized waves. 
Bounding the whole batch by the single-request
+     * timeout would cancel a large batch against a slow server mid-flight and 
report it as a spurious timeout, so the
+     * per-request time is multiplied by the number of waves - the safety 
margin is still added only once.
+     */
+    long batchAwaitTimeoutMillis(int objectCount, int maxParallelRequests) {
+        int parallel = Math.max(1, maxParallelRequests);
+        long waves = Math.max(1L, (objectCount + parallel - 1L) / parallel);
+        return perRequestTimeoutMillis * waves + AWAIT_SAFETY_MARGIN_MILLIS;
     }
 
     /**
@@ -442,6 +458,15 @@ public class OpenFgaAuthorizer {
      * routing thread for ever.
      */
     <T> T await(Exchange exchange, String what, CallSupplier<T> call) throws 
OpenFgaEvaluationException {
+        return await(exchange, what, call, awaitTimeoutMillis);
+    }
+
+    /**
+     * As {@link #await(Exchange, String, CallSupplier)} but with an explicit 
bound, for the batch path whose fan-out
+     * needs more than a single request's budget (see {@link 
#batchAwaitTimeoutMillis}).
+     */
+    <T> T await(Exchange exchange, String what, CallSupplier<T> call, long 
timeoutMillis)
+            throws OpenFgaEvaluationException {
         CompletableFuture<T> future;
         try {
             future = call.get();
@@ -449,7 +474,7 @@ public class OpenFgaAuthorizer {
             throw new OpenFgaEvaluationException("Could not build the OpenFGA 
request to " + what, exchange, e);
         }
         try {
-            return future.get(awaitTimeoutMillis, TimeUnit.MILLISECONDS);
+            return future.get(timeoutMillis, TimeUnit.MILLISECONDS);
         } catch (InterruptedException e) {
             // a shutdown, not a failure of the decision point. Restore the 
flag the interruptible wait cleared and
             // fail: nothing decided that this exchange was permitted, so 
failOpen must not claim it did either.
@@ -461,7 +486,7 @@ public class OpenFgaAuthorizer {
         } catch (TimeoutException e) {
             future.cancel(true);
             throw new OpenFgaEvaluationException(
-                    "Timed out after " + awaitTimeoutMillis + "ms waiting for 
OpenFGA to " + what, exchange, e);
+                    "Timed out after " + timeoutMillis + "ms waiting for 
OpenFGA to " + what, exchange, e);
         } catch (ExecutionException e) {
             // the SDK reports an unreachable server, an HTTP error and a 
serialization problem all through the
             // future; every one of them is the same thing to a route - no 
verdict - so they are reported alike
diff --git 
a/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaProducer.java
 
b/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaProducer.java
index 1972ecb5f068..a33c8d511799 100644
--- 
a/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaProducer.java
+++ 
b/components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaProducer.java
@@ -180,8 +180,9 @@ public class OpenFgaProducer extends DefaultProducer {
             return;
         }
 
+        int maxParallelRequests = 
getEndpoint().getConfiguration().getMaxParallelRequests();
         ClientBatchCheckClientOptions options = new 
ClientBatchCheckClientOptions()
-                
.maxParallelRequests(getEndpoint().getConfiguration().getMaxParallelRequests());
+                .maxParallelRequests(maxParallelRequests);
         if 
(ObjectHelper.isNotEmpty(getEndpoint().getConfiguration().getAuthorizationModelId()))
 {
             
options.authorizationModelId(getEndpoint().getConfiguration().getAuthorizationModelId());
         }
@@ -191,7 +192,10 @@ public class OpenFgaProducer extends DefaultProducer {
 
         List<ClientBatchCheckClientResponse> responses = authorizer.await(
                 exchange, "batch check " + relation + " for " + user,
-                () -> authorizer.getClient().clientBatchCheck(requests, 
options));
+                () -> authorizer.getClient().clientBatchCheck(requests, 
options),
+                // the batch fans out one request per object, 
maxParallelRequests at a time, so it needs more than a
+                // single request's budget - otherwise a large batch against a 
slow server is cancelled mid-flight
+                authorizer.batchAwaitTimeoutMillis(requests.size(), 
maxParallelRequests));
 
         Set<String> permitted = new HashSet<>();
         for (ClientBatchCheckClientResponse response : responses) {
diff --git 
a/components/camel-openfga/src/test/java/org/apache/camel/component/openfga/OpenFgaAuthorizerTest.java
 
b/components/camel-openfga/src/test/java/org/apache/camel/component/openfga/OpenFgaAuthorizerTest.java
new file mode 100644
index 000000000000..eb1595db1d0f
--- /dev/null
+++ 
b/components/camel-openfga/src/test/java/org/apache/camel/component/openfga/OpenFgaAuthorizerTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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 dev.openfga.sdk.api.client.OpenFgaClient;
+import org.apache.camel.CamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Unit tests for how {@link OpenFgaAuthorizer} bounds the wait on a request.
+ */
+class OpenFgaAuthorizerTest {
+
+    private final OpenFgaClient client = mock(OpenFgaClient.class);
+    private final CamelContext camelContext = mock(CamelContext.class);
+
+    @Test
+    void batchAwaitTimeoutScalesWithTheNumberOfFanOutWaves() {
+        OpenFgaConfiguration configuration = new OpenFgaConfiguration();
+        configuration.setConnectTimeout(10000);
+        configuration.setReadTimeout(10000);
+        configuration.setMaxRetries(3);
+        OpenFgaAuthorizer authorizer = new OpenFgaAuthorizer(client, 
configuration, camelContext);
+
+        // connect + read, per attempt, across the first try and the three 
retries
+        long perRequest = (10000L + 10000L) * (3 + 1);
+        // AWAIT_SAFETY_MARGIN_MILLIS, added once however many waves the batch 
fans out into
+        long margin = 5000L;
+
+        // a batch that fits in a single wave is bounded like a single 
request: the per-request time plus the margin
+        assertThat(authorizer.batchAwaitTimeoutMillis(10, 
10)).isEqualTo(perRequest + margin);
+        assertThat(authorizer.batchAwaitTimeoutMillis(5, 
10)).isEqualTo(perRequest + margin);
+        // more objects than can run in parallel fan out into ceil(objectCount 
/ maxParallelRequests) waves, and the
+        // bound grows a whole per-request budget with each extra wave - while 
the margin is still added only once
+        assertThat(authorizer.batchAwaitTimeoutMillis(11, 
10)).isEqualTo(perRequest * 2 + margin);
+        assertThat(authorizer.batchAwaitTimeoutMillis(25, 
10)).isEqualTo(perRequest * 3 + margin);
+        // defensive: a non-positive parallelism is clamped to one in-flight 
request rather than dividing by zero
+        assertThat(authorizer.batchAwaitTimeoutMillis(5, 
0)).isEqualTo(perRequest * 5 + margin);
+        // an empty batch still bounds a single wave rather than collapsing to 
just the margin
+        assertThat(authorizer.batchAwaitTimeoutMillis(0, 
10)).isEqualTo(perRequest + margin);
+    }
+}

Reply via email to