Skip to content

Commit e3a8611

Browse files
committed
Session: Reintroduce delayed sending for disconnect notification
this is directed at MCPE to work around the infamous race condition bug that causes disconnects and transfers to be unreliable. It reintroduces the old mechanism for server-initiated disconnects that was used in 0.12: - First, flush all packets out to clients - Send disconnect notification - Wait for ack - Discard session On 0.13, it sent the disconnect notification immediately when disconnect was initiated, which caused the client to sometimes not handle some packets.
1 parent a7524fb commit e3a8611

File tree

1 file changed

+33
-10
lines changed

1 file changed

+33
-10
lines changed

src/server/Session.php

+33-10
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ class Session{
4545

4646
public const STATE_CONNECTING = 0;
4747
public const STATE_CONNECTED = 1;
48-
public const STATE_DISCONNECTING = 2;
49-
public const STATE_DISCONNECTED = 3;
48+
public const STATE_DISCONNECT_PENDING = 2;
49+
public const STATE_DISCONNECT_NOTIFIED = 3;
50+
public const STATE_DISCONNECTED = 4;
5051

5152
public const MIN_MTU_SIZE = 400;
5253

@@ -149,7 +150,10 @@ public function isTemporal() : bool{
149150
}
150151

151152
public function isConnected() : bool{
152-
return $this->state !== self::STATE_DISCONNECTING and $this->state !== self::STATE_DISCONNECTED;
153+
return
154+
$this->state !== self::STATE_DISCONNECT_PENDING and
155+
$this->state !== self::STATE_DISCONNECT_NOTIFIED and
156+
$this->state !== self::STATE_DISCONNECTED;
153157
}
154158

155159
public function update(float $time) : void{
@@ -159,12 +163,18 @@ public function update(float $time) : void{
159163
return;
160164
}
161165

162-
if($this->state === self::STATE_DISCONNECTING){
166+
if($this->state === self::STATE_DISCONNECT_PENDING || $this->state === self::STATE_DISCONNECT_NOTIFIED){
163167
//by this point we already told the event listener that the session is closing, so we don't need to do it again
164168
if(!$this->sendLayer->needsUpdate() and !$this->recvLayer->needsUpdate()){
165-
$this->state = self::STATE_DISCONNECTED;
166-
$this->logger->debug("Client cleanly disconnected, marking session for destruction");
167-
return;
169+
if($this->state === self::STATE_DISCONNECT_PENDING){
170+
$this->queueConnectedPacket(new DisconnectionNotification(), PacketReliability::RELIABLE_ORDERED, 0, true);
171+
$this->state = self::STATE_DISCONNECT_NOTIFIED;
172+
$this->logger->debug("All pending traffic flushed, sent disconnect notification");
173+
}else{
174+
$this->state = self::STATE_DISCONNECTED;
175+
$this->logger->debug("Client cleanly disconnected, marking session for destruction");
176+
return;
177+
}
168178
}elseif($this->disconnectionTime + 10 < $time){
169179
$this->state = self::STATE_DISCONNECTED;
170180
$this->logger->debug("Timeout during graceful disconnect, forcibly closing session");
@@ -238,7 +248,7 @@ private function handleEncapsulatedPacketRoute(EncapsulatedPacket $packet) : voi
238248
}
239249
}
240250
}elseif($id === DisconnectionNotification::$ID){
241-
$this->initiateDisconnect("client disconnect");
251+
$this->onClientDisconnect();
242252
}elseif($id === ConnectedPing::$ID){
243253
$dataPacket = new ConnectedPing();
244254
$dataPacket->decode(new PacketSerializer($packet->buffer));
@@ -290,9 +300,8 @@ public function handlePacket(Packet $packet) : void{
290300
*/
291301
public function initiateDisconnect(string $reason) : void{
292302
if($this->isConnected()){
293-
$this->state = self::STATE_DISCONNECTING;
303+
$this->state = self::STATE_DISCONNECT_PENDING;
294304
$this->disconnectionTime = microtime(true);
295-
$this->queueConnectedPacket(new DisconnectionNotification(), PacketReliability::RELIABLE_ORDERED, 0, true);
296305
$this->server->getEventListener()->onClientDisconnect($this->internalId, $reason);
297306
$this->logger->debug("Requesting graceful disconnect because \"$reason\"");
298307
}
@@ -307,6 +316,20 @@ public function forciblyDisconnect(string $reason) : void{
307316
$this->logger->debug("Forcibly disconnecting session due to \"$reason\"");
308317
}
309318

319+
private function onClientDisconnect() : void{
320+
//the client will expect an ACK for this; make sure it gets sent, because after forcible termination
321+
//there won't be any session ticks to update it
322+
$this->recvLayer->update();
323+
324+
if($this->isConnected()){
325+
//the client might have disconnected after the server sent a disconnect notification, but before the client
326+
//received it - in this case, we don't want to notify the event handler twice
327+
$this->server->getEventListener()->onClientDisconnect($this->internalId, "client disconnect");
328+
}
329+
$this->state = self::STATE_DISCONNECTED;
330+
$this->logger->debug("Terminating session due to client disconnect");
331+
}
332+
310333
/**
311334
* Returns whether the session is ready to be destroyed (either properly cleaned up or forcibly terminated)
312335
*/

0 commit comments

Comments
 (0)