diff --git a/README.md b/README.md index 1c7ace07..f78deb8f 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ and [`Stream`](https://github.com/reactphp/stream) components. * [connection event](#connection-event) * [error event](#error-event) * [getAddress()](#getaddress) + * [pause()](#pause) + * [resume()](#resume) * [close()](#close) * [Server](#server) * [SecureServer](#secureserver) @@ -134,6 +136,61 @@ $port = parse_url('tcp://' . $address, PHP_URL_PORT); echo 'Server listening on port ' . $port . PHP_EOL; ``` +#### pause() + +The `pause(): void` method can be used to +pause accepting new incoming connections. + +Removes the socket resource from the EventLoop and thus stop accepting +new connections. Note that the listening socket stays active and is not +closed. + +This means that new incoming connections will stay pending in the +operating system backlog until its configurable backlog is filled. +Once the backlog is filled, the operating system may reject further +incoming connections until the backlog is drained again by resuming +to accept new connections. + +Once the server is paused, no futher `connection` events SHOULD +be emitted. + +```php +$server->pause(); + +$server->on('connection', assertShouldNeverCalled()); +``` + +This method is advisory-only, though generally not recommended, the +server MAY continue emitting `connection` events. + +Unless otherwise noted, a successfully opened server SHOULD NOT start +in paused state. + +You can continue processing events by calling `resume()` again. + +Note that both methods can be called any number of times, in particular +calling `pause()` more than once SHOULD NOT have any effect. +Similarly, calling this after `close()` is a NO-OP. + +#### resume() + +The `resume(): void` method can be used to +resume accepting new incoming connections. + +Re-attach the socket resource to the EventLoop after a previous `pause()`. + +```php +$server->pause(); + +$loop->addTimer(1.0, function () use ($server) { + $server->resume(); +}); +``` + +Note that both methods can be called any number of times, in particular +calling `resume()` without a prior `pause()` SHOULD NOT have any effect. +Similarly, calling this after `close()` is a NO-OP. + #### close() The `close(): void` method can be used to diff --git a/src/SecureServer.php b/src/SecureServer.php index abfb9032..c117985c 100644 --- a/src/SecureServer.php +++ b/src/SecureServer.php @@ -140,6 +140,16 @@ public function getAddress() return $this->tcp->getAddress(); } + public function pause() + { + $this->tcp->pause(); + } + + public function resume() + { + $this->tcp->resume(); + } + public function close() { return $this->tcp->close(); diff --git a/src/Server.php b/src/Server.php index db452eac..419727ea 100644 --- a/src/Server.php +++ b/src/Server.php @@ -39,6 +39,7 @@ final class Server extends EventEmitter implements ServerInterface { private $master; private $loop; + private $listening = false; /** * Creates a plaintext TCP/IP socket server and starts listening on the given address @@ -168,17 +169,7 @@ public function __construct($uri, LoopInterface $loop, array $context = array()) } stream_set_blocking($this->master, 0); - $that = $this; - - $this->loop->addReadStream($this->master, function ($master) use ($that) { - $newSocket = @stream_socket_accept($master); - if (false === $newSocket) { - $that->emit('error', array(new \RuntimeException('Error accepting new connection'))); - - return; - } - $that->handleConnection($newSocket); - }); + $this->resume(); } public function getAddress() @@ -199,13 +190,42 @@ public function getAddress() return $address; } + public function pause() + { + if (!$this->listening) { + return; + } + + $this->loop->removeReadStream($this->master); + $this->listening = false; + } + + public function resume() + { + if ($this->listening || !is_resource($this->master)) { + return; + } + + $that = $this; + $this->loop->addReadStream($this->master, function ($master) use ($that) { + $newSocket = @stream_socket_accept($master); + if (false === $newSocket) { + $that->emit('error', array(new \RuntimeException('Error accepting new connection'))); + + return; + } + $that->handleConnection($newSocket); + }); + $this->listening = true; + } + public function close() { if (!is_resource($this->master)) { return; } - $this->loop->removeStream($this->master); + $this->pause(); fclose($this->master); $this->removeAllListeners(); } diff --git a/src/ServerInterface.php b/src/ServerInterface.php index 2d5ab829..4d7d0beb 100644 --- a/src/ServerInterface.php +++ b/src/ServerInterface.php @@ -72,6 +72,67 @@ interface ServerInterface extends EventEmitterInterface */ public function getAddress(); + /** + * Pauses accepting new incoming connections. + * + * Removes the socket resource from the EventLoop and thus stop accepting + * new connections. Note that the listening socket stays active and is not + * closed. + * + * This means that new incoming connections will stay pending in the + * operating system backlog until its configurable backlog is filled. + * Once the backlog is filled, the operating system may reject further + * incoming connections until the backlog is drained again by resuming + * to accept new connections. + * + * Once the server is paused, no futher `connection` events SHOULD + * be emitted. + * + * ```php + * $server->pause(); + * + * $server->on('connection', assertShouldNeverCalled()); + * ``` + * + * This method is advisory-only, though generally not recommended, the + * server MAY continue emitting `connection` events. + * + * Unless otherwise noted, a successfully opened server SHOULD NOT start + * in paused state. + * + * You can continue processing events by calling `resume()` again. + * + * Note that both methods can be called any number of times, in particular + * calling `pause()` more than once SHOULD NOT have any effect. + * Similarly, calling this after `close()` is a NO-OP. + * + * @see self::resume() + * @return void + */ + public function pause(); + + /** + * Resumes accepting new incoming connections. + * + * Re-attach the socket resource to the EventLoop after a previous `pause()`. + * + * ```php + * $server->pause(); + * + * $loop->addTimer(1.0, function () use ($server) { + * $server->resume(); + * }); + * ``` + * + * Note that both methods can be called any number of times, in particular + * calling `resume()` without a prior `pause()` SHOULD NOT have any effect. + * Similarly, calling this after `close()` is a NO-OP. + * + * @see self::pause() + * @return void + */ + public function resume(); + /** * Shuts down this listening socket * diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index bc387486..c6654523 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -25,6 +25,39 @@ public function testEmitsConnectionForNewConnection() Block\sleep(0.1, $loop); } + public function testEmitsNoConnectionForNewConnectionWhenPaused() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + $server->on('connection', $this->expectCallableNever()); + $server->pause(); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsConnectionForNewConnectionWhenResumedAfterPause() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + $server->on('connection', $this->expectCallableOnce()); + $server->pause(); + $server->resume(); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + public function testEmitsConnectionWithRemoteIp() { $loop = Factory::create(); diff --git a/tests/SecureServerTest.php b/tests/SecureServerTest.php index 02e926b8..7c717e2e 100644 --- a/tests/SecureServerTest.php +++ b/tests/SecureServerTest.php @@ -26,6 +26,30 @@ public function testGetAddressWillBePassedThroughToTcpServer() $this->assertEquals('127.0.0.1:1234', $server->getAddress()); } + public function testPauseWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('pause'); + + $loop = $this->getMock('React\EventLoop\LoopInterface'); + + $server = new SecureServer($tcp, $loop, array()); + + $server->pause(); + } + + public function testResumeWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('resume'); + + $loop = $this->getMock('React\EventLoop\LoopInterface'); + + $server = new SecureServer($tcp, $loop, array()); + + $server->resume(); + } + public function testCloseWillBePassedThroughToTcpServer() { $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index be9d34a5..98694401 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -232,6 +232,51 @@ public function testConnectionDoesEndWhenClientCloses() $this->loop->tick(); } + public function testCtorAddsResourceToLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addReadStream'); + + $server = new Server(0, $loop); + } + + public function testResumeWithoutPauseIsNoOp() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addReadStream'); + + $server = new Server(0, $loop); + $server->resume(); + } + + public function testPauseRemovesResourceFromLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('removeReadStream'); + + $server = new Server(0, $loop); + $server->pause(); + } + + public function testPauseAfterPauseIsNoOp() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('removeReadStream'); + + $server = new Server(0, $loop); + $server->pause(); + $server->pause(); + } + + public function testCloseRemovesResourceFromLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('removeReadStream'); + + $server = new Server(0, $loop); + $server->close(); + } + /** * @expectedException RuntimeException */