Skip to content

Commit 08586ce

Browse files
committed
fixed failed integration test
1 parent 4d749ec commit 08586ce

14 files changed

+254
-61
lines changed

src/Bolt/BoltConnection.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class BoltConnection implements ConnectionInterface
6363
* @var list<WeakReference<CypherList>>
6464
*/
6565
private array $subscribedResults = [];
66+
private float $createdAtMillis;
6667

6768
/**
6869
* @return array{0: V4_4|V5|V5_1|V5_2|V5_3|V5_4|null, 1: Connection}
@@ -72,9 +73,6 @@ public function getImplementation(): array
7273
return [$this->boltProtocol, $this->connection];
7374
}
7475

75-
/**
76-
* @psalm-mutation-free
77-
*/
7876
public function __construct(
7977
private V4_4|V5|V5_1|V5_2|V5_3|V5_4|null $boltProtocol,
8078
private readonly Connection $connection,
@@ -85,6 +83,7 @@ public function __construct(
8583
private readonly ?Neo4jLogger $logger,
8684
) {
8785
$this->messageFactory = new BoltMessageFactory($this, $this->logger);
86+
$this->createdAtMillis = (int) (microtime(true) * 1000);
8887
}
8988

9089
public function getEncryptionLevel(): string
@@ -392,4 +391,9 @@ public function assertNoFailure(Response $response): void
392391
throw Neo4jException::fromBoltResponse($response);
393392
}
394393
}
394+
395+
public function getCreatedAtMillis(): float
396+
{
397+
return $this->createdAtMillis;
398+
}
395399
}

src/Bolt/Connection.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace Laudis\Neo4j\Bolt;
1515

1616
use Bolt\connection\IConnection;
17+
use Bolt\error\ConnectionTimeoutException;
18+
use Laudis\Neo4j\Exception\TimeoutException;
1719

1820
class Connection
1921
{
@@ -33,12 +35,20 @@ public function getIConnection(): IConnection
3335

3436
public function write(string $buffer): void
3537
{
36-
$this->connection->write($buffer);
38+
try {
39+
$this->connection->write($buffer);
40+
} catch (ConnectionTimeoutException $e) {
41+
throw new TimeoutException(previous: $e);
42+
}
3743
}
3844

3945
public function read(int $length = 2048): string
4046
{
41-
return $this->connection->read($length);
47+
try {
48+
return $this->connection->read($length);
49+
} catch (ConnectionTimeoutException $e) {
50+
throw new TimeoutException(previous: $e);
51+
}
4252
}
4353

4454
public function disconnect(): void

src/Bolt/ConnectionPool.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
/**
3434
* @implements ConnectionPoolInterface<BoltConnection>
3535
*/
36-
final class ConnectionPool implements ConnectionPoolInterface
36+
class ConnectionPool implements ConnectionPoolInterface
3737
{
3838
/** @var list<BoltConnection> */
3939
private array $activeConnections = [];
@@ -45,6 +45,7 @@ public function __construct(
4545
private readonly ?Neo4jLogger $logger,
4646
private readonly float $acquireConnectionTimeout,
4747
private readonly float $connectionTimeout,
48+
private readonly float $maxConnectionLifetime,
4849
) {
4950
}
5051

@@ -66,7 +67,8 @@ public static function create(
6667
),
6768
$conf->getLogger(),
6869
$conf->getAcquireConnectionTimeout(),
69-
$conf->getConnectionTimeout()
70+
$conf->getConnectionTimeout(),
71+
$conf->getMaxConnectionLifetime()
7072
);
7173
}
7274

@@ -106,9 +108,10 @@ public function acquire(SessionConfiguration $config): Generator
106108
}
107109

108110
try {
109-
$connection = $this->factory->createConnection($this->data, $config, $this->connectionTimeout);
111+
$connection = $this->factory->createConnection($this->data, $config, $this->connectionTimeout, $this->maxConnectionLifetime);
110112

111113
$this->activeConnections[] = $connection;
114+
112115
return $connection;
113116
} catch (ConnectionTimeoutException $e) {
114117
throw new TimeoutException($e->getMessage(), $e->getCode(), $e);
@@ -138,9 +141,15 @@ private function reuseConnectionIfPossible(SessionConfiguration $config): ?BoltC
138141
{
139142
// Ensure random connection reuse before picking one.
140143
shuffle($this->activeConnections);
141-
foreach ($this->activeConnections as $activeConnection) {
144+
foreach ($this->activeConnections as $index => $activeConnection) {
142145
// We prefer a connection that is just ready
143146
if ($activeConnection->getServerState() === 'READY' && $this->factory->canReuseConnection($activeConnection, $config)) {
147+
if ($this->isConnectionExpired($activeConnection)) {
148+
$activeConnection->close();
149+
unset($this->activeConnections[$index]); // Remove expired connection
150+
continue;
151+
}
152+
144153
return $this->factory->reuseConnection($activeConnection, $config);
145154
}
146155
}
@@ -155,4 +164,12 @@ public function close(): void
155164
}
156165
$this->activeConnections = [];
157166
}
167+
168+
public function isConnectionExpired(BoltConnection $activeConnection): bool
169+
{
170+
$now = (int) (microtime(true) * 1000);
171+
$timeSinceCreatedInSeconds = (int) (($now - $activeConnection->getCreatedAtMillis()) / 1000);
172+
173+
return $timeSinceCreatedInSeconds >= $this->maxConnectionLifetime;
174+
}
158175
}

src/BoltFactory.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static function create(?Neo4jLogger $logger): self
4848
return new self(SystemWideConnectionFactory::getInstance(), new ProtocolFactory(), new SslConfigurationFactory(), $logger);
4949
}
5050

51-
public function createConnection(ConnectionRequestData $data, SessionConfiguration $sessionConfig, float $connectionTimeout): BoltConnection
51+
public function createConnection(ConnectionRequestData $data, SessionConfiguration $sessionConfig, float $connectionTimeout, float $maxConnectionLifetime): BoltConnection
5252
{
5353
[$sslLevel, $sslConfig] = $this->sslConfigurationFactory->create($data->getUri()->withHost($data->getHostname()), $data->getSslConfig());
5454

@@ -57,8 +57,7 @@ public function createConnection(ConnectionRequestData $data, SessionConfigurati
5757
$data->getUri()->getPort(),
5858
$sslLevel,
5959
$sslConfig,
60-
$connectionTimeout
61-
);
60+
$connectionTimeout);
6261

6362
$connection = $this->connectionFactory->create($uriConfig);
6463
$protocol = $this->protocolFactory->createProtocol($connection->getIConnection());

src/Contracts/BoltMessage.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
namespace Laudis\Neo4j\Contracts;
1515

16+
use Bolt\error\ConnectionTimeoutException;
1617
use Bolt\protocol\Response;
1718
use Iterator;
1819
use Laudis\Neo4j\Bolt\BoltConnection;
20+
use Laudis\Neo4j\Exception\TimeoutException;
1921

2022
abstract class BoltMessage
2123
{
@@ -31,7 +33,11 @@ abstract public function send(): BoltMessage;
3133

3234
public function getResponse(): Response
3335
{
34-
$response = $this->connection->protocol()->getResponse();
36+
try {
37+
$response = $this->connection->protocol()->getResponse();
38+
} catch (ConnectionTimeoutException $e) {
39+
throw new TimeoutException(previous: $e);
40+
}
3541

3642
$this->connection->assertNoFailure($response);
3743

@@ -43,9 +49,13 @@ public function getResponse(): Response
4349
*/
4450
public function getResponses(): Iterator
4551
{
46-
/**
47-
* @var Iterator<Response>
48-
*/
49-
return $this->connection->protocol()->getResponses();
52+
try {
53+
/**
54+
* @var Iterator<Response>
55+
*/
56+
return $this->connection->protocol()->getResponses();
57+
} catch (ConnectionTimeoutException $e) {
58+
throw new TimeoutException(previous: $e);
59+
}
5060
}
5161
}

src/Databags/DriverConfiguration.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ final class DriverConfiguration
4040
public const DEFAULT_CACHE_IMPLEMENTATION = Cache::class;
4141
public const DEFAULT_ACQUIRE_CONNECTION_TIMEOUT = 60.0;
4242
public const DEFAULT_CONNECTION_TIMEOUT = 30.0;
43+
public const DEFAULT_MAX_CONNECTION_LIFETIME = 3600.0;
4344

4445
/** @var callable():(CacheInterface|null)|CacheInterface|null */
4546
private $cache;
@@ -61,6 +62,7 @@ public function __construct(
6162
CacheInterface|callable|null $cache,
6263
private ?float $acquireConnectionTimeout,
6364
private ?float $connectionTimeout,
65+
private ?float $maxConnectionLifetime,
6466
callable|SemaphoreFactoryInterface|null $semaphore,
6567
?string $logLevel,
6668
?LoggerInterface $logger,
@@ -84,6 +86,7 @@ public static function create(
8486
CacheInterface $cache,
8587
float $acquireConnectionTimeout,
8688
float $connectionTimeout,
89+
float $maxConnectionLifetime,
8790
SemaphoreFactoryInterface $semaphore,
8891
?string $logLevel,
8992
?LoggerInterface $logger,
@@ -95,6 +98,7 @@ public static function create(
9598
$cache,
9699
$acquireConnectionTimeout,
97100
$connectionTimeout,
101+
$maxConnectionLifetime,
98102
$semaphore,
99103
$logLevel,
100104
$logger
@@ -118,6 +122,7 @@ public static function default(): self
118122
null,
119123
null,
120124
null,
125+
null,
121126
null
122127
);
123128
}
@@ -258,6 +263,25 @@ public function withConnectionTimeout(?float $connectionTimeout): self
258263
return $tbr;
259264
}
260265

266+
/**
267+
* @psalm-immutable
268+
*/
269+
public function getMaxConnectionLifetime(): float
270+
{
271+
return $this->maxConnectionLifetime ??= self::DEFAULT_MAX_CONNECTION_LIFETIME;
272+
}
273+
274+
/**
275+
* @psalm-immutable
276+
*/
277+
public function withMaxConnectionLifetime(?float $maxConnectionLifetime): self
278+
{
279+
$tbr = clone $this;
280+
$tbr->maxConnectionLifetime = $maxConnectionLifetime;
281+
282+
return $tbr;
283+
}
284+
261285
/**
262286
* @param callable():(SemaphoreFactoryInterface|null)|SemaphoreFactoryInterface|null $factory
263287
*

src/Exception/TimeoutException.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22

33
declare(strict_types=1);
44

5+
/*
6+
* This file is part of the Neo4j PHP Client and Driver package.
7+
*
8+
* (c) Nagels <https://nagels.tech>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
514
namespace Laudis\Neo4j\Exception;
615

716
use RuntimeException;
817

918
class TimeoutException extends RuntimeException
1019
{
11-
1220
}

src/Neo4j/Neo4jConnectionPool.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public function __construct(
7676
private readonly ?Neo4jLogger $logger,
7777
private readonly float $acquireConnectionTimeout,
7878
private readonly float $connectionTimeout,
79+
private readonly float $maxConnectionLifetimeMillis,
7980
) {
8081
}
8182

@@ -100,7 +101,8 @@ public static function create(
100101
$resolver,
101102
$conf->getLogger(),
102103
$conf->getAcquireConnectionTimeout(),
103-
$conf->getConnectionTimeout()
104+
$conf->getConnectionTimeout(),
105+
$conf->getMaxConnectionLifetime()
104106
);
105107
}
106108

@@ -116,7 +118,7 @@ public function createOrGetPool(string $hostname, UriInterface $uri): Connection
116118

117119
$key = $this->createKey($data);
118120
if (!array_key_exists($key, self::$pools)) {
119-
self::$pools[$key] = new ConnectionPool($this->semaphore, $this->factory, $data, $this->logger, $this->acquireConnectionTimeout, $this->connectionTimeout);
121+
self::$pools[$key] = new ConnectionPool($this->semaphore, $this->factory, $data, $this->logger, $this->acquireConnectionTimeout, $this->connectionTimeout, $this->maxConnectionLifetimeMillis);
120122
}
121123

122124
return self::$pools[$key];

testkit-backend/src/Handlers/RetryableNegative.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,23 @@ public function handle($request): TestkitResponseInterface
5151
// for a 'RetryableNegative' test case.
5252
// It typically involves an operation designed to fail with a retryable error.
5353

54-
throw new Neo4jException(
55-
'Simulated retryable error: Transaction failed due to deadlock.',
56-
'Neo.TransientError.Transaction.DeadlockDetected'
57-
);
58-
54+
throw new Neo4jException('Simulated retryable error: Transaction failed due to deadlock.', 'Neo.TransientError.Transaction.DeadlockDetected');
5955
} catch (Neo4jException $e) {
6056
$this->logger->info('Caught Neo4jException in RetryableNegative handler', ['exception' => $e->getMessage()]);
57+
6158
return new DriverErrorResponse($sessionId, $e);
6259
} catch (TransactionException $e) {
6360
$this->logger->info('Caught TransactionException in RetryableNegative handler', ['exception' => $e->getMessage()]);
61+
6462
return new DriverErrorResponse($sessionId, $e);
6563
} catch (TimeoutException $e) {
6664
$this->logger->info('Caught TimeoutException in RetryableNegative handler', ['exception' => $e->getMessage()]);
65+
6766
return new DriverErrorResponse($sessionId, $e);
6867
} catch (Exception $e) {
6968
$this->logger->error('Unhandled exception in RetryableNegative handler', ['exception' => $e->getMessage()]);
70-
return new FrontendErrorResponse('Unhandled exception in RetryableNegative handler: ' . $e->getMessage());
71-
}
7269

70+
return new FrontendErrorResponse('Unhandled exception in RetryableNegative handler: '.$e->getMessage());
71+
}
7372
}
7473
}

tests/Integration/BoltResultIntegrationTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function testIterationLong(): void
4242
);
4343
$connection = $factory->createConnection(
4444
new ConnectionRequestData($this->getUri()->getHost(), $this->getUri(), Authenticate::fromUrl($this->getUri()), 'a/b', new SslConfiguration(SslMode::FROM_URL(), false)),
45-
SessionConfiguration::default(),DriverConfiguration::DEFAULT_CONNECTION_TIMEOUT
45+
SessionConfiguration::default(), DriverConfiguration::DEFAULT_CONNECTION_TIMEOUT, DriverConfiguration::DEFAULT_MAX_CONNECTION_LIFETIME
4646
);
4747

4848
$connection->protocol()->run('UNWIND range(1, 100000) AS i RETURN i')

0 commit comments

Comments
 (0)