Skip to content

Commit f235ab8

Browse files
authored
Merge pull request #86 from clue-labs/timeout
Support connection timeouts
2 parents faaa6b3 + 3879082 commit f235ab8

File tree

4 files changed

+102
-1
lines changed

4 files changed

+102
-1
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ database, but likely to yield an authentication error in a production system:
145145
$factory->createConnection('localhost');
146146
```
147147

148+
This method respects PHP's `default_socket_timeout` setting (default 60s)
149+
as a timeout for establishing the connection and waiting for successful
150+
authentication. You can explicitly pass a custom timeout value in seconds
151+
(or use a negative number to not apply a timeout) like this:
152+
153+
```php
154+
$factory->createConnection('localhost?timeout=0.5');
155+
```
156+
148157
### ConnectionInterface
149158

150159
The `ConnectionInterface` represents a connection that is responsible for

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"evenement/evenement": "^3.0 || ^2.1 || ^1.1",
99
"react/event-loop": "^1.0 || ^0.5 || ^0.4",
1010
"react/promise": "^2.7",
11+
"react/promise-timer": "^1.5",
1112
"react/socket": "^1.1"
1213
},
1314
"require-dev": {

src/Factory.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use React\MySQL\Io\Parser;
1010
use React\Promise\Deferred;
1111
use React\Promise\PromiseInterface;
12+
use React\Promise\Timer\TimeoutException;
1213
use React\Socket\Connector;
1314
use React\Socket\ConnectorInterface;
1415
use React\Socket\ConnectionInterface;
@@ -116,6 +117,15 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector =
116117
* $factory->createConnection('localhost');
117118
* ```
118119
*
120+
* This method respects PHP's `default_socket_timeout` setting (default 60s)
121+
* as a timeout for establishing the connection and waiting for successful
122+
* authentication. You can explicitly pass a custom timeout value in seconds
123+
* (or use a negative number to not apply a timeout) like this:
124+
*
125+
* ```php
126+
* $factory->createConnection('localhost?timeout=0.5');
127+
* ```
128+
*
119129
* @param string $uri
120130
* @return PromiseInterface Promise<ConnectionInterface, Exception>
121131
*/
@@ -164,6 +174,24 @@ public function createConnection($uri)
164174
$deferred->reject(new \RuntimeException('Unable to connect to database server', 0, $error));
165175
});
166176

167-
return $deferred->promise();
177+
$args = [];
178+
if (isset($parts['query'])) {
179+
parse_str($parts['query'], $args);
180+
}
181+
182+
// use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60)
183+
$timeout = (float) isset($args['timeout']) ? $args['timeout'] : ini_get("default_socket_timeout");
184+
if ($timeout < 0) {
185+
return $deferred->promise();
186+
}
187+
188+
return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) {
189+
if ($e instanceof TimeoutException) {
190+
throw new \RuntimeException(
191+
'Connection to database server timed out after ' . $e->getTimeout() . ' seconds'
192+
);
193+
}
194+
throw $e;
195+
});
168196
}
169197
}

tests/FactoryTest.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,51 @@ public function testConnectWillRejectWhenServerClosesConnection()
9797
$loop->run();
9898
}
9999

100+
public function testConnectWillRejectOnExplicitTimeoutDespiteValidAuth()
101+
{
102+
$loop = \React\EventLoop\Factory::create();
103+
$factory = new Factory($loop);
104+
105+
$uri = $this->getConnectionString() . '?timeout=0';
106+
107+
$promise = $factory->createConnection($uri);
108+
109+
$promise->then(null, $this->expectCallableOnceWith(
110+
$this->logicalAnd(
111+
$this->isInstanceOf('Exception'),
112+
$this->callback(function (\Exception $e) {
113+
return $e->getMessage() === 'Connection to database server timed out after 0 seconds';
114+
})
115+
)
116+
));
117+
118+
$loop->run();
119+
}
120+
121+
public function testConnectWillRejectOnDefaultTimeoutFromIniDespiteValidAuth()
122+
{
123+
$loop = \React\EventLoop\Factory::create();
124+
$factory = new Factory($loop);
125+
126+
$uri = $this->getConnectionString();
127+
128+
$old = ini_get('default_socket_timeout');
129+
ini_set('default_socket_timeout', '0');
130+
$promise = $factory->createConnection($uri);
131+
ini_set('default_socket_timeout', $old);
132+
133+
$promise->then(null, $this->expectCallableOnceWith(
134+
$this->logicalAnd(
135+
$this->isInstanceOf('Exception'),
136+
$this->callback(function (\Exception $e) {
137+
return $e->getMessage() === 'Connection to database server timed out after 0 seconds';
138+
})
139+
)
140+
));
141+
142+
$loop->run();
143+
}
144+
100145
public function testConnectWithValidAuthWillRunUntilQuit()
101146
{
102147
$this->expectOutputString('connected.closed.');
@@ -115,6 +160,24 @@ public function testConnectWithValidAuthWillRunUntilQuit()
115160
$loop->run();
116161
}
117162

163+
public function testConnectWithValidAuthWillIgnoreNegativeTimeoutAndRunUntilQuit()
164+
{
165+
$this->expectOutputString('connected.closed.');
166+
167+
$loop = \React\EventLoop\Factory::create();
168+
$factory = new Factory($loop);
169+
170+
$uri = $this->getConnectionString() . '?timeout=-1';
171+
$factory->createConnection($uri)->then(function (ConnectionInterface $connection) {
172+
echo 'connected.';
173+
$connection->quit()->then(function () {
174+
echo 'closed.';
175+
});
176+
}, 'printf')->then(null, 'printf');
177+
178+
$loop->run();
179+
}
180+
118181
public function testConnectWithValidAuthCanPingAndThenQuit()
119182
{
120183
$this->expectOutputString('connected.ping.closed.');

0 commit comments

Comments
 (0)