Skip to content

Commit 7492476

Browse files
transistiveexaby73
andauthored
[feat]: More precise handling of errors when verifying connectivity (#242)
* Improve error handling when creating connections --------- Co-authored-by: exaby73 <[email protected]>
1 parent 5bf29c7 commit 7492476

7 files changed

+149
-23
lines changed

docker-compose-neo4j-4.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ x-shared:
33
NEO4J_AUTH: neo4j/testtest
44
NEO4J_ACCEPT_LICENSE_AGREEMENT: "yes"
55
NEO4J_dbms_security_allow__csv__import__from__file__urls: "true"
6+
NEO4J_dbms_security_auth__lock__time: 0s
67
NEO4JLABS_PLUGINS: '["apoc"]'
78

89
x-shared-cluster:
@@ -31,7 +32,7 @@ services:
3132
context: .
3233
dockerfile: Dockerfile
3334
args:
34-
PHP_VERSION: ${PHP_VERSION}
35+
PHP_VERSION: "${PHP_VERSION-8.1}"
3536
networks:
3637
- neo4j
3738
volumes:
@@ -46,7 +47,7 @@ services:
4647
context: .
4748
dockerfile: Dockerfile
4849
args:
49-
PHP_VERSION: ${PHP_VERSION}
50+
PHP_VERSION: "${PHP_VERSION-8.1}"
5051
WITH_XDEBUG: true
5152
working_dir: /opt/project
5253
volumes:

docker-compose.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ x-definitions:
22
x-shared-env:
33
&common-env
44
NEO4J_AUTH: neo4j/testtest
5+
NEO4J_dbms_security_auth__lock__time: 0s
56
NEO4J_PLUGINS: '["apoc"]'
67
x-shared-cluster-env:
78
&common-cluster-env
@@ -25,7 +26,7 @@ x-definitions:
2526
context: .
2627
dockerfile: Dockerfile
2728
args:
28-
PHP_VERSION: ${PHP_VERSION}
29+
PHP_VERSION: "${PHP_VERSION-8.1}"
2930
volumes:
3031
- .:/opt/project
3132
x-common-cluster:

src/Bolt/BoltDriver.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace Laudis\Neo4j\Bolt;
1515

16+
use Bolt\error\ConnectException;
1617
use Exception;
1718

1819
use function is_string;
@@ -28,7 +29,7 @@
2829
use Laudis\Neo4j\Databags\SessionConfiguration;
2930
use Laudis\Neo4j\Formatter\OGMFormatter;
3031
use Psr\Http\Message\UriInterface;
31-
use Throwable;
32+
use Psr\Log\LogLevel;
3233

3334
/**
3435
* Drives a singular bolt connections.
@@ -103,7 +104,9 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool
103104
$config ??= SessionConfiguration::default();
104105
try {
105106
GeneratorHelper::getReturnFromGenerator($this->pool->acquire($config));
106-
} catch (Throwable) {
107+
} catch (ConnectException $e) {
108+
$this->pool->getLogger()?->log(LogLevel::WARNING, 'Could not connect to server on URI '.$this->parsedUrl->__toString(), ['error' => $e]);
109+
107110
return false;
108111
}
109112

src/Common/DriverSetupManager.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use function array_key_first;
1818
use function array_reduce;
1919

20+
use Bolt\error\ConnectException;
2021
use Countable;
2122
use InvalidArgumentException;
2223
use Laudis\Neo4j\Authentication\Authenticate;
@@ -29,6 +30,7 @@
2930

3031
use const PHP_INT_MIN;
3132

33+
use Psr\Log\LogLevel;
3234
use RuntimeException;
3335
use SplPriorityQueue;
3436

@@ -144,7 +146,13 @@ public function verifyConnectivity(SessionConfiguration $config, ?string $alias
144146
{
145147
try {
146148
$this->getDriver($config, $alias);
147-
} catch (RuntimeException) {
149+
} catch (ConnectException $e) {
150+
$this->getLogger()?->log(
151+
LogLevel::WARNING,
152+
sprintf('Could not connect to server using alias (%s)', $alias ?? '<default>'),
153+
['exception' => $e]
154+
);
155+
148156
return false;
149157
}
150158

src/Neo4j/Neo4jConnectionPool.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
namespace Laudis\Neo4j\Neo4j;
1515

1616
use function array_unique;
17+
18+
use Bolt\error\ConnectException;
19+
1720
use function count;
1821

1922
use Exception;
@@ -50,9 +53,6 @@
5053

5154
use function sprintf;
5255
use function str_replace;
53-
54-
use Throwable;
55-
5656
use function time;
5757

5858
/**
@@ -146,7 +146,7 @@ public function acquire(SessionConfiguration $config): Generator
146146
/** @var BoltConnection $connection */
147147
$connection = GeneratorHelper::getReturnFromGenerator($pool->acquire($config));
148148
$table = $this->routingTable($connection, $config);
149-
} catch (Throwable $e) {
149+
} catch (ConnectException $e) {
150150
// todo - once client side logging is implemented it must be conveyed here.
151151
$latestError = $e;
152152
continue; // We continue if something is wrong with the current server

src/Neo4j/Neo4jDriver.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace Laudis\Neo4j\Neo4j;
1515

16+
use Bolt\error\ConnectException;
1617
use Exception;
1718

1819
use function is_string;
@@ -31,7 +32,7 @@
3132
use Laudis\Neo4j\Databags\SessionConfiguration;
3233
use Laudis\Neo4j\Formatter\OGMFormatter;
3334
use Psr\Http\Message\UriInterface;
34-
use Throwable;
35+
use Psr\Log\LogLevel;
3536

3637
/**
3738
* Driver for auto client-side routing.
@@ -105,7 +106,9 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool
105106
$config ??= SessionConfiguration::default();
106107
try {
107108
GeneratorHelper::getReturnFromGenerator($this->pool->acquire($config));
108-
} catch (Throwable) {
109+
} catch (ConnectException $e) {
110+
$this->pool->getLogger()?->log(LogLevel::WARNING, 'Could not connect to server on URI '.$this->parsedUrl->__toString(), ['error' => $e]);
111+
109112
return false;
110113
}
111114

tests/Integration/ClientIntegrationTest.php

+121-11
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,95 @@
1313

1414
namespace Laudis\Neo4j\Tests\Integration;
1515

16+
use Exception;
1617
use InvalidArgumentException;
1718
use Laudis\Neo4j\Authentication\Authenticate;
1819
use Laudis\Neo4j\Basic\Driver;
1920
use Laudis\Neo4j\Bolt\BoltDriver;
2021
use Laudis\Neo4j\Bolt\ConnectionPool;
2122
use Laudis\Neo4j\ClientBuilder;
23+
use Laudis\Neo4j\Common\DriverSetupManager;
2224
use Laudis\Neo4j\Contracts\DriverInterface;
2325
use Laudis\Neo4j\Contracts\TransactionInterface;
26+
use Laudis\Neo4j\Databags\DriverConfiguration;
27+
use Laudis\Neo4j\Databags\DriverSetup;
2428
use Laudis\Neo4j\Databags\SessionConfiguration;
2529
use Laudis\Neo4j\Databags\Statement;
30+
use Laudis\Neo4j\Databags\TransactionConfiguration;
2631
use Laudis\Neo4j\Exception\Neo4jException;
32+
use Laudis\Neo4j\Formatter\SummarizedResultFormatter;
2733
use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest;
34+
use Psr\Log\LoggerInterface;
35+
use Psr\Log\LogLevel;
2836
use ReflectionClass;
37+
use RuntimeException;
2938

3039
final class ClientIntegrationTest extends EnvironmentAwareIntegrationTest
3140
{
41+
public function setUp(): void
42+
{
43+
parent::setUp();
44+
$this->driver->closeConnections();
45+
}
46+
47+
public function testDriverAuthFailureVerifyConnectivity(): void
48+
{
49+
if (str_starts_with($this->uri->getScheme(), 'http')) {
50+
$this->markTestSkipped('HTTP does not support auth failure connectivity passing');
51+
}
52+
53+
$uri = $this->uri->withUserInfo('neo4j', 'absolutelyonehundredpercentawrongpassword');
54+
55+
$conf = DriverConfiguration::default()->withLogger(LogLevel::DEBUG, $this->createMock(LoggerInterface::class));
56+
$logger = $conf->getLogger();
57+
if ($logger === null) {
58+
throw new RuntimeException('Logger not set');
59+
}
60+
61+
$driver = Driver::create($uri, $conf);
62+
63+
$this->expectException(Neo4jException::class);
64+
$this->expectExceptionMessage(
65+
'Neo4j errors detected. First one with code "Neo.ClientError.Security.Unauthorized" and message "The client is unauthorized due to authentication failure."'
66+
);
67+
68+
$driver->verifyConnectivity();
69+
}
70+
71+
public function testClientAuthFailureVerifyConnectivity(): void
72+
{
73+
if (str_starts_with($this->uri->getScheme(), 'http')) {
74+
$this->markTestSkipped('HTTP does not support auth failure connectivity passing');
75+
}
76+
77+
$uri = $this->uri->withUserInfo('neo4j', 'absolutelyonehundredpercentawrongpassword');
78+
79+
/** @noinspection PhpUnhandledExceptionInspection */
80+
$conf = DriverConfiguration::default()->withLogger(LogLevel::DEBUG, $this->createMock(LoggerInterface::class));
81+
$logger = $conf->getLogger();
82+
if ($logger === null) {
83+
throw new RuntimeException('Logger not set');
84+
}
85+
86+
$client = (new ClientBuilder(
87+
SessionConfiguration::create(),
88+
TransactionConfiguration::create(),
89+
(new DriverSetupManager(
90+
SummarizedResultFormatter::create(),
91+
$conf,
92+
))->withSetup(
93+
new DriverSetup($uri, Authenticate::fromUrl($uri, $logger))
94+
)
95+
))->build();
96+
97+
$this->expectException(Neo4jException::class);
98+
$this->expectExceptionMessage(
99+
'Neo4j errors detected. First one with code "Neo.ClientError.Security.Unauthorized" and message "The client is unauthorized due to authentication failure."'
100+
);
101+
102+
$client->getDriver(null);
103+
}
104+
32105
public function testDifferentAuth(): void
33106
{
34107
$auth = Authenticate::fromUrl($this->getUri());
@@ -53,28 +126,37 @@ public function testAvailabilityFullImplementation(): void
53126

54127
public function testTransactionFunction(): void
55128
{
56-
$result = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x'));
129+
$result = $this->getSession()->transaction(
130+
static fn (TransactionInterface $tsx) => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x')
131+
);
57132

58133
self::assertEquals(1, $result);
59134

60-
$result = $this->getSession()->readTransaction(static fn (TransactionInterface $tsx) => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x'));
135+
$result = $this->getSession()->readTransaction(
136+
static fn (TransactionInterface $tsx) => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x')
137+
);
61138

62139
self::assertEquals(1, $result);
63140

64-
$result = $this->getSession()->writeTransaction(static fn (TransactionInterface $tsx) => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x'));
141+
$result = $this->getSession()->writeTransaction(
142+
static fn (TransactionInterface $tsx) => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x')
143+
);
65144

66145
self::assertEquals(1, $result);
67146
}
68147

69148
public function testValidRun(): void
70149
{
71-
$response = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<<'CYPHER'
150+
$response = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(
151+
<<<'CYPHER'
72152
MERGE (x:TestNode {test: $test})
73153
WITH x
74154
MERGE (y:OtherTestNode {test: $otherTest})
75155
WITH x, y, {c: 'd'} AS map, [1, 2, 3] AS list
76156
RETURN x, y, x.test AS test, map, list
77-
CYPHER, ['test' => 'a', 'otherTest' => 'b']));
157+
CYPHER,
158+
['test' => 'a', 'otherTest' => 'b']
159+
));
78160

79161
self::assertEquals(1, $response->count());
80162
$map = $response->first();
@@ -89,7 +171,12 @@ public function testValidRun(): void
89171
public function testInvalidRun(): void
90172
{
91173
$this->expectException(Neo4jException::class);
92-
$this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('MERGE (x:Tes0342hdm21.())', ['test' => 'a', 'otherTest' => 'b']));
174+
$this->getSession()->transaction(
175+
static fn (TransactionInterface $tsx) => $tsx->run(
176+
'MERGE (x:Tes0342hdm21.())',
177+
['test' => 'a', 'otherTest' => 'b']
178+
)
179+
);
93180
}
94181

95182
public function testInvalidRunRetry(): void
@@ -108,13 +195,18 @@ public function testInvalidRunRetry(): void
108195

109196
public function testValidStatement(): void
110197
{
111-
$response = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->runStatement(Statement::create(<<<'CYPHER'
198+
$response = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->runStatement(
199+
Statement::create(
200+
<<<'CYPHER'
112201
MERGE (x:TestNode {test: $test})
113202
WITH x
114203
MERGE (y:OtherTestNode {test: $otherTest})
115204
WITH x, y, {c: 'd'} AS map, [1, 2, 3] AS list
116205
RETURN x, y, x.test AS test, map, list
117-
CYPHER, ['test' => 'a', 'otherTest' => 'b'])));
206+
CYPHER,
207+
['test' => 'a', 'otherTest' => 'b']
208+
)
209+
));
118210

119211
self::assertEquals(1, $response->count());
120212
$map = $response->first();
@@ -187,9 +279,27 @@ public function testInvalidConnectionCheck(): void
187279
->withDriver('http', 'http://localboast')
188280
->build();
189281

190-
self::assertFalse($client->verifyConnectivity('bolt'));
191-
self::assertFalse($client->verifyConnectivity('neo4j'));
192-
self::assertFalse($client->verifyConnectivity('http'));
282+
$exceptionThrownCount = 0;
283+
try {
284+
$client->verifyConnectivity('bolt');
285+
} catch (Exception $e) {
286+
self::assertInstanceOf(RuntimeException::class, $e);
287+
++$exceptionThrownCount;
288+
}
289+
try {
290+
$client->verifyConnectivity('neo4j');
291+
} catch (Exception $e) {
292+
self::assertInstanceOf(RuntimeException::class, $e);
293+
++$exceptionThrownCount;
294+
}
295+
try {
296+
$client->verifyConnectivity('http');
297+
} catch (Exception $e) {
298+
self::assertInstanceOf(RuntimeException::class, $e);
299+
++$exceptionThrownCount;
300+
}
301+
302+
self::assertEquals(3, $exceptionThrownCount);
193303
}
194304

195305
public function testValidConnectionCheck(): void

0 commit comments

Comments
 (0)