reactphp / http

Event-driven, streaming HTTP client and server implementation for ReactPHP.
https://reactphp.org/http/
MIT License
744 stars 143 forks source link

ServerInstance socket not closing on unexpected program termination or loop stop #526

Closed valzargaming closed 5 months ago

valzargaming commented 5 months ago

When a program using the library ends unexpectedly or the loop is stopped, the ServerInstance socket might not close the port. This results in the port remaining open until the process times out. If the program restarts suddenly and didn't properly close the socket, it will be unable to reopen the port.

To Reproduce

  1. Create a simple ReactPHP HttpServer instance.
  2. Start the server and listen on a specific port.
  3. Terminate the program unexpectedly (e.g., force kill the process).
  4. Attempt to restart the program and listen on the same port.

Expected behavior The socket should close properly, releasing the port for reuse immediately after the program terminates or the loop stops.

Actual behavior The port remains open and the HttpServer will not start, throwing an error that it couldn't listen to the port.

Example Code

<?php

use React\EventLoop\Factory;
use React\Http\Server;
use React\Socket\Server as SocketServer;

require 'vendor/autoload.php';

$loop = Factory::create();

$server = new Server(function (Psr\Http\Message\ServerRequestInterface $request) {
    return new React\Http\Message\Response(
        200,
        array('Content-Type' => 'text/plain'),
        "Hello World!\n"
    );
});

$socket = new SocketServer('127.0.0.1:8080', $loop);
$server->listen($socket);

$loop->run();

// Simulate unexpected termination (e.g., force kill the process)

This took me a while to figure out, and the best workaround to fixing this odd behavior is to explicitly call $socket->close(); in my error handlers or anywhere that might cause my scripts to restart. I think it would be a good idea to define a __destruct() magic method for this class that makes sure that the socket closes out completely, but I'm not sure if that is the best fix for this bug.

SimonFrings commented 5 months ago

Hey @valzargaming, thanks for bringing this up and taking the time to properly explain your case :+1:

I copied the example you posted above and tried to reproduce the behavior, but I didn't achieve the same results you're describing. I can easily stop/kill and restart the server and I don't see any messages saying that the port is in use.

Seems like there's more going on on your side than you initially described. Can you give us some more information on how your using this, which platform you're using, is there any webserver in place, etc.

FYI: I changed the label of this ticket from bug to question for now until we can be sure this is actually a bug

valzargaming commented 5 months ago

Thanks for taking the time to look this over @SimonFrings! It has been rather hard to reproduce and only happens from time to time (usually when a fatal error is thrown) and doesn't always happen, so I'm not always sure under what conditions the crash needs to happen before the port used to host the HttpServer isn't able to be re-opened upon restart. I am using this library alongside DiscordPHP, which is also powered by the ReactPHP libraries, and the project I'm running into this most often can be found over at https://github.com/Valgorithms/Civilizationbot/blob/4ac4d0b0d4d152f248676b734c679c95811be3fd/bot.php#L414-L437.

I have at least been able to confirm that closing the socket stops the issue from happening, but if an error happens elsewhere that throws a fatal error which doesn't trigger this error handler it will not always be able to re-open without another full restart of the script. I think the OS usually automatically closes the port when the script terminates, but there may be a delay in doing so in some cases.

valzargaming commented 5 months ago

I've finally been able to replicate the issue! It turns out that it's not related (at least not always) to the socket not closing when the script stops, but rather trying to access the HTTP server at all during the startup process will result in the port failing to bind. For instance, if my bot crashes and a browser is still trying to access the port, it will throw this fatal error:

PHP Fatal error:  Uncaught RuntimeException: Failed to listen on "tcp://0.0.0.0:55555": Address already in use (EADDRINUSE) in /home/civ13/Civilizationbot/vendor/react/socket/src/>Stack trace:
#0 /home/civ13/Civilizationbot/vendor/react/socket/src/SocketServer.php(61): React\Socket\TcpServer->__construct()
#1 /home/civ13/Civilizationbot/bot.php(369): React\Socket\SocketServer->__construct()
#2 {main}
  thrown in /home/civ13/Civilizationbot/vendor/react/socket/src/TcpServer.php on line 184

In my specific case, even with the script killed and my browser session closed, I was able to confirm that my OS was still listening on that port by running sudo lsof -i:55555. Forcefully killing this process by its PID allowed the script to properly start up again, though I think it's important to note that I already did ps aux | grep -i php prior to finding and killing this process, and whatever this process was that was using the port was not actually the PHP script (as I had already killed it) but the browser that was trying to connect to the server.

COMMAND       PID USER   FD   TYPE     DEVICE SIZE/OFF NODE NAME
sudo      1286503 root    6u  IPv4  985852326      0t0  TCP *:55555 (LISTEN)

For now, my workaround to fix this is to use the GNU Debugger with the following flow, then restart the HTTP server:

sudo lsof -i:$port
sudo gdb -p $pid
call close(6)
SimonFrings commented 5 months ago

@valzargaming thanks for giving additional input on this and sharing your results :+1:

Seems to me that this is a problem laying outside of ReactPHP and has something to do with your use case/setup. Not sure why your OS is still listening on that port once you kill the script, maybe you have something in place that is automatically restarting it (like systemd), but these are only guesses. I also found https://stackoverflow.com/questions/3855127/find-and-kill-process-locking-port-3000-on-mac which seems to describe similar cases and also contains the suggested solution you described above. Answers the "how to fix it" question, but not really the "why is this happening".

Feels like this ticket is answered for now, so I'll close this. If you find any additional input that explains this behavior, please let us know in here, so others with the same problem can find an answer to this. If you encounter this behavior again and can prove that it's caused by ReactPHP, we can reopen this ticket and take another look :+1: