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


##########
components/camel-paho-mqtt5/src/main/docs/paho-mqtt5-component.adoc:
##########
@@ -79,6 +79,29 @@ String payload = "message";
 producerTemplate.sendBody("paho-mqtt5:topic", payload);
 ----
 
+=== Manual acknowledgements
+
+With `manualAcksEnabled=true` a message is acknowledged when its exchange 
completes. MQTT has no negative
+acknowledgement: the broker sends an unacknowledged message again only when 
the client connects again to a session
+that the broker kept, and it does not send more than `receiveMaximum` 
unacknowledged messages to the client. The
+client holds back only the acknowledgement of a QoS 1 message; it completes 
the QoS 2 flow on its own when the message
+arrives, so the broker never sends a QoS 2 message again. So when an exchange 
fails:
+
+* for a QoS 1 message on a session that the broker keeps after the disconnect 
(`cleanStart=false` and
+  `sessionExpiryInterval` greater than 0) the consumer disconnects and 
connects again, and the broker sends the
+  message again;
+* in all other cases (a clean session, which is the default, a QoS 2 message, 
or `cleanStart=false` without
+  `sessionExpiryInterval`, where the session ends with the connection) the 
broker never sends the message again, so
+  the consumer acknowledges it, and the failure is left to the Camel error 
handler;
+* with a client set by the `client` option, a QoS 1 message on a session that 
the broker keeps stays unacknowledged
+  until that client connects again.
+
+The reconnects of failed exchanges in a row back off: the first one waits one 
second, each further one waits twice as

Review Comment:
   💡 **Minor doc imprecision:** "the first one waits one second" implies a 
hardcoded value, but the code actually uses `automaticReconnectMinDelay` from 
`MqttConnectionOptions` (defaulting to 1 s). If a user configures a different 
min delay, the docs will be misleading. Consider wording like: _"the first one 
waits `automaticReconnectMinDelay` (default: one second)"_.



##########
components/camel-paho-mqtt5/src/main/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5Consumer.java:
##########
@@ -230,6 +266,122 @@ private Exception closeOwnedClient(MqttClient 
ownedClient, Exception primaryExce
         return primaryException;
     }
 
+    /**
+     * MQTT has no negative acknowledgement: the broker sends an 
unacknowledged message again only when the client
+     * connects again to a session that the broker kept, and it sends no more 
than receiveMaximum unacknowledged
+     * messages to the client. Paho holds back only the PUBACK of a QoS 1 
message until it is acknowledged manually; it
+     * completes the QoS 2 flow (PUBREC, PUBCOMP) on its own, so the broker 
never sends a QoS 2 message again. A failed
+     * QoS 1 message that is just left unacknowledged is therefore never 
retried, and enough of them stop the consumer
+     * for good.
+     * <p>
+     * For a QoS 1 message on a session that the broker keeps after the 
disconnect (cleanStart=false and
+     * sessionExpiryInterval greater than 0) the client reconnects, so that 
the broker sends the message again; the
+     * reconnects of failed exchanges in a row back off (see {@link 
#reconnectDelay(int)}), so that a message that
+     * always fails does not reconnect in a tight loop. In every other case 
the broker never sends the message again, so
+     * it is acknowledged, which frees its place in the receive window.
+     */
+    private void rollback(Exchange exchange, MqttMessage mqttMessage) {
+        MqttClient current = client;
+        if (current == null) {
+            return;
+        }
+        if (mqttMessage.getQos() != 1 || !isSessionKeptAfterDisconnect()) {
+            LOG.debug("Acknowledging failed MQTT message with ID: {} (QoS {}) 
of Exchange ID: {} as the broker does not"
+                      + " deliver it again",
+                    mqttMessage.getId(), mqttMessage.getQos(), 
exchange.getExchangeId());
+            try {
+                current.messageArrivedComplete(mqttMessage.getId(), 
mqttMessage.getQos());
+            } catch (MqttException e) {
+                getExceptionHandler().handleException(
+                        "Error acknowledging failed MQTT message with ID: " + 
mqttMessage.getId(), exchange, e);
+            }
+        } else if (stopClient) {
+            reconnectAfterFailure(current, exchange, mqttMessage);
+        } else {
+            LOG.warn("Rollback due to error processing Exchange ID: {}: MQTT 
message with ID: {} stays unacknowledged"
+                     + " until the externally provided client connects again",
+                    exchange.getExchangeId(), mqttMessage.getId());
+        }
+    }
+
+    private void reconnectAfterFailure(MqttClient current, Exchange exchange, 
MqttMessage mqttMessage) {
+        String reason = "Rollback due to error processing Exchange ID: " + 
exchange.getExchangeId()
+                        + ", so that the broker delivers MQTT message with ID: 
" + mqttMessage.getId() + " again";
+        if (!scheduleReconnect(current, reason)) {
+            LOG.debug("{}: a reconnect is already scheduled", reason);
+        }
+    }
+
+    /**
+     * The delay before the reconnect of the given failed exchange in a row: 
the minimum automatic reconnect delay of
+     * the client (1 second), doubled for each further failed exchange in a 
row, up to maxReconnectDelay.
+     */
+    private long reconnectDelay(int attempt) {
+        long max = Math.max(0, 
getEndpoint().getConfiguration().getMaxReconnectDelay());
+        MqttConnectionOptions options = connectionOptions;
+        long min = options != null ? 
TimeUnit.SECONDS.toMillis(options.getAutomaticReconnectMinDelay()) : 1000;
+        long delay = min << Math.min(attempt - 1, 20);
+        return Math.min(delay, max);
+    }

Review Comment:
   ⚠️ **Edge case: `maxReconnectDelay = 0` causes reconnect storm**
   
   When `maxReconnectDelay` is 0 (the default) or negative, `Math.max(0, ...)` 
yields `0`, and `Math.min(delay, 0)` clamps every computed delay to **0 ms**. 
This means the consumer reconnects immediately in a tight loop, with no backoff 
at all — a reconnect storm.
   
   The fix should guard against `max == 0` (i.e. unconfigured) and fall back to 
an unbounded delay when the cap is not set:
   
   ```suggestion
       private long reconnectDelay(int attempt) {
           long max = getEndpoint().getConfiguration().getMaxReconnectDelay();
           MqttConnectionOptions options = connectionOptions;
           long min = options != null ? 
TimeUnit.SECONDS.toMillis(options.getAutomaticReconnectMinDelay()) : 1000;
           long delay = min << Math.min(attempt - 1, 20);
           return max > 0 ? Math.min(delay, max) : delay;
       }
   ```



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