Skip to content

Commit 948be2b

Browse files
authored
feat/incompatible-with-neo4j-php/neo4j-php-client-3.4.0 (#98)
feat/incompatible-with-neo4j-php/neo4j-php-client-3.4.0: Created custom retry logic instead of relying on TransactionHelper
1 parent 94dbd25 commit 948be2b

File tree

9 files changed

+651
-49
lines changed

9 files changed

+651
-49
lines changed

phpunit.xml.dist

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phpunit
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4-
bootstrap="./vendor/autoload.php"
4+
bootstrap="./tests/bootstrap.php"
55
colors="true"
66
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
77
failOnWarning="true"
88
beStrictAboutOutputDuringTests="true"
9+
convertDeprecationsToExceptions="false"
910
>
1011
<testsuites>
12+
<testsuite name="Unit tests">
13+
<directory suffix="Test.php">./tests/Unit</directory>
14+
</testsuite>
1115
<testsuite name="Functional tests">
1216
<directory suffix="Test.php">./tests/Functional</directory>
1317
</testsuite>

src/Decorators/SymfonySession.php

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,33 @@
33
namespace Neo4j\Neo4jBundle\Decorators;
44

55
use Laudis\Neo4j\Basic\Session;
6-
use Laudis\Neo4j\Common\TransactionHelper;
6+
use Laudis\Neo4j\Contracts\ConnectionPoolInterface;
7+
use Laudis\Neo4j\Contracts\CypherSequence;
78
use Laudis\Neo4j\Contracts\SessionInterface;
89
use Laudis\Neo4j\Databags\Bookmark;
10+
use Laudis\Neo4j\Databags\SessionConfiguration;
911
use Laudis\Neo4j\Databags\Statement;
1012
use Laudis\Neo4j\Databags\SummarizedResult;
1113
use Laudis\Neo4j\Databags\TransactionConfiguration;
14+
use Laudis\Neo4j\Enum\AccessMode;
15+
use Laudis\Neo4j\Exception\Neo4jException;
1216
use Laudis\Neo4j\Types\CypherList;
1317
use Neo4j\Neo4jBundle\EventHandler;
1418
use Neo4j\Neo4jBundle\Factories\SymfonyDriverFactory;
1519

1620
final class SymfonySession implements SessionInterface
1721
{
22+
private const MAX_RETRIES = 3;
23+
private const ROLLBACK_CLASSIFICATIONS = ['ClientError', 'TransientError', 'DatabaseError'];
24+
1825
public function __construct(
1926
private readonly Session $session,
2027
private readonly EventHandler $handler,
2128
private readonly SymfonyDriverFactory $factory,
2229
private readonly string $alias,
2330
private readonly string $schema,
31+
private readonly SessionConfiguration $config,
32+
private readonly ConnectionPoolInterface $pool,
2433
) {
2534
}
2635

@@ -76,10 +85,7 @@ public function beginTransaction(?iterable $statements = null, ?TransactionConfi
7685
#[\Override]
7786
public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null)
7887
{
79-
return TransactionHelper::retry(
80-
fn () => $this->beginTransaction(config: $config),
81-
$tsxHandler
82-
);
88+
return $this->retryTransaction($tsxHandler, $config, read: false);
8389
}
8490

8591
/**
@@ -92,8 +98,7 @@ public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration
9298
#[\Override]
9399
public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null)
94100
{
95-
// TODO: create read transaction here.
96-
return $this->writeTransaction($tsxHandler, $config);
101+
return $this->retryTransaction($tsxHandler, $config, read: true);
97102
}
98103

99104
/**
@@ -114,4 +119,68 @@ public function getLastBookmark(): Bookmark
114119
{
115120
return $this->session->getLastBookmark();
116121
}
122+
123+
/**
124+
* Custom retry transaction logic to replace TransactionHelper.
125+
*
126+
* @template HandlerResult
127+
*
128+
* @param callable(SymfonyTransaction):HandlerResult $tsxHandler
129+
*
130+
* @return HandlerResult
131+
*/
132+
private function retryTransaction(callable $tsxHandler, ?TransactionConfiguration $config, bool $read)
133+
{
134+
$attempt = 0;
135+
136+
while (true) {
137+
++$attempt;
138+
$transaction = null;
139+
140+
try {
141+
$sessionConfig = $this->config->withAccessMode($read ? AccessMode::READ() : AccessMode::WRITE());
142+
$transaction = $this->startTransaction($config, $sessionConfig);
143+
144+
$result = $tsxHandler($transaction);
145+
146+
self::triggerLazyResult($result);
147+
$transaction->commit();
148+
149+
return $result;
150+
} catch (Neo4jException $e) {
151+
if ($transaction && !in_array($e->getClassification(), self::ROLLBACK_CLASSIFICATIONS)) {
152+
$transaction->rollback();
153+
}
154+
155+
if ('NotALeader' === $e->getTitle()) {
156+
$this->pool->close();
157+
} elseif ('TransientError' !== $e->getClassification()) {
158+
throw $e;
159+
}
160+
161+
if ($attempt >= self::MAX_RETRIES) {
162+
throw $e;
163+
}
164+
165+
usleep(100_000);
166+
}
167+
}
168+
}
169+
170+
private static function triggerLazyResult(mixed $tbr): void
171+
{
172+
if ($tbr instanceof CypherSequence) {
173+
$tbr->preload();
174+
}
175+
}
176+
177+
private function startTransaction(?TransactionConfiguration $config, SessionConfiguration $sessionConfig): SymfonyTransaction
178+
{
179+
return $this->factory->createTransaction(
180+
session: $this->session,
181+
config: $config,
182+
alias: $this->alias,
183+
schema: $this->schema
184+
);
185+
}
117186
}

src/DependencyInjection/Neo4jExtension.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use Neo4j\Neo4jBundle\EventHandler;
1212
use Neo4j\Neo4jBundle\EventListener\Neo4jProfileListener;
1313
use Psr\Log\LoggerInterface;
14-
use Psr\Log\LogLevel;
1514
use Symfony\Component\Config\FileLocator;
1615
use Symfony\Component\DependencyInjection\ContainerBuilder;
1716
use Symfony\Component\DependencyInjection\ContainerInterface;

src/Factories/SymfonyDriverFactory.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public function createSession(
5454
factory: $this,
5555
alias: $alias,
5656
schema: $schema,
57+
config: $config ?? new SessionConfiguration(),
58+
pool: $this->getPoolFromDriver($driver),
5759
);
5860
}
5961

@@ -70,6 +72,21 @@ public function createDriver(
7072
);
7173
}
7274

75+
private function getPoolFromDriver(Driver $driver): \Laudis\Neo4j\Contracts\ConnectionPoolInterface
76+
{
77+
// Use reflection to access the private pool property from the underlying driver
78+
$reflection = new \ReflectionClass($driver);
79+
$driverProperty = $reflection->getProperty('driver');
80+
$driverProperty->setAccessible(true);
81+
$underlyingDriver = $driverProperty->getValue($driver);
82+
83+
$underlyingReflection = new \ReflectionClass($underlyingDriver);
84+
$poolProperty = $underlyingReflection->getProperty('pool');
85+
$poolProperty->setAccessible(true);
86+
87+
return $poolProperty->getValue($underlyingDriver);
88+
}
89+
7390
private function generateTransactionId(): string
7491
{
7592
if ($this->uuidFactory) {

tests/App/Controller/TestController.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public function __construct(private readonly LoggerInterface $logger)
1414
{
1515
}
1616

17+
public function index(): Response
18+
{
19+
return $this->render('index.html.twig');
20+
}
21+
1722
public function runOnClient(ClientInterface $client): Response
1823
{
1924
$client->run('MATCH (n {foo: $bar}) RETURN n', ['bar' => 'baz']);

tests/App/config/routes.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
index:
2+
path: /
3+
controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController::index
14
run-on-client:
25
path: /client
36
controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController::runOnClient
@@ -9,8 +12,8 @@ run-on-transaction:
912
controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController::runOnTransaction
1013

1114
web_profiler_wdt:
12-
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
15+
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
1316
prefix: /_wdt
1417
web_profiler_profiler:
15-
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
18+
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
1619
prefix: /_profiler4

0 commit comments

Comments
 (0)