Skip to content

Commit

Permalink
Merge pull request #86 from clue-labs/timeout
Browse files Browse the repository at this point in the history
Support connection timeouts
  • Loading branch information
WyriHaximus authored Oct 18, 2018
2 parents faaa6b3 + 3879082 commit f235ab8
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 1 deletion.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ database, but likely to yield an authentication error in a production system:
$factory->createConnection('localhost');
```

This method respects PHP's `default_socket_timeout` setting (default 60s)
as a timeout for establishing the connection and waiting for successful
authentication. You can explicitly pass a custom timeout value in seconds
(or use a negative number to not apply a timeout) like this:

```php
$factory->createConnection('localhost?timeout=0.5');
```

### ConnectionInterface

The `ConnectionInterface` represents a connection that is responsible for
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"evenement/evenement": "^3.0 || ^2.1 || ^1.1",
"react/event-loop": "^1.0 || ^0.5 || ^0.4",
"react/promise": "^2.7",
"react/promise-timer": "^1.5",
"react/socket": "^1.1"
},
"require-dev": {
Expand Down
30 changes: 29 additions & 1 deletion src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use React\MySQL\Io\Parser;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Promise\Timer\TimeoutException;
use React\Socket\Connector;
use React\Socket\ConnectorInterface;
use React\Socket\ConnectionInterface;
Expand Down Expand Up @@ -116,6 +117,15 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector =
* $factory->createConnection('localhost');
* ```
*
* This method respects PHP's `default_socket_timeout` setting (default 60s)
* as a timeout for establishing the connection and waiting for successful
* authentication. You can explicitly pass a custom timeout value in seconds
* (or use a negative number to not apply a timeout) like this:
*
* ```php
* $factory->createConnection('localhost?timeout=0.5');
* ```
*
* @param string $uri
* @return PromiseInterface Promise<ConnectionInterface, Exception>
*/
Expand Down Expand Up @@ -164,6 +174,24 @@ public function createConnection($uri)
$deferred->reject(new \RuntimeException('Unable to connect to database server', 0, $error));
});

return $deferred->promise();
$args = [];
if (isset($parts['query'])) {
parse_str($parts['query'], $args);
}

// use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60)
$timeout = (float) isset($args['timeout']) ? $args['timeout'] : ini_get("default_socket_timeout");
if ($timeout < 0) {
return $deferred->promise();
}

return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) {
if ($e instanceof TimeoutException) {
throw new \RuntimeException(
'Connection to database server timed out after ' . $e->getTimeout() . ' seconds'
);
}
throw $e;
});
}
}
63 changes: 63 additions & 0 deletions tests/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,51 @@ public function testConnectWillRejectWhenServerClosesConnection()
$loop->run();
}

public function testConnectWillRejectOnExplicitTimeoutDespiteValidAuth()
{
$loop = \React\EventLoop\Factory::create();
$factory = new Factory($loop);

$uri = $this->getConnectionString() . '?timeout=0';

$promise = $factory->createConnection($uri);

$promise->then(null, $this->expectCallableOnceWith(
$this->logicalAnd(
$this->isInstanceOf('Exception'),
$this->callback(function (\Exception $e) {
return $e->getMessage() === 'Connection to database server timed out after 0 seconds';
})
)
));

$loop->run();
}

public function testConnectWillRejectOnDefaultTimeoutFromIniDespiteValidAuth()
{
$loop = \React\EventLoop\Factory::create();
$factory = new Factory($loop);

$uri = $this->getConnectionString();

$old = ini_get('default_socket_timeout');
ini_set('default_socket_timeout', '0');
$promise = $factory->createConnection($uri);
ini_set('default_socket_timeout', $old);

$promise->then(null, $this->expectCallableOnceWith(
$this->logicalAnd(
$this->isInstanceOf('Exception'),
$this->callback(function (\Exception $e) {
return $e->getMessage() === 'Connection to database server timed out after 0 seconds';
})
)
));

$loop->run();
}

public function testConnectWithValidAuthWillRunUntilQuit()
{
$this->expectOutputString('connected.closed.');
Expand All @@ -115,6 +160,24 @@ public function testConnectWithValidAuthWillRunUntilQuit()
$loop->run();
}

public function testConnectWithValidAuthWillIgnoreNegativeTimeoutAndRunUntilQuit()
{
$this->expectOutputString('connected.closed.');

$loop = \React\EventLoop\Factory::create();
$factory = new Factory($loop);

$uri = $this->getConnectionString() . '?timeout=-1';
$factory->createConnection($uri)->then(function (ConnectionInterface $connection) {
echo 'connected.';
$connection->quit()->then(function () {
echo 'closed.';
});
}, 'printf')->then(null, 'printf');

$loop->run();
}

public function testConnectWithValidAuthCanPingAndThenQuit()
{
$this->expectOutputString('connected.ping.closed.');
Expand Down

0 comments on commit f235ab8

Please sign in to comment.