Skip to content

Commit 0d2d61a

Browse files
Merge pull request #77 from neo4j-php/timeout
Added reconfigurable timeouts on IConnection
2 parents 880ab58 + 1e7a6f7 commit 0d2d61a

File tree

7 files changed

+199
-75
lines changed

7 files changed

+199
-75
lines changed

src/connection/AConnection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,9 @@ public function getTimeout(): float
7171
{
7272
return $this->timeout;
7373
}
74+
75+
public function setTimeout(float $timeout): void
76+
{
77+
$this->timeout = $timeout;
78+
}
7479
}

src/connection/IConnection.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,6 @@ public function getIp(): string;
3131
public function getPort(): int;
3232

3333
public function getTimeout(): float;
34+
35+
public function setTimeout(float $timeout): void;
3436
}

src/connection/Socket.php

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Bolt\Bolt;
66
use Bolt\error\ConnectException;
7+
use Bolt\error\ConnectionTimeoutException;
78

89
/**
910
* Socket class
@@ -20,6 +21,10 @@ class Socket extends AConnection
2021
*/
2122
private $socket = false;
2223

24+
private const POSSIBLE_TIMEOUTS_CODES = [11, 10060];
25+
/** @var float|null */
26+
private $timetAtTimeoutConfiguration;
27+
2328
/**
2429
* Create socket connection
2530
* @return bool
@@ -42,11 +47,7 @@ public function connect(): bool
4247

4348
socket_set_option($this->socket, SOL_TCP, TCP_NODELAY, 1);
4449
socket_set_option($this->socket, SOL_SOCKET, SO_KEEPALIVE, 1);
45-
$timeoutSeconds = floor($this->timeout);
46-
$microSeconds = floor(($this->timeout - $timeoutSeconds) * 1000000);
47-
$timeoutOption = ['sec' => $timeoutSeconds, 'usec' => $microSeconds];
48-
socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, $timeoutOption);
49-
socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, $timeoutOption);
50+
$this->configureTimeout();
5051

5152
$conn = @socket_connect($this->socket, $this->ip, $this->port);
5253
if (!$conn) {
@@ -75,10 +76,9 @@ public function write(string $buffer)
7576
$this->printHex($buffer);
7677

7778
while (0 < $size) {
78-
$sent = socket_write($this->socket, $buffer, $size);
79+
$sent = @socket_write($this->socket, $buffer, $size);
7980
if ($sent === false) {
80-
$code = socket_last_error($this->socket);
81-
throw new ConnectException(socket_strerror($code), $code);
81+
$this->throwConnectException();
8282
}
8383

8484
$buffer = mb_strcut($buffer, $sent, null, '8bit');
@@ -101,10 +101,9 @@ public function read(int $length = 2048): string
101101
}
102102

103103
do {
104-
$readed = socket_read($this->socket, $length - mb_strlen($output, '8bit'), PHP_BINARY_READ);
104+
$readed = @socket_read($this->socket, $length - mb_strlen($output, '8bit'), PHP_BINARY_READ);
105105
if ($readed === false) {
106-
$code = socket_last_error($this->socket);
107-
throw new ConnectException(socket_strerror($code), $code);
106+
$this->throwConnectException();
108107
}
109108
$output .= $readed;
110109
} while (mb_strlen($output, '8bit') < $length);
@@ -125,4 +124,36 @@ public function disconnect()
125124
@socket_close($this->socket);
126125
}
127126
}
127+
128+
public function setTimeout(float $timeout): void
129+
{
130+
parent::setTimeout($timeout);
131+
$this->configureTimeout();
132+
}
133+
134+
private function configureTimeout(): void
135+
{
136+
$timeoutSeconds = floor($this->timeout);
137+
$microSeconds = floor(($this->timeout - $timeoutSeconds) * 1000000);
138+
$timeoutOption = ['sec' => $timeoutSeconds, 'usec' => $microSeconds];
139+
socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, $timeoutOption);
140+
socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, $timeoutOption);
141+
$this->timetAtTimeoutConfiguration = microtime(true);
142+
}
143+
144+
/**
145+
* @throws ConnectException
146+
* @throws ConnectionTimeoutException
147+
*/
148+
private function throwConnectException(): void
149+
{
150+
$code = socket_last_error($this->socket);
151+
if (in_array($code, self::POSSIBLE_TIMEOUTS_CODES)) {
152+
$timediff = microtime(true) - $this->timetAtTimeoutConfiguration;
153+
if ($timediff >= $this->timeout) {
154+
throw ConnectionTimeoutException::createFromTimeout($this->timeout);
155+
}
156+
}
157+
throw new ConnectException(socket_strerror($code), $code);
158+
}
128159
}

src/connection/StreamSocket.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use Bolt\Bolt;
77
use Bolt\error\ConnectException;
8+
use Bolt\error\ConnectionTimeoutException;
89

910
/**
1011
* Stream socket class
@@ -66,10 +67,7 @@ public function connect(): bool
6667
}
6768
}
6869

69-
$timeout = (int) floor($this->timeout);
70-
if (!stream_set_timeout($this->stream, $timeout, (int) floor(($this->timeout - $timeout) * 1000000))) {
71-
throw new ConnectException('Cannot set timeout on stream');
72-
}
70+
$this->configureTimeout();
7371

7472
return true;
7573
}
@@ -100,7 +98,7 @@ public function read(int $length = 2048): string
10098
$res = stream_get_contents($this->stream, $length);
10199

102100
if (stream_get_meta_data($this->stream)["timed_out"])
103-
throw new ConnectException('Connection timeout reached after '.$this->timeout.' seconds.');
101+
throw ConnectionTimeoutException::createFromTimeout($this->timeout);
104102

105103
if (empty($res))
106104
throw new ConnectException('Read error');
@@ -120,4 +118,21 @@ public function disconnect()
120118
stream_socket_shutdown($this->stream, STREAM_SHUT_RDWR);
121119
}
122120

121+
public function setTimeout(float $timeout): void
122+
{
123+
parent::setTimeout($timeout);
124+
$this->configureTimeout();
125+
}
126+
127+
/**
128+
* @return void
129+
* @throws ConnectException
130+
*/
131+
private function configureTimeout(): void
132+
{
133+
$timeout = (int)floor($this->timeout);
134+
if (!stream_set_timeout($this->stream, $timeout, (int)floor(($this->timeout - $timeout) * 1000000))) {
135+
throw new ConnectException('Cannot set timeout on stream');
136+
}
137+
}
123138
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Bolt\error;
4+
5+
class ConnectionTimeoutException extends ConnectException
6+
{
7+
public static function createFromTimeout(float $timeout): self
8+
{
9+
return new self('Connection timeout reached after '.$timeout.' seconds.');
10+
}
11+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace Bolt\tests\connection;
4+
5+
use Bolt\Bolt;
6+
use Bolt\connection\IConnection;
7+
use Bolt\connection\Socket;
8+
use Bolt\connection\StreamSocket;
9+
use Bolt\error\ConnectionTimeoutException;
10+
use Bolt\error\MessageException;
11+
use Bolt\helpers\Auth;
12+
use Bolt\protocol\V4;
13+
use PHPUnit\Framework\TestCase;
14+
15+
final class AConnectionTest extends TestCase
16+
{
17+
public function provideConnections(): array
18+
{
19+
return [
20+
StreamSocket::class => [StreamSocket::class],
21+
Socket::class => [Socket::class],
22+
];
23+
}
24+
25+
/**
26+
* @dataProvider provideConnections
27+
*/
28+
public function testMillisecondTimeout(string $alias): void
29+
{
30+
$socket = $this->getConnection($alias);
31+
$protocol = (new Bolt($socket))->build();
32+
$socket->setTimeout(1.5);
33+
$protocol->init(Auth::basic($GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']));
34+
35+
$time = microtime(true);
36+
try {
37+
$protocol->run('FOREACH ( i IN range(1,10000) |
38+
MERGE (d:Day {day: i})
39+
)');
40+
$this->fail('No timeout error triggered');
41+
} catch (ConnectionTimeoutException $e) {
42+
$newTime = microtime(true);
43+
44+
$this->assertGreaterThanOrEqual(1.5, $newTime - $time);
45+
}
46+
}
47+
48+
/**
49+
* @dataProvider provideConnections
50+
*/
51+
public function testSecondsTimeout(string $alias): void
52+
{
53+
$socket = $this->getConnection($alias);
54+
$protocol = (new Bolt($socket))->build();
55+
$protocol->init(Auth::basic($GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']));
56+
57+
$time = microtime(true);
58+
try {
59+
$protocol->run('FOREACH ( i IN range(1,10000) |
60+
MERGE (d:Day {day: i})
61+
)');
62+
$this->fail('No timeout error triggered');
63+
} catch (ConnectionTimeoutException $e) {
64+
$newTime = microtime(true);
65+
66+
$this->assertGreaterThanOrEqual(1.0, $newTime - $time);
67+
}
68+
}
69+
70+
/**
71+
* @dataProvider provideConnections
72+
*/
73+
public function testTimeoutRecoverAndReset(string $alias): void
74+
{
75+
$socket = $this->getConnection($alias);
76+
$protocol = (new Bolt($socket))->build();
77+
$protocol->init(Auth::basic($GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']));
78+
79+
$time = microtime(true);
80+
try {
81+
$protocol->run('FOREACH ( i IN range(1,10000) |
82+
MERGE (d:Day {day: i})
83+
)');
84+
$this->fail('No timeout error triggered');
85+
} catch (ConnectionTimeoutException $e) {
86+
$newTime = microtime(true);
87+
88+
$this->assertGreaterThanOrEqual(1.0, $newTime - $time);
89+
}
90+
91+
$socket->setTimeout(100.0);
92+
try {
93+
$protocol->reset();
94+
} catch (MessageException $e) {
95+
echo $e->getMessage();
96+
$protocol = (new Bolt($socket))->build();
97+
$protocol->init(Auth::basic($GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']));
98+
}
99+
100+
$socket->setTimeout(1.0);
101+
102+
$time = microtime(true);
103+
try {
104+
$protocol->run('FOREACH ( i IN range(1,10000) |
105+
MERGE (d:Day {day: i})
106+
)');
107+
$this->fail('No timeout error triggered');
108+
} catch (ConnectionTimeoutException $e) {
109+
$newTime = microtime(true);
110+
111+
$this->assertGreaterThanOrEqual(1.0, $newTime - $time);
112+
}
113+
}
114+
115+
private function getConnection(string $class): IConnection
116+
{
117+
return new $class($GLOBALS['NEO_HOST'] ?? '127.0.0.1', (int) ($GLOBALS['NEO_PORT'] ?? 7687), 1);
118+
}
119+
}

tests/connection/StreamSocketTest.php

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)