Skip to content

Commit

Permalink
Session: Reintroduce delayed sending for disconnect notification
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dktapps committed Oct 4, 2021
1 parent a7524fb commit e3a8611
Showing 1 changed file with 33 additions and 10 deletions.
43 changes: 33 additions & 10 deletions src/server/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ class Session{

public const STATE_CONNECTING = 0;
public const STATE_CONNECTED = 1;
public const STATE_DISCONNECTING = 2;
public const STATE_DISCONNECTED = 3;
public const STATE_DISCONNECT_PENDING = 2;
public const STATE_DISCONNECT_NOTIFIED = 3;
public const STATE_DISCONNECTED = 4;

public const MIN_MTU_SIZE = 400;

Expand Down Expand Up @@ -149,7 +150,10 @@ public function isTemporal() : bool{
}

public function isConnected() : bool{
return $this->state !== self::STATE_DISCONNECTING and $this->state !== self::STATE_DISCONNECTED;
return
$this->state !== self::STATE_DISCONNECT_PENDING and
$this->state !== self::STATE_DISCONNECT_NOTIFIED and
$this->state !== self::STATE_DISCONNECTED;
}

public function update(float $time) : void{
Expand All @@ -159,12 +163,18 @@ public function update(float $time) : void{
return;
}

if($this->state === self::STATE_DISCONNECTING){
if($this->state === self::STATE_DISCONNECT_PENDING || $this->state === self::STATE_DISCONNECT_NOTIFIED){
//by this point we already told the event listener that the session is closing, so we don't need to do it again
if(!$this->sendLayer->needsUpdate() and !$this->recvLayer->needsUpdate()){
$this->state = self::STATE_DISCONNECTED;
$this->logger->debug("Client cleanly disconnected, marking session for destruction");
return;
if($this->state === self::STATE_DISCONNECT_PENDING){
$this->queueConnectedPacket(new DisconnectionNotification(), PacketReliability::RELIABLE_ORDERED, 0, true);
$this->state = self::STATE_DISCONNECT_NOTIFIED;
$this->logger->debug("All pending traffic flushed, sent disconnect notification");
}else{
$this->state = self::STATE_DISCONNECTED;
$this->logger->debug("Client cleanly disconnected, marking session for destruction");
return;
}
}elseif($this->disconnectionTime + 10 < $time){
$this->state = self::STATE_DISCONNECTED;
$this->logger->debug("Timeout during graceful disconnect, forcibly closing session");
Expand Down Expand Up @@ -238,7 +248,7 @@ private function handleEncapsulatedPacketRoute(EncapsulatedPacket $packet) : voi
}
}
}elseif($id === DisconnectionNotification::$ID){
$this->initiateDisconnect("client disconnect");
$this->onClientDisconnect();
}elseif($id === ConnectedPing::$ID){
$dataPacket = new ConnectedPing();
$dataPacket->decode(new PacketSerializer($packet->buffer));
Expand Down Expand Up @@ -290,9 +300,8 @@ public function handlePacket(Packet $packet) : void{
*/
public function initiateDisconnect(string $reason) : void{
if($this->isConnected()){
$this->state = self::STATE_DISCONNECTING;
$this->state = self::STATE_DISCONNECT_PENDING;
$this->disconnectionTime = microtime(true);
$this->queueConnectedPacket(new DisconnectionNotification(), PacketReliability::RELIABLE_ORDERED, 0, true);
$this->server->getEventListener()->onClientDisconnect($this->internalId, $reason);
$this->logger->debug("Requesting graceful disconnect because \"$reason\"");
}
Expand All @@ -307,6 +316,20 @@ public function forciblyDisconnect(string $reason) : void{
$this->logger->debug("Forcibly disconnecting session due to \"$reason\"");
}

private function onClientDisconnect() : void{
//the client will expect an ACK for this; make sure it gets sent, because after forcible termination
//there won't be any session ticks to update it
$this->recvLayer->update();

if($this->isConnected()){
//the client might have disconnected after the server sent a disconnect notification, but before the client
//received it - in this case, we don't want to notify the event handler twice
$this->server->getEventListener()->onClientDisconnect($this->internalId, "client disconnect");
}
$this->state = self::STATE_DISCONNECTED;
$this->logger->debug("Terminating session due to client disconnect");
}

/**
* Returns whether the session is ready to be destroyed (either properly cleaned up or forcibly terminated)
*/
Expand Down

0 comments on commit e3a8611

Please sign in to comment.