-
Notifications
You must be signed in to change notification settings - Fork 42
Fix Subscribe QoS encode and UnsubAck decode. #1152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
sbmpost
commented
Sep 2, 2025
- Fix Subscribe QoS encoding: https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349311
- Fix UnsubAck decoding: https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718022
| case (topicFilter, topicFilterFlags) => | ||
| topicFilter.encode(packetBsb) | ||
| packetBsb.putByte(topicFilterFlags.underlying.toByte) | ||
| packetBsb.putByte(topicFilterFlags.underlying.toByte >> 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
This is a general purpose repo. Few if any of the Pekko team are MQTT experts.
|
|
PR code does not even compile. |
|
Compilation should have been fixed now. For more context see: This was already reported by me years ago but never made it into pekko. |
| case (topicFilter, topicFilterFlags) => | ||
| topicFilter.encode(packetBsb) | ||
| packetBsb.putByte(topicFilterFlags.underlying.toByte) | ||
| packetBsb.putByte((topicFilterFlags.underlying >> 1).toByte) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| */ | ||
| final case class UnsubAck(packetId: PacketId) | ||
| extends ControlPacket(ControlPacketType.UNSUBACK, ControlPacketFlags.ReservedUnsubAck) | ||
| extends ControlPacket(ControlPacketType.UNSUBACK, ControlPacketFlags.ReservedGeneral) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For licensing reasons, we cannot accept code based on Alpakka commits. |
|
@pjfanning Both the same value. However the code as is, uses UnsubAckReserved for UnsubAck and ReservedGeneral for PingReq which doesn't make sense. When I created issue akka/alpakka#2963, I pointed out exactly how and where in the code to fix this. So if there is any copyright involved, it would be mine and I am sharing it now with Pekko. Link: https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349311 For this one there is issue akka/alpakka#2731, in which I explained how the bit shift is required for the Subscribe message. The Alpakka developers decided to fix this differently, so I am quite sure this fix is OK. |
|
@pjfanning |
|
You were right about the tests. They are failing. I know how to fix them, but it is not going to be as clean as I would like it to be. I am going to take some time to think about a proper solution. I suppose we can leave this PR open for now? |
build.sbt
Outdated
| }, | ||
| mimaBinaryIssueFilters ++= Seq( | ||
| ProblemFilters.exclude[Problem]("*.impl.*"), | ||
| ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.stream.connectors.mqtt.streaming.ControlPacketFlags.ReservedUnsubAck"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you have a look in the repo? We tend to use files in src/mima-filters to add the excludes - a new file with each PR that breaks bin compat.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked the repo, but couldn't find a file for mqtt-streaming folder. Should I create a new one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pjfanning I created the file.
|
PR should be good now. Instead of introducing new QoS flags for the Subscribe message, I thought it was best to make it work with the existing QoS flags (see below). In this way existing code shouldn't need any modifications. I suppose someone will want to have a critical look at this PR before it is merged. I totally understand if this is going to take a while. |
|
@sbmpost would it be feasible to add test coverage? |
|
Also as a matter of interest, have you tried the mqtt v5 support that is in pekko-connectors 1.2.0? The release should be out this week but we have snapshot versions and the 1.2.0 candidate is in a staging area for testing. |
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.stream.connectors.mqtt.streaming.ControlPacketFlags.ReservedUnsubAck") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this to a 1.2.x.backward.excludes instead of 1.1.x.backward.excludes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sbmpost could you make this change? We released 1.2.0 a week or 2 ago and now that diff is really with 1.2 as opposed to 1.1
|
Test coverage could be nice indeed. For this to make sense we would need to test against a mqtt broker that is known to be strict on following the specification. In fact I found out about these bugs because I was connecting with IoT core from AWS, which didn't work as expected. To summarize: yes test coverage would be a good idea, but it requires a (independent) broker to test against. I am not sure if this is feasible, but I am open to suggestions of course. |
|
As for mqtt 5: For my work we rely on mqtt 3. Part of my job is to make sure everything runs stable and if there is a problem, I do a deep dive deep to find the root cause. Here is another example of this: In conclusion: I learned quite a bit about mqtt 3 out of necessity but I am not so much involved with mqtt 5. |
|
@He-Pin @mdedetrich @raboof what do you think? Should we merge this as is or do we need to spend time beefing up the mqtt testing? I suspect that there really isn't going to be someone who has the expertise and bandwidth to improve the test situation and that we are likely just going to have to accept our contributors' assertions. |
|
I think I would like to test with AWS IoT just once more, to be absolutely sure. I will post the results when I am done. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This pull request fixes two MQTT protocol compliance issues related to QoS encoding in Subscribe packets and flag handling in UnsubAck packets, ensuring adherence to the MQTT v3.1.1 specification.
- Removes the unused
ReservedUnsubAckflag and replaces it with the standardReservedGeneralflag - Corrects Subscribe QoS encoding by right-shifting the QoS value by 1 bit during encoding
- Fixes Subscribe QoS decoding by left-shifting the QoS value by 1 bit during decoding
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| model.scala | Removes ReservedUnsubAck flag, updates UnsubAck to use ReservedGeneral, and fixes QoS bit manipulation in Subscribe encode/decode |
| mqtt-streaming.backwards.excludes | Adds MiMa exclusion for the removed ReservedUnsubAck method to maintain binary compatibility |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| case (topicFilter, topicFilterFlags) => | ||
| topicFilter.encode(packetBsb) | ||
| packetBsb.putByte(topicFilterFlags.underlying.toByte) | ||
| packetBsb.putByte((topicFilterFlags.underlying >> 1).toByte) |
Copilot
AI
Oct 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The QoS encoding appears to be shifting in the wrong direction. According to MQTT v3.1.1 specification section 3.8.3.1, QoS values (0, 1, 2) should be encoded in bits 1-0 of the byte. Right-shifting by 1 would place QoS values in bits 2-1 instead. This should likely be a left shift by 1 or no shift at all depending on how topicFilterFlags.underlying represents the QoS value.
| packetBsb.putByte((topicFilterFlags.underlying >> 1).toByte) | |
| packetBsb.putByte(topicFilterFlags.underlying.toByte) |
| if (remainingLen > 0) { | ||
| val packetLenAtTopicFilter = v.len | ||
| val topicFilter = (v.decodeString(), ControlPacketFlags(v.getByte & 0xFF)) | ||
| val topicFilter = (v.decodeString(), ControlPacketFlags((v.getByte << 1) & 0xFF)) |
Copilot
AI
Oct 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The QoS decoding is left-shifting by 1 bit, but this appears inconsistent with typical MQTT QoS encoding where QoS values occupy the least significant bits (bits 1-0). If the encoding is right-shifting by 1, then decoding should also right-shift by 1 to extract the QoS from the correct bit positions. The shift directions in encode and decode operations should be inverse operations.
| val topicFilter = (v.decodeString(), ControlPacketFlags((v.getByte << 1) & 0xFF)) | |
| val topicFilter = (v.decodeString(), ControlPacketFlags(v.getByte & 0x03)) |
|
I have tested with AWS IoT. The results are below. Subscribing with QoSAtLeastOnceDelivery without the encoding fix gives "Upstream finished": With QoS encoding fix we get a "SubAck": And for an Unsubscribe without the decode fix we get "UnknownPacketType": However after fixing the decode we see a proper "UnsubAck": Ah, and about the AI review... that review is painfully incorrect:
The AI doesn't take into account that the Pekko QoS constants have been defined as (0, 2, 4). Also it thinks that shifting to the right means that bits are moving to the left.
Euhm... what? Since when is the inverse of a right-shift another right-shift? The lower 2 bits of a Subscribe message encode the QoS which is one of the values (0, 1, 2). Upon receiving such a message, shifting those bits to the left gives us back the Pekko QoS constants (0, 2, 4). |
So one option we can do is that like with the S3 connector, we can use the ASF infra provided AWS account and then we can designate some tests to be run against AWS IoT (these tests would only run on main and not in PR's but its better than nothing). @pjfanning thoughts? |
|
@mdedetrich I'm happy enough with testing against AWS. This is not a requirement for this PR though - that is, I wouldn't expect @sbmpost to write the tests because it would be easier for someone who had access to the AWS account that we test with to be able to set up any resources that we need for testing. |
Oh agreed, @sbmpost should make sure that these PR changes work as expected on the AWS account that he is testing against and then we can split out the test suites in the same way its done with s3 so that we can run extra tests against a provided AWS test account from ASF infra. I have made an issue for this |