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

gnodet pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new 57387dfa7742 [backport camel-4.22.x] CAMEL-26550: camel-grpc - 
routeControlledStreamObserver lets the route reply to streaming requests with 
the PROPAGATION strategy (#29300)
57387dfa7742 is described below

commit 57387dfa774260a207085d80c2e001f3609c435c
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Nov 9 13:01:25 2026 +0100

    [backport camel-4.22.x] CAMEL-26550: camel-grpc - 
routeControlledStreamObserver lets the route reply to streaming requests with 
the PROPAGATION strategy (#29300)
    
    * [backport camel-4.22.x] CAMEL-26550: camel-grpc - 
routeControlledStreamObserver lets the route reply to streaming requests with 
the PROPAGATION strategy
    
    * fix: backport companion changes to GrpcRequestAbstractStreamObserver and 
GrpcMethodHandler
    
    The backport cherry-picked GrpcRequestPropagationStreamObserver.java which
    references sendFailure(), sendResponses(), and responseType from the 
abstract
    base class, and setRouteControlledProperties()/toStatusException() from
    GrpcMethodHandler — none of which exist in camel-4.22.x.
    
    Add the minimum required:
    - GrpcConstants: GRPC_RESPONSE_TYPE, GRPC_MUTE_EXCEPTION, 
GRPC_RESPONSE_OBSERVER_FAILED
    - GrpcRequestAbstractStreamObserver: responseType field, sendFailure(), 
sendResponses()
    - GrpcMethodHandler: setRouteControlledProperties(), toStatusException() 
static methods
    
    * fix: remove unused import java.util.List from 
GrpcRequestPropagationStreamObserver
    
    * fix: address review comments on backport PR #29300
    
    - GrpcPropagationRouteControlledTest: inline PongResponseStreamObserver
      as a static inner class instead of importing it from 
GrpcConsumerReplyTypeTest,
      which does not exist in the camel-4.22.x branch (it was introduced by
      CAMEL-26187 / PR #29012)
    - grpc-component.adoc: replace the type-conversion consumer-reply section
      (copied from CAMEL-26187's conflict resolution) with an accurate 
description
      of what GrpcProducerToRouteControlledStreamObserver actually does in this
      branch: forward onNext/onCompleted/onError events by CamelGrpcEventType
      header, without type conversion
    - GrpcMethodHandler.toStatusException(): add null guard on 
cause.getMessage()
      to prevent NPE when exchange.isFailed() is true but getException() returns
      null (fault messages)
    
    * fix: update @return Javadoc in sendResponses() to match simplified 
backport behavior
    
    * fix: address review — restore 4.22.x behavior in non-routeControlled 
path, remove dead API
    
    - Restore the original 4.22.x reply logic (body forwarded via onNext 
unconditionally) in
      the non-routeControlled branch of 
GrpcRequestPropagationStreamObserver.onNext().
      sendFailure() was introduced from main (CAMEL-25250 / #27247) which is 
not on 4.22.x;
      removing it avoids a behavioral regression for existing PROPAGATION users.
    - Remove the dead sendFailure()/sendResponses() helpers and 
responseType/getResponseType/
      setResponseType from GrpcRequestAbstractStreamObserver (no callers on 
this branch).
    - Remove the static setRouteControlledProperties() and toStatusException() 
methods from
      GrpcMethodHandler (no callers after the above cleanup).
    - Remove the three dead public API constants GRPC_RESPONSE_TYPE, 
GRPC_MUTE_EXCEPTION and
      GRPC_RESPONSE_OBSERVER_FAILED from GrpcConstants (unused on 4.22.x).
    - Add an upgrade-guide entry for CAMEL-26550 in the 4.22.2→4.22.3 section.
    
    * fix: remove unused Exchange import and fix import ordering in 
GrpcRequestPropagationStreamObserver
    
    * nit: fix comment on 'failed' field to reflect it is set in both 
routeControlled and non-routeControlled modes
---
 .../org/apache/camel/catalog/components/grpc.json  |   2 +-
 .../apache/camel/catalog/docs/grpc-component.adoc  |  15 +-
 .../org/apache/camel/component/grpc/grpc.json      |   2 +-
 .../camel-grpc/src/main/docs/grpc-component.adoc   |  15 +-
 .../camel/component/grpc/GrpcConfiguration.java    |   4 +-
 .../GrpcRequestPropagationStreamObserver.java      |  43 ++++-
 .../grpc/GrpcPropagationRouteControlledTest.java   | 203 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |  10 +
 .../endpoint/dsl/GrpcEndpointBuilderFactory.java   |  10 +-
 9 files changed, 292 insertions(+), 12 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/grpc.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/grpc.json
index 05519fe07a38..09bb31224c06 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/grpc.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/grpc.json
@@ -43,7 +43,7 @@
     "forwardOnError": { "index": 5, "kind": "parameter", "displayName": 
"Forward On Error", "group": "common", "label": "common", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Determines if onError events should be pushed 
to the Camel route. Exceptions will be set a [...]
     "maxMessageSize": { "index": 6, "kind": "parameter", "displayName": "Max 
Message Size", "group": "common", "label": "", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 4194304, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "The maximum message size allowed to be 
received\/sent (MiB)" },
     "autoDiscoverServerInterceptors": { "index": 7, "kind": "parameter", 
"displayName": "Auto Discover Server Interceptors", "group": "consumer", 
"label": "consumer", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": true, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Setting the autoDiscoverServerInterceptors 
mechanism,  [...]
-    "consumerStrategy": { "index": 8, "kind": "parameter", "displayName": 
"Consumer Strategy", "group": "consumer", "label": "consumer", "required": 
false, "type": "enum", "javaType": 
"org.apache.camel.component.grpc.GrpcConsumerStrategy", "enum": [ 
"AGGREGATION", "PROPAGATION", "DELEGATION" ], "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "PROPAGATION", "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configura [...]
+    "consumerStrategy": { "index": 8, "kind": "parameter", "displayName": 
"Consumer Strategy", "group": "consumer", "label": "consumer", "required": 
false, "type": "enum", "javaType": 
"org.apache.camel.component.grpc.GrpcConsumerStrategy", "enum": [ 
"AGGREGATION", "PROPAGATION", "DELEGATION" ], "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "PROPAGATION", "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configura [...]
     "initialFlowControlWindow": { "index": 9, "kind": "parameter", 
"displayName": "Initial Flow Control Window", "group": "consumer", "label": 
"consumer", "required": false, "type": "integer", "javaType": "int", 
"deprecated": false, "autowired": false, "secret": false, "defaultValue": 
1048576, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Sets the initial flow control window in 
bytes." },
     "keepAliveTime": { "index": 10, "kind": "parameter", "displayName": "Keep 
Alive Time", "group": "consumer", "label": "consumer", "required": false, 
"type": "integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 7200000, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Sets a custom keepalive time in milliseconds, 
the delay time for sending next keepalive  [...]
     "keepAliveTimeout": { "index": 11, "kind": "parameter", "displayName": 
"Keep Alive Timeout", "group": "consumer", "label": "consumer", "required": 
false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": 20000, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Sets a custom keepalive timeout in 
milliseconds, the timeout for keepalive ping requ [...]
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/grpc-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/grpc-component.adoc
index c092f6a8e8ed..b480d6c1b73a 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/grpc-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/grpc-component.adoc
@@ -73,6 +73,17 @@ To enable these features, the following component properties 
combinations must b
 |||jwtSubject||Optional
 |=======================================================================
 
+=== gRPC consumer reply
+
+The message body at the end of the consumer route is the reply to the client: 
the response message of the called
+method, or a `List` of them for a streaming response. A message without a body 
sends an empty response message.
+
+With `routeControlledStreamObserver=true` the route sends the responses 
itself, with a producer endpoint with
+`toRouteControlledStreamObserver=true`. The producer sends the event named by 
the `CamelGrpcEventType` header
+(`onNext`, `onCompleted` or `onError`). The consumer sets this header for the 
events of client streaming and
+bidirectional streaming calls. The body of an `onNext` event is sent to the 
client as is. The body of an `onError`
+event must be a `Throwable`, which ends the call.
+
 === gRPC producer resource type mapping
 
 The table below shows the types of objects in the message body, depending on 
the types (simple or stream) of incoming and outgoing parameters, as well as 
the invocation style (synchronous or asynchronous). Please note that invocation 
of the procedures with incoming stream parameter in asynchronous style is not 
allowed.
@@ -212,7 +223,9 @@ Therefore, you need the same technique as described in the 
server streaming sect
 
 But there is another thing: requests also come in streaming mode. So you need 
the following:
 
-1. Set consumer strategy to DELEGATION - that differs from the default 
PROPAGATION option in the fact that consumer will not produce responses at all. 
If you set PROPAGATION, then you will receive more responses than you expected;
+1. Choose the consumer strategy. With `routeControlledStreamObserver=true` the 
consumer sends no responses itself with either strategy:
+the default `PROPAGATION` routes the events of a call one at a time, in order, 
while `DELEGATION` does not wait for an
+event to be done before it routes the next one;
 2. Forward `onError` and `onCompletion` on consumer;
 3. Set producer strategy to STREAMING.
 
diff --git 
a/components/camel-grpc/src/generated/resources/META-INF/org/apache/camel/component/grpc/grpc.json
 
b/components/camel-grpc/src/generated/resources/META-INF/org/apache/camel/component/grpc/grpc.json
index 05519fe07a38..09bb31224c06 100644
--- 
a/components/camel-grpc/src/generated/resources/META-INF/org/apache/camel/component/grpc/grpc.json
+++ 
b/components/camel-grpc/src/generated/resources/META-INF/org/apache/camel/component/grpc/grpc.json
@@ -43,7 +43,7 @@
     "forwardOnError": { "index": 5, "kind": "parameter", "displayName": 
"Forward On Error", "group": "common", "label": "common", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Determines if onError events should be pushed 
to the Camel route. Exceptions will be set a [...]
     "maxMessageSize": { "index": 6, "kind": "parameter", "displayName": "Max 
Message Size", "group": "common", "label": "", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 4194304, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "The maximum message size allowed to be 
received\/sent (MiB)" },
     "autoDiscoverServerInterceptors": { "index": 7, "kind": "parameter", 
"displayName": "Auto Discover Server Interceptors", "group": "consumer", 
"label": "consumer", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": true, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Setting the autoDiscoverServerInterceptors 
mechanism,  [...]
-    "consumerStrategy": { "index": 8, "kind": "parameter", "displayName": 
"Consumer Strategy", "group": "consumer", "label": "consumer", "required": 
false, "type": "enum", "javaType": 
"org.apache.camel.component.grpc.GrpcConsumerStrategy", "enum": [ 
"AGGREGATION", "PROPAGATION", "DELEGATION" ], "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "PROPAGATION", "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configura [...]
+    "consumerStrategy": { "index": 8, "kind": "parameter", "displayName": 
"Consumer Strategy", "group": "consumer", "label": "consumer", "required": 
false, "type": "enum", "javaType": 
"org.apache.camel.component.grpc.GrpcConsumerStrategy", "enum": [ 
"AGGREGATION", "PROPAGATION", "DELEGATION" ], "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "PROPAGATION", "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configura [...]
     "initialFlowControlWindow": { "index": 9, "kind": "parameter", 
"displayName": "Initial Flow Control Window", "group": "consumer", "label": 
"consumer", "required": false, "type": "integer", "javaType": "int", 
"deprecated": false, "autowired": false, "secret": false, "defaultValue": 
1048576, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Sets the initial flow control window in 
bytes." },
     "keepAliveTime": { "index": 10, "kind": "parameter", "displayName": "Keep 
Alive Time", "group": "consumer", "label": "consumer", "required": false, 
"type": "integer", "javaType": "long", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 7200000, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Sets a custom keepalive time in milliseconds, 
the delay time for sending next keepalive  [...]
     "keepAliveTimeout": { "index": 11, "kind": "parameter", "displayName": 
"Keep Alive Timeout", "group": "consumer", "label": "consumer", "required": 
false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": 20000, "configurationClass": 
"org.apache.camel.component.grpc.GrpcConfiguration", "configurationField": 
"configuration", "description": "Sets a custom keepalive timeout in 
milliseconds, the timeout for keepalive ping requ [...]
diff --git a/components/camel-grpc/src/main/docs/grpc-component.adoc 
b/components/camel-grpc/src/main/docs/grpc-component.adoc
index c092f6a8e8ed..b480d6c1b73a 100644
--- a/components/camel-grpc/src/main/docs/grpc-component.adoc
+++ b/components/camel-grpc/src/main/docs/grpc-component.adoc
@@ -73,6 +73,17 @@ To enable these features, the following component properties 
combinations must b
 |||jwtSubject||Optional
 |=======================================================================
 
+=== gRPC consumer reply
+
+The message body at the end of the consumer route is the reply to the client: 
the response message of the called
+method, or a `List` of them for a streaming response. A message without a body 
sends an empty response message.
+
+With `routeControlledStreamObserver=true` the route sends the responses 
itself, with a producer endpoint with
+`toRouteControlledStreamObserver=true`. The producer sends the event named by 
the `CamelGrpcEventType` header
+(`onNext`, `onCompleted` or `onError`). The consumer sets this header for the 
events of client streaming and
+bidirectional streaming calls. The body of an `onNext` event is sent to the 
client as is. The body of an `onError`
+event must be a `Throwable`, which ends the call.
+
 === gRPC producer resource type mapping
 
 The table below shows the types of objects in the message body, depending on 
the types (simple or stream) of incoming and outgoing parameters, as well as 
the invocation style (synchronous or asynchronous). Please note that invocation 
of the procedures with incoming stream parameter in asynchronous style is not 
allowed.
@@ -212,7 +223,9 @@ Therefore, you need the same technique as described in the 
server streaming sect
 
 But there is another thing: requests also come in streaming mode. So you need 
the following:
 
-1. Set consumer strategy to DELEGATION - that differs from the default 
PROPAGATION option in the fact that consumer will not produce responses at all. 
If you set PROPAGATION, then you will receive more responses than you expected;
+1. Choose the consumer strategy. With `routeControlledStreamObserver=true` the 
consumer sends no responses itself with either strategy:
+the default `PROPAGATION` routes the events of a call one at a time, in order, 
while `DELEGATION` does not wait for an
+event to be done before it routes the next one;
 2. Forward `onError` and `onCompletion` on consumer;
 3. Set producer strategy to STREAMING.
 
diff --git 
a/components/camel-grpc/src/main/java/org/apache/camel/component/grpc/GrpcConfiguration.java
 
b/components/camel-grpc/src/main/java/org/apache/camel/component/grpc/GrpcConfiguration.java
index b5154c051d44..a2a4a3de7304 100644
--- 
a/components/camel-grpc/src/main/java/org/apache/camel/component/grpc/GrpcConfiguration.java
+++ 
b/components/camel-grpc/src/main/java/org/apache/camel/component/grpc/GrpcConfiguration.java
@@ -349,7 +349,9 @@ public class GrpcConfiguration {
      * to the stream, and the response will be immediately sent back to the 
sender. If a delegation strategy is
      * selected, request is sent to the stream, but no response generated 
under the assumption that all necessary
      * responses will be sent at another part of route. Delegation strategy 
always comes with
-     * routeControlledStreamObserver=true to be able to achieve the assumption.
+     * routeControlledStreamObserver=true to be able to achieve the 
assumption. With routeControlledStreamObserver=true,
+     * the propagation strategy does not send responses either: it sends the 
requests to the route one at a time, and
+     * the route sends the responses.
      */
     public GrpcConsumerStrategy getConsumerStrategy() {
         return consumerStrategy;
diff --git 
a/components/camel-grpc/src/main/java/org/apache/camel/component/grpc/server/GrpcRequestPropagationStreamObserver.java
 
b/components/camel-grpc/src/main/java/org/apache/camel/component/grpc/server/GrpcRequestPropagationStreamObserver.java
index 4764cceac0b7..514673e81a3a 100644
--- 
a/components/camel-grpc/src/main/java/org/apache/camel/component/grpc/server/GrpcRequestPropagationStreamObserver.java
+++ 
b/components/camel-grpc/src/main/java/org/apache/camel/component/grpc/server/GrpcRequestPropagationStreamObserver.java
@@ -19,35 +19,57 @@ package org.apache.camel.component.grpc.server;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import io.grpc.stub.StreamObserver;
+import org.apache.camel.component.grpc.GrpcConstants;
 import org.apache.camel.component.grpc.GrpcConsumer;
 import org.apache.camel.component.grpc.GrpcEndpoint;
 
 /**
  * This is the default consumer strategy for client-streaming and 
bi-directional streaming gRPC calls. gRPC request
- * stream observer which is propagating every onNext(), onError() or 
onCompleted() calls to the Camel route.
+ * stream observer which is propagating every onNext(), onError() or 
onCompleted() calls to the Camel route, one event
+ * at a time.
+ * <p>
+ * With routeControlledStreamObserver=true the route sends the replies and 
ends the call itself, as with the DELEGATION
+ * strategy: the consumer sends nothing to the client.
  */
 public class GrpcRequestPropagationStreamObserver extends 
GrpcRequestAbstractStreamObserver {
 
+    private final boolean routeControlled;
+    // set once the call ended with an error due to an interrupt (both 
routeControlled and non-routeControlled):
+    // nothing more can be sent to the client
+    private final AtomicBoolean failed = new AtomicBoolean();
+
     public GrpcRequestPropagationStreamObserver(GrpcEndpoint endpoint, 
GrpcConsumer consumer,
                                                 StreamObserver<Object> 
responseObserver, Map<String, Object> headers) {
         super(endpoint, consumer, responseObserver, headers);
+        this.routeControlled = 
endpoint.getConfiguration().isRouteControlledStreamObserver();
     }
 
     @Override
     public void onNext(Object request) {
+        if (routeControlled && failed.get()) {
+            return;
+        }
         CountDownLatch latch = new CountDownLatch(1);
 
         exchange = endpoint.createExchange();
         exchange.getIn().setBody(request);
         exchange.getIn().setHeaders(headers);
+        if (routeControlled) {
+            exchange.setProperty(GrpcConstants.GRPC_RESPONSE_OBSERVER, 
responseObserver);
+        }
 
         consumer.process(exchange, doneSync -> latch.countDown());
 
         try {
             latch.await();
 
+            if (routeControlled) {
+                // the route sends the replies itself
+                return;
+            }
             Object responseBody = exchange.getMessage().getBody();
             if (responseBody instanceof List) {
                 List<?> responseList = (List<?>) responseBody;
@@ -57,8 +79,9 @@ public class GrpcRequestPropagationStreamObserver extends 
GrpcRequestAbstractStr
             }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
-            responseObserver.onError(e);
-
+            if (failed.compareAndSet(false, true)) {
+                responseObserver.onError(e);
+            }
         }
     }
 
@@ -66,15 +89,25 @@ public class GrpcRequestPropagationStreamObserver extends 
GrpcRequestAbstractStr
     public void onError(Throwable throwable) {
         exchange = endpoint.createExchange();
         exchange.getIn().setHeaders(headers);
+        if (routeControlled) {
+            exchange.setProperty(GrpcConstants.GRPC_RESPONSE_OBSERVER, 
responseObserver);
+        }
         consumer.onError(exchange, throwable);
-        responseObserver.onError(throwable);
+        if (!routeControlled && !failed.get()) {
+            responseObserver.onError(throwable);
+        }
     }
 
     @Override
     public void onCompleted() {
         exchange = endpoint.createExchange();
         exchange.getIn().setHeaders(headers);
+        if (routeControlled) {
+            exchange.setProperty(GrpcConstants.GRPC_RESPONSE_OBSERVER, 
responseObserver);
+        }
         consumer.onCompleted(exchange);
-        responseObserver.onCompleted();
+        if (!routeControlled && !failed.get()) {
+            responseObserver.onCompleted();
+        }
     }
 }
diff --git 
a/components/camel-grpc/src/test/java/org/apache/camel/component/grpc/GrpcPropagationRouteControlledTest.java
 
b/components/camel-grpc/src/test/java/org/apache/camel/component/grpc/GrpcPropagationRouteControlledTest.java
new file mode 100644
index 000000000000..12bb5ea8f384
--- /dev/null
+++ 
b/components/camel-grpc/src/test/java/org/apache/camel/component/grpc/GrpcPropagationRouteControlledTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.grpc;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.stub.StreamObserver;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.CamelEvent.ExchangeEvent;
+import org.apache.camel.support.EventNotifierSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * With the default PROPAGATION consumer strategy and 
routeControlledStreamObserver=true, the route sends the replies of
+ * client streaming and bidirectional streaming calls with the 
toRouteControlledStreamObserver producer: the consumer
+ * sends nothing itself.
+ */
+public class GrpcPropagationRouteControlledTest extends GrpcTestSupport {
+
+    private static final String CONSUMER = 
"grpc://localhost:0/org.apache.camel.component.grpc.PingPong?synchronous=true"
+                                           + 
"&consumerStrategy=PROPAGATION&routeControlledStreamObserver=true"
+                                           + 
"&forwardOnCompleted=true&muteException=false";
+    private static final String REPLY = 
"grpc://dummy:0/?toRouteControlledStreamObserver=true";
+
+    private final Map<String, ManagedChannel> channels = new 
ConcurrentHashMap<>();
+    // the exception of each exchange done by a consumer route (null when it 
completed), by route id
+    private final Map<String, List<Exception>> doneExchanges = new 
ConcurrentHashMap<>();
+    private final AtomicInteger clientStreamingPings = new AtomicInteger();
+
+    @AfterEach
+    public void stopGrpcChannels() {
+        channels.values().forEach(channel -> channel.shutdown().shutdownNow());
+        channels.clear();
+    }
+
+    @Test
+    public void bidiStreamingRouteSendsOneReplyPerEvent() throws Exception {
+        PongResponseStreamObserver responseObserver = new 
PongResponseStreamObserver();
+        StreamObserver<PingRequest> requestObserver
+                = 
PingPongGrpc.newStub(channel("propagation-bidi")).pingAsyncAsync(responseObserver);
+        requestObserver.onNext(pingRequest("PING1"));
+        requestObserver.onNext(pingRequest("PING2"));
+        requestObserver.onCompleted();
+        assertTrue(responseObserver.latch.await(5, TimeUnit.SECONDS));
+
+        assertNull(responseObserver.error, "the call must not end with an 
error");
+        assertEquals(2, responseObserver.pongResponses.size(), "exactly one 
reply per event: "
+                                                               + 
responseObserver.pongResponses);
+        assertEquals("PING1PONG", 
responseObserver.pongResponses.get(0).getPongName());
+        assertEquals("PING2PONG", 
responseObserver.pongResponses.get(1).getPongName());
+        assertEquals(1, responseObserver.completed.get());
+        // two onNext exchanges and the forwarded onCompleted exchange, none 
failed
+        assertEquals(Collections.nCopies(3, null), 
awaitDoneExchanges("propagation-bidi", 3));
+    }
+
+    @Test
+    public void clientStreamingRouteSendsTheReplyOnCompleted() throws 
Exception {
+        PongResponseStreamObserver responseObserver = new 
PongResponseStreamObserver();
+        StreamObserver<PingRequest> requestObserver
+                = 
PingPongGrpc.newStub(channel("propagation-client-streaming")).pingAsyncSync(responseObserver);
+        requestObserver.onNext(pingRequest("PING1"));
+        requestObserver.onNext(pingRequest("PING2"));
+        requestObserver.onCompleted();
+        assertTrue(responseObserver.latch.await(5, TimeUnit.SECONDS));
+
+        // the consumer does not send the unchanged requests of the onNext 
events as replies
+        assertNull(responseObserver.error, "the call must not end with an 
error");
+        assertEquals(1, responseObserver.pongResponses.size(), "exactly one 
reply: " + responseObserver.pongResponses);
+        assertEquals("PONG2", 
responseObserver.pongResponses.get(0).getPongName());
+        assertEquals(1, responseObserver.completed.get());
+        assertEquals(Collections.nCopies(3, null), 
awaitDoneExchanges("propagation-client-streaming", 3));
+    }
+
+    private List<Exception> awaitDoneExchanges(String routeId, int count) {
+        List<Exception> exceptions
+                = doneExchanges.computeIfAbsent(routeId, id -> 
Collections.synchronizedList(new ArrayList<>()));
+        await().atMost(5, TimeUnit.SECONDS).until(() -> exceptions.size() >= 
count);
+        synchronized (exceptions) {
+            assertEquals(count, exceptions.size(), "exchanges done by route " 
+ routeId + ": " + exceptions);
+            return new ArrayList<>(exceptions);
+        }
+    }
+
+    private static PingRequest pingRequest(String name) {
+        return PingRequest.newBuilder().setPingName(name).setPingId(1).build();
+    }
+
+    private ManagedChannel channel(String routeId) {
+        return channels.computeIfAbsent(routeId,
+                id -> ManagedChannelBuilder.forAddress("localhost", 
getRoutePort(id)).usePlaintext().build());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                getContext().getManagementStrategy().addEventNotifier(new 
EventNotifierSupport() {
+                    @Override
+                    public void notify(CamelEvent event) {
+                        Exchange exchange = ((ExchangeEvent) 
event).getExchange();
+                        if (exchange.getFromRouteId() != null) {
+                            
doneExchanges.computeIfAbsent(exchange.getFromRouteId(),
+                                    id -> Collections.synchronizedList(new 
ArrayList<>()))
+                                    .add(exchange.getException());
+                        }
+                    }
+
+                    @Override
+                    public boolean isEnabled(CamelEvent event) {
+                        return event instanceof 
CamelEvent.ExchangeCompletedEvent
+                                || event instanceof 
CamelEvent.ExchangeFailedEvent;
+                    }
+                });
+
+                // bidirectional streaming: a reply for each request, then the 
forwarded onCompleted ends the call
+                from(CONSUMER + "&hash=1")
+                        .routeId("propagation-bidi")
+                        .process(e -> {
+                            PingRequest request = 
e.getMessage().getBody(PingRequest.class);
+                            if (request != null) {
+                                
e.getMessage().setBody(PongResponse.newBuilder()
+                                        .setPongName(request.getPingName() + 
"PONG").setPongId(request.getPingId())
+                                        .build());
+                            }
+                        })
+                        .to(REPLY);
+
+                // client streaming: the requests are counted, and the single 
reply is sent on the forwarded onCompleted
+                from(CONSUMER + "&hash=2")
+                        .routeId("propagation-client-streaming")
+                        .choice()
+                        
.when(header(GrpcConstants.GRPC_EVENT_TYPE_HEADER).isEqualTo(GrpcConstants.GRPC_EVENT_TYPE_ON_NEXT))
+                        .process(e -> clientStreamingPings.incrementAndGet())
+                        .otherwise()
+                        .process(e -> e.getMessage().setBody(
+                                PongResponse.newBuilder().setPongName("PONG" + 
clientStreamingPings.get()).build()))
+                        .setHeader(GrpcConstants.GRPC_EVENT_TYPE_HEADER, 
constant(GrpcConstants.GRPC_EVENT_TYPE_ON_NEXT))
+                        .to(REPLY)
+                        .setHeader(GrpcConstants.GRPC_EVENT_TYPE_HEADER,
+                                
constant(GrpcConstants.GRPC_EVENT_TYPE_ON_COMPLETED))
+                        .to(REPLY)
+                        .end();
+            }
+        };
+    }
+
+    static class PongResponseStreamObserver implements 
StreamObserver<PongResponse> {
+        final List<PongResponse> pongResponses = new CopyOnWriteArrayList<>();
+        final AtomicInteger completed = new AtomicInteger();
+        final CountDownLatch latch = new CountDownLatch(1);
+        volatile Throwable error;
+
+        @Override
+        public void onNext(PongResponse value) {
+            pongResponses.add(value);
+        }
+
+        @Override
+        public void onError(Throwable t) {
+            error = t;
+            latch.countDown();
+        }
+
+        @Override
+        public void onCompleted() {
+            completed.incrementAndGet();
+            latch.countDown();
+        }
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index de1af9af0608..f32382d9b5b2 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -214,6 +214,16 @@ nodes, and XInclude is not processed. Before, the Flowable 
7 deserializer parsed
 An event that relies on an external DTD or an external entity no longer gets 
their content. A channel that sets
 `deserializerDelegateExpression` keeps its own deserializer, which can parse 
such events.
 
+=== camel-grpc - routeControlledStreamObserver now supported for the 
PROPAGATION strategy
+
+The `routeControlledStreamObserver` option now works with the PROPAGATION 
consumer strategy
+(CAMEL-26550). When enabled, the consumer dispatches each streaming event to 
the route and the
+route is responsible for sending replies via 
`toRouteControlledStreamObserver`, exactly as with
+the DELEGATION strategy.
+
+No changes are required for existing routes that use PROPAGATION without
+`routeControlledStreamObserver=true`.
+
 === camel-jsch - the scp producer refuses a file name with a line feed or a 
NUL character
 
 The `scp` producer now refuses a file name with a line feed or a NUL 
character, in the file name or in one of its
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GrpcEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GrpcEndpointBuilderFactory.java
index 32cb1e241016..8be192e47867 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GrpcEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GrpcEndpointBuilderFactory.java
@@ -210,7 +210,10 @@ public interface GrpcEndpointBuilderFactory {
          * stream, but no response generated under the assumption that all
          * necessary responses will be sent at another part of route. 
Delegation
          * strategy always comes with routeControlledStreamObserver=true to be
-         * able to achieve the assumption.
+         * able to achieve the assumption. With
+         * routeControlledStreamObserver=true, the propagation strategy does 
not
+         * send responses either: it sends the requests to the route one at a
+         * time, and the route sends the responses.
          * 
          * The option is a:
          * <code>org.apache.camel.component.grpc.GrpcConsumerStrategy</code>
@@ -237,7 +240,10 @@ public interface GrpcEndpointBuilderFactory {
          * stream, but no response generated under the assumption that all
          * necessary responses will be sent at another part of route. 
Delegation
          * strategy always comes with routeControlledStreamObserver=true to be
-         * able to achieve the assumption.
+         * able to achieve the assumption. With
+         * routeControlledStreamObserver=true, the propagation strategy does 
not
+         * send responses either: it sends the requests to the route one at a
+         * time, and the route sends the responses.
          * 
          * The option will be converted to a
          * <code>org.apache.camel.component.grpc.GrpcConsumerStrategy</code>

Reply via email to