Skip to content

fix: guarantee QoS 1 buffered messages deliver before live messages on reconnect - #4

Open
amitgujarpatil wants to merge 1 commit into
Intangles:mainfrom
amitgujarpatil:fix/qos1-ordered-delivery-on-reconnect
Open

amitgujarpatil wants to merge 1 commit into
Intangles:mainfrom
amitgujarpatil:fix/qos1-ordered-delivery-on-reconnect

Conversation

@amitgujarpatil

Copy link
Copy Markdown

Summary

  • Fixes a race condition where live QoS 1 messages could arrive at a reconnecting client before buffered (offline) messages stored in persistence
  • Adds aedes-persistence-redis v11.2.2 as a dependency, which now supports Redis Cluster via opts.cluster (ioredis)

Root Cause

The connectActions pipeline runs in this order:

authenticate → fetchSubs → restoreSubs → registerClient → doConnack → emptyQueue

restoreSubs registers mqemitter listeners — live messages start flowing immediately. emptyQueue drains the persistence store only after that, so buffered messages arrived after live ones.

Fix

Three changes to lib/client.js and lib/handlers/connect.js:

  1. client._drainingQueue flag (constructor) — false by default
  2. deliverQoS gate — when _drainingQueue is true, incoming live QoS packets are pushed to client._liveQueue instead of written immediately
  3. emptyQueue — before/after drain:
    • Sets _drainingQueue = true before streaming from persistence
    • afterDrain callback clears the flag and flushes _liveQueue in order
  4. client.close() — drains _liveQueue callbacks so mqemitter never stalls if the client closes mid-drain

Message flow after fix:

restoreSubs → live message arrives → deliverQoS → _drainingQueue=true → pushed to _liveQueue (held)
emptyQueue drains persistence → writes buffered packets to client  ← buffered first ✓
afterDrain → _drainingQueue=false → flushes _liveQueue             ← live after ✓

aedes-persistence-redis

Previous comment in the server blocked this with "does not support connecting to the clustered redis". v11.2.2 resolves this — it accepts opts.cluster and internally uses ioredis Cluster mode.

Test plan

  • Subscriber connects with cleanSession=false, QoS 1 subscription
  • Subscriber disconnects; server publishes N messages to its topic
  • Subscriber reconnects — verify all N buffered messages arrive before any live messages
  • Subscriber disconnects mid-drain — verify no mqemitter stall / process hang
  • clean=true session — verify unaffected (_drainingQueue is set and cleared with empty stream)

🤖 Generated with Claude Code

…n reconnect

When a client reconnects with cleanSession=false, restoreSubs wires up
mqemitter listeners before emptyQueue drains the persistence store. This
allowed live messages to reach the client before buffered ones, breaking
delivery order for QoS 1 sessions.

Fix: set client._drainingQueue=true at the start of emptyQueue so that
deliverQoS pushes incoming live packets into client._liveQueue instead of
writing them immediately. Once the persistence drain finishes, _drainingQueue
is cleared and _liveQueue is flushed in order. client.close() drains any
held callbacks so mqemitter never stalls.

Also adds aedes-persistence-redis v11.2.2 as a dependency. This version
supports Redis Cluster via opts.cluster (ioredis), resolving the prior
blocker that prevented persistent sessions in clustered deployments.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant