gnodet-bot commented on code in PR #28730:
URL: https://github.com/apache/camel/pull/28730#discussion_r4441300592


##########
components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/support/streaming/KafkaRecordStreamingProcessor.java:
##########
@@ -104,6 +145,363 @@ public ProcessingResult processExchange(
         return result;
     }
 
+    /**
+     * A route that did not claim the source metadata with an exactly-once 
producer still has to advance the source
+     * offset (for example, a filter branch). Run this after route completion 
on the consumer polling thread.
+     */
+    private void completeExactlyOnceProcessing(Exchange exchange) {
+        if (!configuration.isExactlyOnce()) {
+            return;
+        }
+
+        KafkaExactlyOnceMetadata metadata = exchange.getProperty(
+                KafkaExactlyOnceMetadata.EXCHANGE_PROPERTY, 
KafkaExactlyOnceMetadata.class);
+        if (metadata != null && metadata.isDeadLetterSendFailed()) {
+            if (exchange.getException() == null) {
+                exchange.setException(new IllegalStateException(
+                        "A Kafka dead-letter send failed; the source record 
must be replayed"));
+            }
+            return;
+        }
+        if (metadata != null && metadata.isClaimRejected()) {
+            if (exchange.getException() == null) {
+                exchange.setException(new IllegalStateException(
+                        "The Kafka exactlyOnce source metadata was rejected by 
another exchange or producer: "
+                                                                + 
metadata.getClaimRejectionReason()));
+            }
+            return;
+        }
+
+        boolean transactionAborted = Boolean.TRUE.equals(
+                
exchange.getProperty(KafkaExactlyOnceMetadata.TRANSACTION_ABORTED_PROPERTY));
+        if (transactionAborted || exchange.isRollbackOnly()) {
+            if (exchange.getException() == null) {
+                exchange.setException(new IllegalStateException(
+                        "The Kafka exactlyOnce transaction was rolled back; 
the source record must be replayed"));
+            }
+            return;
+        }
+        if (exchange.getException() != null) {
+            return;
+        }
+
+        Throwable sendFailure = metadata != null ? metadata.getSendFailure() : 
null;
+        if (sendFailure != null && !metadata.isClaimed()) {
+            // A producer failed before it could register a transaction 
synchronization. Do not mistake a handled
+            // configuration/startup failure for a filter branch and commit 
its source offset.
+            exchange.setException(new IllegalStateException(
+                    "The exactlyOnce Kafka producer failed before it could 
claim the source offsets", sendFailure));
+            return;
+        }
+        if (metadata == null) {
+            exchange.setException(new IllegalStateException(
+                    "exactlyOnce source metadata was not captured for the 
consumed record"));
+            return;
+        }
+        if (!metadata.isClaimed()) {
+            if (hasAsynchronousHandoff(exchange)) {
+                exchange.setException(new IllegalStateException(
+                        "An exactlyOnce Kafka source cannot commit offsets 
directly when the route hands the exchange "
+                                                                + "to an 
asynchronous endpoint before an exactly-once Kafka producer claims it; "
+                                                                + "invoke the 
destination synchronously from the source exchange"));
+                return;
+            }
+            try {
+                metadata.commitOffsets();
+            } catch (Exception commitFailure) {
+                exchange.setException(commitFailure);
+            }
+        }
+    }
+
+    /**
+     * Detect asynchronous hand-offs actually performed by this source 
exchange. The event notifier marks the shared
+     * metadata object, so the original source observes a hand-off even when 
Camel emits the event for a copied
+     * exchange.
+     */
+    boolean hasAsynchronousHandoff(Exchange exchange) {
+        String toEndpoint = 
exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT, String.class);
+        KafkaExactlyOnceMetadata metadata = exchange.getProperty(
+                KafkaExactlyOnceMetadata.EXCHANGE_PROPERTY, 
KafkaExactlyOnceMetadata.class);
+        return isAsynchronousHandoffEndpoint(toEndpoint)
+                || metadata != null && metadata.hasAsynchronousKafkaHandoff();
+    }
+
+    /**
+     * Register an event notifier while at least one exactly-once Kafka source 
consumer is active in this context.
+     * Events without exactly-once metadata return before URI inspection, 
avoiding work for unrelated routes.
+     */
+    static void registerAsynchronousHandoffTracking(CamelContext context) {
+        if (context == null) {
+            return;
+        }
+        synchronized (ASYNCHRONOUS_HANDOFF_TRACKED_CONTEXTS) {
+            HandoffTrackingRegistration existing = 
ASYNCHRONOUS_HANDOFF_TRACKED_CONTEXTS.get(context);
+            if (existing != null) {
+                existing.references++;
+                return;
+            }
+
+            EventNotifierSupport notifier = new EventNotifierSupport() {
+                @Override
+                public void notify(CamelEvent event) {
+                    if (event instanceof CamelEvent.ExchangeSendingEvent 
sendingEvent) {
+                        Exchange exchange = sendingEvent.getExchange();
+                        KafkaExactlyOnceMetadata metadata = metadata(exchange);
+                        if (metadata == null) {
+                            return;
+                        }
+                        if 
(isAsynchronousHandoffEndpoint(sendingEvent.getEndpoint().getEndpointUri())) {
+                            // The metadata object is shared by shallow 
exchange copies; do not put this marker only on
+                            // the copied exchange that raised 
ExchangeSendingEvent.
+                            metadata.markAsynchronousKafkaHandoff();
+                        }
+                        return;
+                    }
+
+                    if (event instanceof 
CamelEvent.ExchangeFailureHandlingEvent handlingEvent) {
+                        Exchange exchange = handlingEvent.getExchange();
+                        if (metadata(exchange) == null || 
!handlingEvent.isDeadLetterChannel()) {
+                            return;
+                        }
+                        Object current = exchange.getProperty(
+                                
KafkaExactlyOnceMetadata.FAILURE_HANDLER_DELIVERY_DEPTH_PROPERTY);
+                        int depth = current instanceof Integer value ? value : 
0;
+                        exchange.setProperty(
+                                
KafkaExactlyOnceMetadata.FAILURE_HANDLER_DELIVERY_DEPTH_PROPERTY, depth + 1);
+                        exchange.setProperty(
+                                
KafkaExactlyOnceMetadata.FAILURE_HANDLER_DELIVERY_PROPERTY, Boolean.TRUE);
+                        return;
+                    }
+
+                    if (event instanceof 
CamelEvent.ExchangeFailureHandledEvent handledEvent) {
+                        Exchange exchange = handledEvent.getExchange();
+                        if (metadata(exchange) == null || 
!handledEvent.isDeadLetterChannel()) {
+                            return;
+                        }
+                        Object current = exchange.getProperty(
+                                
KafkaExactlyOnceMetadata.FAILURE_HANDLER_DELIVERY_DEPTH_PROPERTY);
+                        int depth = current instanceof Integer value ? value : 
0;
+                        if (depth <= 1) {
+                            exchange.removeProperty(
+                                    
KafkaExactlyOnceMetadata.FAILURE_HANDLER_DELIVERY_DEPTH_PROPERTY);
+                            exchange.removeProperty(
+                                    
KafkaExactlyOnceMetadata.FAILURE_HANDLER_DELIVERY_PROPERTY);
+                        } else {
+                            exchange.setProperty(
+                                    
KafkaExactlyOnceMetadata.FAILURE_HANDLER_DELIVERY_DEPTH_PROPERTY, depth - 1);
+                        }
+                    }
+                }
+            };
+            notifier.setIgnoreCamelContextEvents(true);
+            notifier.setIgnoreCamelContextInitEvents(true);
+            notifier.setIgnoreRouteEvents(true);
+            notifier.setIgnoreServiceEvents(true);
+            notifier.setIgnoreStepEvents(true);
+            notifier.setIgnoreExchangeCreatedEvent(true);
+            notifier.setIgnoreExchangeCompletedEvent(true);
+            // FailureHandling/FailureHandled are ExchangeFailureEvent 
variants; keep them enabled to distinguish
+            // a real Dead Letter Channel from doTry/doCatch and 
onException(...).continued(true).
+            notifier.setIgnoreExchangeFailedEvents(false);
+            notifier.setIgnoreExchangeRedeliveryEvents(true);
+            notifier.setIgnoreExchangeSentEvents(true);
+            notifier.setIgnoreExchangeSendingEvents(false);
+            context.getManagementStrategy().addEventNotifier(notifier);
+            ASYNCHRONOUS_HANDOFF_TRACKED_CONTEXTS.put(context, new 
HandoffTrackingRegistration(notifier));
+        }
+    }
+
+    /** Remove the notifier when the final exactly-once Kafka source consumer 
stops. */
+    static void unregisterAsynchronousHandoffTracking(CamelContext context) {
+        if (context == null) {
+            return;
+        }
+        synchronized (ASYNCHRONOUS_HANDOFF_TRACKED_CONTEXTS) {
+            HandoffTrackingRegistration registration = 
ASYNCHRONOUS_HANDOFF_TRACKED_CONTEXTS.get(context);
+            if (registration == null) {
+                return;
+            }
+            if (--registration.references == 0) {
+                
context.getManagementStrategy().removeEventNotifier(registration.notifier);
+                ASYNCHRONOUS_HANDOFF_TRACKED_CONTEXTS.remove(context);
+            }
+        }
+    }
+
+    private static KafkaExactlyOnceMetadata metadata(Exchange exchange) {
+        return 
exchange.getProperty(KafkaExactlyOnceMetadata.EXCHANGE_PROPERTY, 
KafkaExactlyOnceMetadata.class);
+    }
+
+    /**
+     * Validate WireTap targets before a source consumer starts polling. A 
WireTap runs on a copied exchange and cannot
+     * participate in the source transaction. Static target graphs are 
followed through route endpoints; dynamic WireTap
+     * URIs and dynamic send EIPs in a target graph are rejected 
conservatively because their destination is unknowable
+     * at startup.
+     */
+    static void validateNoKafkaWireTaps(CamelContext context, String routeId) {
+        if (context == null || routeId == null) {
+            throw new IllegalArgumentException(
+                    "exactlyOnce requires a route id and CamelContext to 
validate WireTap destinations");
+        }
+        Route sourceRoute = context.getRoute(routeId);
+        if (sourceRoute == null) {
+            throw new IllegalArgumentException(
+                    "exactlyOnce cannot validate WireTap destinations because 
route " + routeId + " is not available");
+        }
+        if (containsKafkaWireTap(sourceRoute.navigate(), context, new 
HashSet<>())) {
+            throw new IllegalArgumentException(
+                    "Kafka exactlyOnce does not support a WireTap whose target 
can reach a Kafka producer; "
+                                               + "send to Kafka synchronously 
from the original source exchange (route "
+                                               + routeId + ")");
+        }
+    }
+
+    private static boolean containsKafkaWireTap(
+            Navigate<Processor> navigation, CamelContext context, Set<String> 
visitedRoutes) {
+        if (navigation == null || !navigation.hasNext()) {
+            return false;
+        }
+        List<Processor> children = navigation.next();
+        if (children == null) {
+            return false;
+        }
+        for (Processor child : children) {
+            Processor processor = unwrapChannel(child);
+            if (processor == null) {
+                continue;
+            }
+            if (processor instanceof WireTapProcessor wireTap
+                    && (isDynamicKafkaWireTap(wireTap) || 
routesToKafka(context, wireTap.getUri(), visitedRoutes))) {
+                return true;
+            }
+            if (processor instanceof Navigate<?> nested) {
+                @SuppressWarnings("unchecked")
+                Navigate<Processor> childNavigation = (Navigate<Processor>) 
nested;
+                if (containsKafkaWireTap(childNavigation, context, 
visitedRoutes)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private static boolean routesToKafka(CamelContext context, String uri, 
Set<String> visitedRoutes) {
+        String target = endpointKey(uri);
+        if (target.startsWith("kafka:")) {
+            return true;
+        }
+        if (target.isEmpty() || !visitedRoutes.add(target)) {
+            return false;
+        }
+        for (Route route : context.getRoutes()) {
+            if (route.getEndpoint() != null
+                    && 
endpointKey(route.getEndpoint().getEndpointUri()).equals(target)
+                    && routeMayReachKafka(route.navigate(), context, 
visitedRoutes)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean routeMayReachKafka(
+            Navigate<Processor> navigation, CamelContext context, Set<String> 
visitedRoutes) {
+        if (navigation == null || !navigation.hasNext()) {
+            return false;
+        }
+        List<Processor> children = navigation.next();
+        if (children == null) {
+            return false;
+        }
+        for (Processor child : children) {
+            Processor processor = unwrapChannel(child);
+            if (processor == null) {
+                continue;
+            }
+            if (processor instanceof WireTapProcessor wireTap) {
+                if (isDynamicKafkaWireTap(wireTap) || routesToKafka(context, 
wireTap.getUri(), visitedRoutes)) {
+                    return true;
+                }
+            } else if (processor instanceof EndpointSending && !(processor 
instanceof SendProcessor)) {
+                // SendDynamicProcessor, RecipientList and RoutingSlip can 
resolve destinations at runtime.
+                return true;
+            } else if (processor instanceof EndpointAware endpointAware && 
endpointAware.getEndpoint() != null) {
+                String endpointUri = 
endpointAware.getEndpoint().getEndpointUri();
+                String endpoint = endpointKey(endpointUri);
+                if (endpoint.startsWith("kafka:")) {
+                    return true;
+                }
+                if (isRoutedEndpoint(endpoint) && routesToKafka(context, 
endpointUri, visitedRoutes)) {
+                    return true;
+                }
+            } else if (processor instanceof EndpointSending) {
+                // A dynamic send can resolve to Kafka at runtime; fail safe 
for Kafka-directed WireTap graphs.
+                return true;
+            }
+
+            if (processor instanceof Navigate<?> nested) {
+                @SuppressWarnings("unchecked")
+                Navigate<Processor> childNavigation = (Navigate<Processor>) 
nested;
+                if (routeMayReachKafka(childNavigation, context, 
visitedRoutes)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Determine whether the WireTap actually uses a dynamic sender.
+     *
+     * The dynamicUri option defaults to true, but a static URI without a 
Simple expression is still optimized to a
+     * normal SendProcessor by WireTapReifier. ignoreInvalidEndpoint also 
requires a dynamic sender.
+     */
+    private static boolean isDynamicKafkaWireTap(WireTapProcessor wireTap) {
+        return wireTap.isIgnoreInvalidEndpoint()
+                || wireTap.isDynamicUri() && 
LanguageSupport.hasSimpleFunction(wireTap.getUri())
+                        && LanguageSupport.hasSimpleFunction(wireTap.getUri());

Review Comment:
   💡 **Nit: duplicated `hasSimpleFunction` call**
   
   `LanguageSupport.hasSimpleFunction(wireTap.getUri())` is evaluated twice in 
the same `&&` chain. The second call is always identical to the first (the 
method is pure) and cannot change the result. The condition currently reads 
`isDynamicUri() && hasSimpleFunction(uri) && hasSimpleFunction(uri)`.
   
   This looks like a copy-paste error — the second clause was likely intended 
to be a different guard (e.g. 
`LanguageSupport.hasDynamicFunction(wireTap.getUri())` to also catch other 
expression languages, or simply `!wireTap.getUri().isEmpty()`). If 
`hasSimpleFunction` is the only intended check, remove the duplicate:
   
   ```suggestion
       private static boolean isDynamicKafkaWireTap(WireTapProcessor wireTap) {
           return wireTap.isIgnoreInvalidEndpoint()
                   || wireTap.isDynamicUri() && 
LanguageSupport.hasSimpleFunction(wireTap.getUri());
       }
   ```



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

Reply via email to