Skip to content

Reconnect Retransmission of Inflight PUBLISH/PUBREL Is Missing #494

@LiD0209

Description

@LiD0209

Reconnect Retransmission of Inflight PUBLISH/PUBREL Is Missing

Summary

This document shows that wolfMQTT lacks the persistent in-flight state needed to re-send unacknowledged QoS>0 PUBLISH and PUBREL packets after a CleanSession=0 reconnect. The broker can reassociate subscriptions, but it does not preserve original packet identifiers or maintain a retransmission flow for reconnect recovery.

English Standard Text

The relevant MQTT 3.1.1 rule is in Section 4.4 Message delivery retry, clause [MQTT-4.4.0-1].

Short original English excerpts:

"CleanSession set to 0"
"MUST re-send any unacknowledged PUBLISH Packets"
"and PUBREL Packets"
"using their original Packet Identifiers"

The full rule means:

  • The trigger is a client reconnecting with CleanSession=0.
  • Both the Client and Server are covered by this requirement.
  • The packets to re-send are unacknowledged PUBLISH packets with QoS > 0 and unacknowledged PUBREL packets.
  • The re-send must reuse the original Packet Identifier. Allocating a new packet id is not equivalent.

The Packet Identifier rule in Section 2.3.1 Packet Identifier is consistent with this:

"same Packet Identifier in subsequent re-sends"
"same conditions apply to a Server"

So this is a MUST-level retransmission requirement, not an optional QoS optimization.

Expected Behavior

For a persistent MQTT 3.1.1 session:

  1. A client connects with CleanSession=0.
  2. The Server sends a QoS1/QoS2 PUBLISH to that client, or sends a PUBREL during a QoS2 exchange.
  3. The packet is not completely acknowledged before the network connection is lost.
  4. The same client reconnects with the same ClientId and CleanSession=0.
  5. The Server must re-send the unacknowledged PUBLISH or PUBREL.
  6. The re-sent packet must use the original packet_id.

This requires some form of per-client inflight session state, such as:

  • outbound QoS1 PUBLISH waiting for PUBACK;
  • outbound QoS2 PUBLISH waiting for PUBREC;
  • outbound QoS2 PUBREL waiting for PUBCOMP;
  • original packet identifiers associated with those pending packets.

Code Description

1. Reconnect restores subscriptions only

File: wolfMQTT-master/src/mqtt_broker.c:1745

static void BrokerSubs_ReassociateClient(MqttBroker* broker,
    const char* client_id, BrokerClient* new_bc)
{
    int count = 0;
    if (broker == NULL || client_id == NULL || client_id[0] == '\0' ||
        new_bc == NULL) {
        return;
    }

The function searches stored subscriptions for the same ClientId and reassigns them to the new connection:

if (s->client == NULL && BROKER_STR_VALID(s->client_id) &&
    XSTRCMP(s->client_id, client_id) == 0) {
    s->client = new_bc;
    count++;
}

This handles subscription ownership after reconnect. It does not traverse an inflight message list, rebuild a pending QoS retransmission queue, or re-send saved PUBLISH / PUBREL packets.

2. CONNECT path calls only subscription reassociation

File: wolfMQTT-master/src/mqtt_broker.c:2768

if (!mc.clean_session) {
    /* Reassociate old client's subs to new client */
    BrokerSubs_ReassociateClient(broker, bc->client_id, bc);
}

File: wolfMQTT-master/src/mqtt_broker.c:2775

else if (!mc.clean_session) {
    /* No existing client, but check for orphaned subs from
     * a previous session (clean_session=0 reconnect) */
    BrokerSubs_ReassociateClient(broker, bc->client_id, bc);
}

Both reconnect branches call BrokerSubs_ReassociateClient(). There is no adjacent step that re-sends unacknowledged QoS packets or restores original packet identifiers.

3. Disconnect preserves subscriptions, not inflight packets

File: wolfMQTT-master/src/mqtt_broker.c:3497

/* Session persistence: keep subs if clean_session=0 */
if (bc->clean_session) {
    BrokerSubs_RemoveClient(broker, bc);
}
else {
    BrokerSubs_OrphanClient(broker, bc);
}
BrokerClient_Remove(broker, bc);

For clean_session=0, the disconnect path calls BrokerSubs_OrphanClient() so subscriptions remain available for a later reconnect. It does not persist any per-client unacknowledged PUBLISH / PUBREL state before removing the broker client object.

4. New outbound PUBLISH packets receive newly allocated packet ids

File: wolfMQTT-master/src/mqtt_broker.c:1628

static word16 BrokerNextPacketId(MqttBroker* broker)
{
    word16 id = broker->next_packet_id;
    broker->next_packet_id++;
    if (broker->next_packet_id == 0) {
        broker->next_packet_id = 1; /* wrap: skip 0 */
    }
    return id;
}

File: wolfMQTT-master/src/mqtt_broker.c:3278

if (topic != NULL && (payload != NULL || pub.total_len == 0)) {
    /* Fan out to matching subscribers */

File: wolfMQTT-master/src/mqtt_broker.c:3301

if (eff_qos >= MQTT_QOS_1) {
    out_pub.packet_id = BrokerNextPacketId(broker);
}

This is appropriate for a new outbound QoS message. It is not sufficient for retransmission after reconnect, because [MQTT-4.4.0-1] requires the original Packet Identifier to be reused.

5. QoS2 PUBREC/PUBREL handling is online and stateless

File: wolfMQTT-master/src/mqtt_broker.c:3401

static int BrokerHandle_PublishRec(BrokerClient* bc, int rx_len)
{
    ...
    rc = MqttDecode_PublishResp(bc->rx_buf, rx_len,
            MQTT_PACKET_TYPE_PUBLISH_REC, &resp);

File: wolfMQTT-master/src/mqtt_broker.c:3425

rc = MqttEncode_PublishResp(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc),
        MQTT_PACKET_TYPE_PUBLISH_REL, &resp);

This path receives PUBREC and immediately sends PUBREL using the decoded response packet id. It does not store an outbound PUBREL as an inflight record to be resent after a later reconnect.

Similarly, incoming PUBREL is handled by immediately sending PUBCOMP:

File: wolfMQTT-master/src/mqtt_broker.c:3374

WBLOG_DBG(bc->broker, "broker: PUBLISH_REL recv sock=%d len=%d", (int)bc->sock, rx_len);

File: wolfMQTT-master/src/mqtt_broker.c:3389

rc = MqttEncode_PublishResp(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc),
        MQTT_PACKET_TYPE_PUBLISH_COMP, &resp);

These online QoS handlers do not create persistent QoS2 session state for reconnect retransmission.

6. Broker data model has no per-client inflight queue

File: wolfMQTT-master/wolfmqtt/mqtt_broker.h:224

BROKER_SOCKET_T sock;
byte    protocol_level;
word16  keep_alive_sec;
WOLFMQTT_BROKER_TIME_T last_rx;
byte    clean_session;
byte    connected;       /* set after successful CONNECT handshake */

File: wolfMQTT-master/wolfmqtt/mqtt_broker.h:251

typedef struct BrokerSub {
    ...
    char*   client_id; /* For session persistence */
    struct BrokerSub* next;
    ...
    struct BrokerClient* client; /* NULL if client disconnected (session persisted) */
    MqttQoS qos;
} BrokerSub;

File: wolfMQTT-master/wolfmqtt/mqtt_broker.h:311

typedef struct MqttBroker {
    BROKER_SOCKET_T listen_sock;
    word16  port;
    int     running;
    byte    log_level;
    MqttBrokerNet net;
    word16  next_packet_id;

The visible broker model tracks clients, subscriptions, retained messages, pending wills, and a global next_packet_id. It does not define a per-client inflight table containing saved outbound PUBLISH / PUBREL packets and their original packet ids.

Inconsistency Reason

The standard requires persistent-session retransmission state. wolfMQTT's broker implementation keeps only enough session state to restore subscriptions. That partially supports persistent subscriptions, but it does not preserve the QoS handshake state needed by [MQTT-4.4.0-1].

The key mismatch is:

  • Standard requirement: on CleanSession=0 reconnect, re-send unacknowledged PUBLISH(QoS>0) and PUBREL using original packet identifiers.
  • Implementation behavior: on reconnect, reattach orphaned subscriptions; new outbound QoS messages use BrokerNextPacketId(); online QoS2 response handling is not persisted.

Therefore, the implementation cannot guarantee original-packet-id retransmission after reconnect.

Conclusion

The reconnect retransmission issue is related to the broader session persistence gap, but it is more specific than "offline QoS messages are not stored". It should be tracked as a separate sub-issue:

Reconnect retransmission semantics missing:
no per-client inflight queue and no original-packet-id re-send flow for
unacknowledged PUBLISH/PUBREL after CleanSession=0 reconnect.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions