Repository navigation
Fix stream desync when a non-blocking read hits a partial message ("Invalid offset" disconnects) - #43
Merged
VirxEC merged 2 commits intoOct 2, 2026
Conversation
handle_incoming_messages(blocking=False) treated a BlockingIOError raised part-way through read_message() as "no messages", discarding the header and payload bytes already consumed. The next read then parsed bytes from the middle of a message as a length header, which surfaces as "Error while unpacking message (0 bytes) ... Invalid offset" and terminates the bot/hivemind/script. Only the first recv of a message is now non-blocking; once any byte has been consumed the rest of the message is read blocking.
Collaborator
|
Oh I just KNOW this is a Windows-only issue lmao |
Collaborator
|
Anyways could you please bump the verison in |
Contributor
Author
|
bumping now. |
Contributor
Author
|
Yeah, I tried to see if anyone else had the issue but aparrently im the only windows user LMAO |
VirxEC
approved these changes
Oct 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What happened
I've been building a bot with RLGym v2 (a PPO policy trained in RocketSim, run in RLBot through
rlgym_compat). I play it in RLBot v5 through the GUI on Windows 11, Python 3.12,rlbot2.0.0b55,rlbot_flatbuffers0.19.0.When I started a 2v2 (two copies of my bot vs two Nextos), my bots kept disconnecting about a second after spawning. Nexto was fine every time. It was random: sometimes one of my bots dropped, sometimes both, and they'd just sit there for the rest of the match. This is what the log looked like:
Sometimes the server also logged
Session N's outbound message queue is full (… messages missed)right before closing the session.The weird part was that the same bot worked fine on other people's machines, so at first I assumed it was my setup. I updated
rlbotandrlbot_flatbuffersto the latest versions, did a full uninstall and reinstall of RLBot v5 (which pulledRLBotServer.exev5.0.0-rc17 andrlbotgui.exebeta23), and turned off my antivirus. None of it helped. The problem turned out to be inSocketRelayitself.What's going on
The bot manager reads from the socket like this (
bot.py#L167-L185;hivemind.pyandscript.pydo the same):So whenever the bot has a packet waiting to be processed, which means it's falling behind, the next read is non-blocking. In
interface.py:read_message()reads a 2-byte length and then_read_exact(size). Theexcept BlockingIOErrorassumes the error means "nothing to read", but it can also be raised partway through a message:recv_intocalls in_read_exactsucceed too, then the next one raisesBlockingIOError.handle_incoming_messagesreturnsNO_INCOMING_MSGS, and the bytes it already read are thrown away.0._read_exact(0)returnsb"",CorePacket.unpack(b"")raisesInvalidFlatbuffer, and the relay returnsTERMINATED. That's theError while unpacking message (0 bytes) ... Invalid offsetabove. A non-zero garbage length just pushes the reader further out of step until it hits one of those.The same thing happens if only one of the two header bytes has arrived: the stream ends up shifted by one byte. In my test that case doesn't even log an error. Messages just stop arriving.
It's not the server sending empty messages.
SpecStreamWriter.Writerefuses to send an empty payload, so a 0-byte message can only come from the client losing its place in the stream.Why it mostly hits at spawn, and only on some machines
The non-blocking read only happens while the bot is behind. At spawn:
GamePackets start coming in at 240 Hz.wants_ball_predictions=True(the default), eachBallPredictionwith 720 slices is about 37.5 KB, compared with about 1.1 KB for a 2v2GamePacket. Big messages are much more likely to be only partly there when you read.get_outputcalls are usually the slowest (model warm-up and so on), and Rocket League plus every bot process are all competing for the CPU.My bot does real work on every packet, so on my machine it fell behind right at spawn. On faster machines it keeps up and never takes the non-blocking path, which explains why it worked for other people.
The fix
read_message()now does only its firstrecvnon-blocking. Once any byte of a message has arrived, it reads the rest of that message blocking, so it never throws a half-read message away:recv(2)raisesBlockingIOErrorbefore reading anything, so you still getNO_INCOMING_MSGSand nothing is lost.handle_incoming_messagesalready callssetblocking(blocking)at the start of every call, so the caller's mode is back on the next call. On localhost the wait is tiny, and it beats dropping the connection.recvraisesEOFError, which ends up as"SocketRelay disconnected unexpectedly!"/TERMINATEDlike before._read_intwas only used byread_message, so I removed it.Since
Bot,HivemindandScriptall go through this path, they all get the fix. No public API changes.This fixed it for me. The same 2v2 that broke on almost every launch now plays normally, with all four bots.
Reproducing it
To show it deterministically, I used a
socket.socketpair()to feed aSocketRelayrealCorePacketframes in pieces, with a delay in between, while callinghandle_incoming_messages(blocking=False)the way the managers do.repro script
On
master(cc246ed):On this branch:
On
master, the split-payload case gives the exact sameInvalid offsetcrash I was getting in real matches. The split-header case is the silent version: the stream is off by one byte and neither message ever gets delivered.Happy to turn the repro into a proper test if you'd like one in the repo.