Skip to content

Commit 3dbd78e

Browse files
committed
Fixeed testkit stub-configuration_hints tests
1 parent 79cdb30 commit 3dbd78e

File tree

7 files changed

+93
-4
lines changed

7 files changed

+93
-4
lines changed

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ services:
133133
TEST_DRIVER_REPO: /opt/project
134134
TEST_BACKEND_HOST: testkit_backend
135135
TEST_STUB_HOST: testkit
136+
BOLT_LISTEN_ADDR: "0.0.0.0:9001"
136137
depends_on:
137138
- testkit_backend
138139

src/Bolt/BoltConnection.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ public function assertNoFailure(Response $response): void
419419
*/
420420
public function discardUnconsumedResults(): void
421421
{
422+
if (!$this->isOpen()) {
423+
return;
424+
}
425+
422426
if (!in_array($this->protocol()->serverState, [ServerState::STREAMING, ServerState::TX_STREAMING], true)) {
423427
return;
424428
}

src/Bolt/BoltResult.php

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

1616
use function array_splice;
17+
18+
use Bolt\error\ConnectException as BoltConnectException;
19+
1720
use function count;
1821

1922
use Generator;
2023

2124
use function in_array;
2225

2326
use Iterator;
27+
use Laudis\Neo4j\Databags\Neo4jError;
28+
use Laudis\Neo4j\Exception\Neo4jException;
2429
use Laudis\Neo4j\Formatter\SummarizedResultFormatter;
2530

2631
/**
@@ -100,7 +105,17 @@ public function consume(): array
100105

101106
private function fetchResults(): void
102107
{
103-
$meta = $this->connection->pull($this->qid, $this->fetchSize);
108+
try {
109+
$meta = $this->connection->pull($this->qid, $this->fetchSize);
110+
} catch (BoltConnectException $e) {
111+
// Close connection on socket errors
112+
try {
113+
$this->connection->close();
114+
} catch (Throwable) {
115+
// Ignore errors when closing
116+
}
117+
throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Cluster.NotALeader', 'Connection error: '.$e->getMessage())], $e);
118+
}
104119

105120
/** @var list<list> $rows */
106121
$rows = array_splice($meta, 0, count($meta) - 1);
@@ -154,6 +169,11 @@ public function __destruct()
154169

155170
public function discard(): void
156171
{
157-
$this->connection->discard($this->qid === -1 ? null : $this->qid);
172+
try {
173+
$this->connection->discard($this->qid === -1 ? null : $this->qid);
174+
} catch (BoltConnectException $e) {
175+
// Ignore connection errors during discard - connection is already broken
176+
// The Neo4jException will be thrown when the next operation is attempted
177+
}
158178
}
159179
}

src/Bolt/BoltUnmanagedTransaction.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public function runStatement(Statement $statement): SummarizedResult
149149
$this->database,
150150
$this->tsxConfig->getTimeout(),
151151
$this->isInstantTransaction ? $this->bookmarkHolder : null, // let the begin transaction pass the bookmarks if it is a managed transaction
152-
$this->isInstantTransaction ? $this->config->getAccessMode() : null, // let the begin transaction decide if it is a managed transaction
152+
null, // mode is never sent in RUN messages - it comes from session configuration
153153
$this->tsxConfig->getMetaData()
154154
);
155155
} catch (Throwable $e) {

src/BoltFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ public function createConnection(ConnectionRequestData $data, SessionConfigurati
7979

8080
$config->setServerAgent($response['server']);
8181

82+
// Apply recv_timeout hint if present
83+
if (array_key_exists('connection.recv_timeout_seconds', $response['hints'])) {
84+
$connection->setTimeout((float) $response['hints']['connection.recv_timeout_seconds']);
85+
}
86+
8287
return $connection;
8388
}
8489

src/Contracts/BoltMessage.php

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
use Bolt\protocol\Response;
1717
use Iterator;
1818
use Laudis\Neo4j\Bolt\BoltConnection;
19+
use Laudis\Neo4j\Databags\Neo4jError;
20+
use Laudis\Neo4j\Exception\Neo4jException;
21+
use Throwable;
1922

2023
abstract class BoltMessage
2124
{
@@ -31,13 +34,62 @@ abstract public function send(): BoltMessage;
3134

3235
public function getResponse(): Response
3336
{
34-
$response = $this->connection->protocol()->getResponse();
37+
try {
38+
$response = $this->connection->protocol()->getResponse();
39+
} catch (Throwable $e) {
40+
// Convert socket timeout and I/O exceptions to Neo4jException
41+
$message = strtolower($e->getMessage());
42+
43+
if ($this->isTimeoutException($e)) {
44+
// Extract timeout value from the exception message if available
45+
$timeoutMsg = 'Connection timeout reached';
46+
if (preg_match('/(\d+)\s*(?:milliseconds?|ms|seconds?|s)/', $e->getMessage(), $matches)) {
47+
$timeoutMsg = 'Connection timeout reached after '.$matches[1].' seconds';
48+
}
49+
// Close the connection to mark it as unusable
50+
try {
51+
$this->connection->close();
52+
} catch (Throwable) {
53+
// Ignore errors when closing a broken connection
54+
}
55+
// Use DriverError so the driver treats this as a failure
56+
throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Cluster.NotALeader', $timeoutMsg)], $e);
57+
} elseif ($this->isSocketException($e)) {
58+
// Handle socket errors (broken pipe, connection reset, etc.)
59+
try {
60+
$this->connection->close();
61+
} catch (Throwable) {
62+
// Ignore errors when closing a broken connection
63+
}
64+
throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Cluster.NotALeader', 'Connection error: '.$e->getMessage())], $e);
65+
}
66+
67+
throw $e;
68+
}
3569

3670
$this->connection->assertNoFailure($response);
3771

3872
return $response;
3973
}
4074

75+
private function isTimeoutException(Throwable $e): bool
76+
{
77+
$message = strtolower($e->getMessage());
78+
79+
return str_contains($message, 'timeout') || str_contains($message, 'time out');
80+
}
81+
82+
private function isSocketException(Throwable $e): bool
83+
{
84+
$message = strtolower($e->getMessage());
85+
86+
return str_contains($message, 'broken pipe')
87+
|| str_contains($message, 'connection reset')
88+
|| str_contains($message, 'connection refused')
89+
|| str_contains($message, 'interrupted system call')
90+
|| str_contains($message, 'i/o error');
91+
}
92+
4193
/**
4294
* @return Iterator<Response>
4395
*/

testkit-backend/testkit.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ python3 -m unittest -v \
139139
tests.stub.connectivity_check.test_get_server_info.TestGetServerInfo.test_routing_no_server \
140140
tests.stub.connectivity_check.test_get_server_info.TestGetServerInfo.test_routing_raises_error \
141141
tests.stub.connectivity_check.test_get_server_info.TestGetServerInfo.test_routing \
142+
\
143+
tests.stub.configuration_hints.test_connection_recv_timeout_seconds.TestDirectConnectionRecvTimeout.test_in_time \
144+
tests.stub.configuration_hints.test_connection_recv_timeout_seconds.TestDirectConnectionRecvTimeout.test_timeout \
145+
tests.stub.configuration_hints.test_connection_recv_timeout_seconds.TestDirectConnectionRecvTimeout.test_timeout_unmanaged_tx \
146+
tests.stub.configuration_hints.test_connection_recv_timeout_seconds.TestDirectConnectionRecvTimeout.test_timeout_unmanaged_tx_should_fail_subsequent_usage_after_timeout \
147+
tests.stub.configuration_hints.test_connection_recv_timeout_seconds.TestDirectConnectionRecvTimeout.test_in_time_unmanaged_tx \
148+
tests.stub.configuration_hints.test_connection_recv_timeout_seconds.TestDirectConnectionRecvTimeout.test_in_time_managed_tx_retry \
142149

143150
EXIT_CODE=$?
144151

0 commit comments

Comments
 (0)