-
Notifications
You must be signed in to change notification settings - Fork 10.1k
Connection State Recovery fails for long-lived connections #5282
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
Comments
I'm having the same problem, is there any solution? |
Periodically (maxDisconnectionDuration/2) send server->client garbage packet? |
Maybe, but it would increase the load on the server |
That seems familiar - IIRC, connection state recovery requires Your cursor is highlighting where the server periodically discards packets older than |
I've been trying to solve the issue, but the problem is that the packets are being saved without any relationship to who sent them. How can we know which packet should stay? |
That loop goes through each connection, one at a time. It also gives you the socket ID. |
I think you could always keep the last packet in Alternatively, you could save off the last packet from (Also, I thought I should mention I don't know JavaScript, you have now reached further chasing this down than I got). |
The problem is also that right now each packet that is sent is stored in this. packets, and at some point we have to know when to delete them and besides that each packet is sent with a different ID within the array to know where it was left and send the packets that did not reach the client, we must also take into account that the sessions are only created once the client is disconnected, so if for example we make each packet has the id of the session that belongs and delete them only from the array when the client disconnects, when storing the packets, would not be a problem to store everything sent by the server and only delete it when the client disconnects? For example if in the server we have that to a client has been sent 1000 messages all that would be stored until it disconnected. |
I think restoring the data of a client would be easy but sending the packets that haven't been sent is the issue |
Maybe this method should return the session if the index is -1 and the function to send the lost packets should be a separate thing. override restoreSession(
pid: PrivateSessionId,
offset: string,
): Promise<Session> {
const session = this.sessions.get(pid);
if (!session) {
// the session may have expired
return null;
}
const hasExpired =
session.disconnectedAt + this.maxDisconnectionDuration < Date.now();
if (hasExpired) {
// the session has expired
this.sessions.delete(pid);
return null;
}
const index = this.packets.findIndex((packet) => packet.id === offset);
if (index === -1) {
// the offset may be too old
return null;
}
const missedPackets = [];
for (let i = index + 1; i < this.packets.length; i++) {
const packet = this.packets[i];
if (shouldIncludePacket(session.rooms, packet.opts)) {
missedPackets.push(packet.data);
}
}
return Promise.resolve({
...session,
missedPackets,
});
} |
Perhaps splitting the thresholds and having something like this would be a good solution export class SessionAwareAdapter extends Adapter {
private readonly maxDisconnectionDuration: number;
private readonly maxMissingPacketExpiration: number;
private sessions: Map<PrivateSessionId, SessionWithTimestamp> = new Map();
private packets: PersistedPacket[] = [];
constructor(readonly nsp: any) {
super(nsp);
this.maxDisconnectionDuration =
nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration;
this.maxMissingPacketExpiration = nsp.server.opts.connectionStateRecovery.maxMissingPacketExpiration
const timer = setInterval(() => {
const maxDisconnectionDurationThreshold = Date.now() - this.maxDisconnectionDuration;
const maxMissingPacketExpirationThreshold = Date.now() - this.maxMissingPacketExpiration;
this.sessions.forEach((session, sessionId) => {
const hasExpired = session.disconnectedAt < maxDisconnectionDurationThreshold;
if (hasExpired) {
this.sessions.delete(sessionId);
}
});
for (let i = this.packets.length - 1; i >= 0; i--) {
const hasExpired = this.packets[i].emittedAt < maxMissingPacketExpirationThreshold;
if (hasExpired) {
this.packets.splice(0, i + 1);
break;
}
}
}, 60 * 1000);
// prevents the timer from keeping the process alive
timer.unref();
}
override persistSession(session: SessionToPersist) {
(session as SessionWithTimestamp).disconnectedAt = Date.now();
this.sessions.set(session.pid, session as SessionWithTimestamp);
}
override restoreSession(
pid: PrivateSessionId,
offset: string,
): Promise<Session> {
const session = this.sessions.get(pid);
if (!session) {
// the session may have expired
return null;
}
const hasExpired =
session.disconnectedAt + this.maxDisconnectionDuration < Date.now();
if (hasExpired) {
// the session has expired
this.sessions.delete(pid);
return null;
}
const index = this.packets.findIndex((packet) => packet.id === offset);
if (index === -1) {
// the offset may be too old
return Promise.resolve({
...session,
missedPackets: [],
})
}
const missedPackets = [];
for (let i = index + 1; i < this.packets.length; i++) {
const packet = this.packets[i];
if (shouldIncludePacket(session.rooms, packet.opts)) {
missedPackets.push(packet.data);
}
}
return Promise.resolve({
...session,
missedPackets,
});
}
override broadcast(packet: any, opts: BroadcastOptions) {
const isEventPacket = packet.type === 2;
// packets with acknowledgement are not stored because the acknowledgement function cannot be serialized and
// restored on another server upon reconnection
const withoutAcknowledgement = packet.id === undefined;
const notVolatile = opts.flags?.volatile === undefined;
if (isEventPacket && withoutAcknowledgement && notVolatile) {
const id = yeast();
// the offset is stored at the end of the data array, so the client knows the ID of the last packet it has
// processed (and the format is backward-compatible)
packet.data.push(id);
this.packets.push({
id,
opts,
data: packet.data,
emittedAt: Date.now(),
});
}
super.broadcast(packet, opts);
}
} |
Ohhh, I did not understand that the loop that trims stale packets runs for all packets - I missed that the for-each-session-id doesn't cover that. That makes things much more complicated. |
For the moment I think the best thing to do is to create a package to solve this and when there is an answer see what to do next, wdyt? |
Sorry, I don't understand Socket.io well enough to offer a useful opinion |
Describe the bug
If the server does not periodically send the client messages, connection state recovery may fail unexpectedly.
To Reproduce
Socket.IO server version:
4.7.5
, in-memory adapterServer
We noticed this using a bespoke client, but appears to be a server-side issue. I've tried to mock up the client-side code accordingly.
Client
Steps:
Expected behavior
Connection State Recovery should succeed within maxDisconnectionDuration of disconnection, as long as the server must send at least one event, in order to initialize the offset on the client side.
It might help if server were to store offset of last queued packet in a separate variable, which could be checked even if restoreSession() cannot find the offset in the queue.
If server no longer relies upon pending message queue, we could then use Engine.IO PING/PONG events to trim queue(Once PONG is received, all events sent before the PING that triggered PONG are known to have arrived at the client).
Platform:
The text was updated successfully, but these errors were encountered: