oscerd commented on code in PR #27834:
URL: https://github.com/apache/camel/pull/27834#discussion_r4280267391


##########
components/camel-azure/camel-azure-servicebus/src/main/java/org/apache/camel/component/azure/servicebus/ServiceBusConsumer.java:
##########
@@ -46,13 +46,22 @@
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.SynchronizationAdapter;
 import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.StringHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class ServiceBusConsumer extends DefaultConsumer implements 
ShutdownAware {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(ServiceBusConsumer.class);
     private static final int LOCK_RENEW_INTERVAL_SECONDS = 10;
+    // Azure recommends putting the exception type in the dead-letter reason 
and its stack trace in the description,
+    // but warns that doing so "might result in some messages exceeding the 
256 KB quota limit for the Standard tier".
+    // A rejected dead-letter request leaves the message locked, so cap both 
rather than risk the quota: the head of a
+    // stack trace carries the cause, the tail is error-handler frames.
+    private static final int MAX_DEAD_LETTER_REASON_LENGTH = 1024;
+    private static final int MAX_DEAD_LETTER_DESCRIPTION_LENGTH = 8192;

Review Comment:
   You are right and I was wrong — thanks for pushing back with the source 
rather than the doc. Fixed in `7941ee6`: both fields are now capped at 4096.
   
   I verified it rather than take it second-hand, and it holds:
   
   - `azure-sdk-for-net`, 
`sdk/servicebus/Azure.Messaging.ServiceBus/src/Constants.cs` — `public const 
int MaxDeadLetterReasonLength = 4096;`
   - the same repo, `Amqp/AmqpReceiver.cs`, as the first statements of 
`DeadLetterInternalAsync` — `Argument.AssertNotTooLong(deadLetterReason, 
Constants.MaxDeadLetterReasonLength, ...)` and 
`Argument.AssertNotTooLong(deadLetterErrorDescription, 
Constants.MaxDeadLetterReasonLength, ...)`. Both fields, against the reason 
constant, before either disposition path.
   - `azure-sdk-for-js`, `sdk/core/core-amqp/src/util/constants.ts` — 
`maxDeadLetterReasonLength: 4096`.
   
   **Why I got it wrong**, since it is the interesting part. I originally cited 
4096 from the .NET `DeadLetterMessageAsync` reference page. Then I went looking 
for corroboration, found none on the dead-letter concept page or the quotas 
page, found that the Java SDK's `DeadLetterOptions` does no validation, and 
retracted the number on CAMEL-25660. That inference rested on two absences — no 
figure in the prose docs, no check in the Java SDK — and read absence of 
evidence as evidence of absence. The Java SDK turned out to be the outlier 
among three. I have withdrawn the retraction on the issue with the quotes above.
   
   The 256 KB message quota is still real and still a reason to bound a stack 
trace; it is just not the tightest constraint. The class comment now gives 
both, with 4096 as the operative one.
   
   One difference from your suggestion worth flagging: rather than setting the 
constant to 4093 so that the suffix brings it to 4096, I kept 
`MAX_DEAD_LETTER_TEXT_LENGTH = 4096` — the number a reader can match against 
the SDK constant — and subtract the suffix where it is applied:
   
   ```java
   private String limitToDeadLetterLength(String value) {
       return StringHelper.limitLength(value, MAX_DEAD_LETTER_TEXT_LENGTH - 
TRUNCATED_SUFFIX.length(), TRUNCATED_SUFFIX);
   }
   ```
   
   Same output, and `4093` never appears as a magic number needing its own 
explanation. The test asserts `hasSizeLessThanOrEqualTo(4096)` on both fields, 
and reverting the constant to 8192 fails it on all 3 runs.
   
   _Claude Code on behalf of oscerd_
   



##########
components/camel-azure/camel-azure-servicebus/src/main/java/org/apache/camel/component/azure/servicebus/ServiceBusConsumer.java:
##########
@@ -405,5 +407,43 @@ public void onFailure(Exchange exchange) {
                 pendingExchanges.decrementAndGet();
             }
         }
+
+        /**
+         * Describes the failure for the dead-letter queue, bounded so that 
the diagnostics cannot be what makes the
+         * broker reject the message.
+         */
+        private DeadLetterOptions deadLetterOptions(Exception cause) {
+            String stackTrace = Arrays.stream(cause.getStackTrace())
+                    .map(StackTraceElement::toString)
+                    .collect(Collectors.joining("\n"));
+
+            return new DeadLetterOptions()
+                    .setDeadLetterReason(StringHelper.limitLength(
+                            String.format("%s: %s", 
cause.getClass().getName(), cause.getMessage()),
+                            MAX_DEAD_LETTER_REASON_LENGTH, TRUNCATED_SUFFIX))
+                    .setDeadLetterErrorDescription(StringHelper.limitLength(
+                            stackTrace, MAX_DEAD_LETTER_DESCRIPTION_LENGTH, 
TRUNCATED_SUFFIX));

Review Comment:
   Correct, and it made the cap wrong by three characters. 
`StringHelper.limitLength(s, maxLength, suffix)` cuts to `maxLength` and *then* 
appends, so it returns `maxLength + suffix.length()`.
   
   Fixed in `7941ee6` by subtracting the suffix where the cap is applied, so 
the constant can stay the number that matches the SDK:
   
   ```java
   return StringHelper.limitLength(value, MAX_DEAD_LETTER_TEXT_LENGTH - 
TRUNCATED_SUFFIX.length(), TRUNCATED_SUFFIX);
   ```
   
   The test now asserts `hasSizeLessThanOrEqualTo(4096)` rather than the `max + 
3` it had before — which, embarrassingly, encoded the bug as the expectation.
   
   _Claude Code on behalf of oscerd_
   



##########
components/camel-azure/camel-azure-servicebus/src/main/java/org/apache/camel/component/azure/servicebus/ServiceBusConsumer.java:
##########
@@ -405,5 +407,43 @@ public void onFailure(Exchange exchange) {
                 pendingExchanges.decrementAndGet();
             }
         }
+
+        /**
+         * Describes the failure for the dead-letter queue, bounded so that 
the diagnostics cannot be what makes the
+         * broker reject the message.
+         */
+        private DeadLetterOptions deadLetterOptions(Exception cause) {
+            String stackTrace = Arrays.stream(cause.getStackTrace())
+                    .map(StackTraceElement::toString)
+                    .collect(Collectors.joining("\n"));
+
+            return new DeadLetterOptions()
+                    .setDeadLetterReason(StringHelper.limitLength(
+                            String.format("%s: %s", 
cause.getClass().getName(), cause.getMessage()),
+                            MAX_DEAD_LETTER_REASON_LENGTH, TRUNCATED_SUFFIX))
+                    .setDeadLetterErrorDescription(StringHelper.limitLength(
+                            stackTrace, MAX_DEAD_LETTER_DESCRIPTION_LENGTH, 
TRUNCATED_SUFFIX));
+        }
+
+        /**
+         * Dead-letters the message, reporting a rejected request instead of 
letting it leave this synchronization.
+         * <p/>
+         * Nothing else would report it: the exception would surface inside 
the Azure SDK's processor callback, the
+         * message would stay locked until the lock expired, and Service Bus 
would redeliver it until
+         * {@code maxDeliveryCount} was reached - dead-lettering it with its 
own reason, which is the opposite of what
+         * {@code enableDeadLettering} is for.

Review Comment:
   Confirmed and corrected in `7941ee6`. `UnitOfWorkHelper.doneSynchronization` 
wraps the `synchronization.onFailure(exchange)` call and catches:
   
   ```java
   } catch (Exception e) {
       // must catch exceptions to ensure all synchronizations have a chance to 
run
       LOG.warn("Exception occurred during onCompletion. This exception will be 
ignored.", e);
   }
   ```
   
   So nothing reaches the SDK callback, and my javadoc, the PR description and 
the Jira all overstated it. The javadoc now says what the change actually buys: 
the failure is reported through the consumer's `ExceptionHandler`, where it is 
attributable to this endpoint and routable, instead of a generic core warning 
that names `onCompletion`. The part that does not change either way — the 
message is unsettled, stays locked, is redelivered, and ends up dead-lettered 
as `MaxDeliveryCountExceeded` — is still stated, since that is the 
operator-visible consequence.
   
   _Claude Code on behalf of oscerd_
   



##########
components/camel-azure/camel-azure-servicebus/src/main/java/org/apache/camel/component/azure/servicebus/ServiceBusConsumer.java:
##########
@@ -385,17 +394,10 @@ public void onFailure(Exchange exchange) {
                 if (getConfiguration().getServiceBusReceiveMode() == 
ServiceBusReceiveMode.PEEK_LOCK) {
                     if (getConfiguration().isEnableDeadLettering() && 
(ObjectHelper.isEmpty(getConfiguration().getSubQueue())
                             || 
ObjectHelper.equal(getConfiguration().getSubQueue(), SubQueue.NONE))) {
-                        DeadLetterOptions deadLetterOptions = new 
DeadLetterOptions();
                         if (cause != null) {
-                            deadLetterOptions
-                                    .setDeadLetterReason(
-                                            String.format("%s: %s", 
cause.getClass().getName(), cause.getMessage()));
-                            
deadLetterOptions.setDeadLetterErrorDescription(Arrays.stream(cause.getStackTrace())
-                                    .map(StackTraceElement::toString)
-                                    .collect(Collectors.joining("\n")));
-                            messageContext.deadLetter(deadLetterOptions);
+                            deadLetter(exchange, deadLetterOptions(cause));
                         } else {
-                            messageContext.deadLetter();
+                            deadLetter(exchange, null);
                         }
                     } else {
                         messageContext.abandon();

Review Comment:
   Taken, in `7941ee6`. Both settlement calls in `onFailure` now go through one 
helper:
   
   ```java
   settle(exchange, "dead-letter", () -> messageContext.deadLetter(options));
   ...
   settle(exchange, "abandon", messageContext::abandon);
   ```
   
   New test `synchronizationReportsARejectedAbandonInsteadOfThrowing` covers 
it: dead-lettering off so the failure path abandons, `abandon()` stubbed to 
throw, asserting the exception handler receives it.
   
   For completeness, the other `abandon()` in this class — in the catch around 
`getAsyncProcessor().process(...)` — is already wrapped in its own try/catch, 
so it was the only unguarded one.
   
   _Claude Code on behalf of oscerd_
   



##########
docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc:
##########
@@ -414,6 +414,20 @@ Previously it returned only the first chunk of the event 
log (at most 500 events
 any sign. A wide `after`/`before` window now returns every event in one list: 
that is one request per 500 events plus
 one that finds the end of the log, and more memory. Narrow the window when the 
event log is large.
 
+=== camel-azure-servicebus - the dead-letter reason and description are bounded
+
+When the consumer dead-letters a message after a failed route with 
`enableDeadLettering=true`, the reason and the
+error description it writes are now capped - the reason at 1024 characters and 
the description, which holds the stack
+trace, at 8192. A value that was cut ends with `...` so it is clear it was 
truncated rather than that the trace ended
+there.
+
+Azure recommends putting the exception type in the reason and its stack trace 
in the description, but warns that doing
+so can push a message past the 256 KB quota of the Standard tier. Nothing 
bounded either field before, so a long
+stack trace could make the broker reject the dead-letter request - and because 
that rejection was not handled, the
+message was then left locked and redelivered until `maxDeliveryCount` was 
reached, at which point the broker
+dead-lettered it with its own `MaxDeliveryCountExceeded` reason. A rejected 
request is now reported through the
+consumer's exception handler instead.

Review Comment:
   Agreed, and the old wording was inaccurate as well as long — it said the 
rejection "was not handled", when the unit of work was logging a WARN for it 
all along. Cut to:
   
   > When the consumer dead-letters a message after a failed route with 
`enableDeadLettering=true`, the reason and the error description it writes are 
now capped at 4096 characters, the limit the Azure SDKs apply to both fields. A 
value that was cut ends with `...`. The description holds the stack trace, so a 
deep one is now truncated rather than sent in full and rejected by the broker.
   >
   > A settlement request the broker rejects is now reported through the 
consumer's exception handler.
   
   Kept the first paragraph because the truncation is the part an upgrading 
user can actually observe — a stack trace in their dead-letter queue that now 
stops at 4096 characters.
   
   _Claude Code on behalf of oscerd_
   



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