Skip to content

Commit 05d27a9

Browse files
Add retry mechanism to use method for connection handling with tests
1 parent a70164f commit 05d27a9

File tree

2 files changed

+150
-9
lines changed

2 files changed

+150
-9
lines changed

src/Pools/Pool.php

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,21 +219,37 @@ public function setTelemetry(Telemetry $telemetry): static
219219
*
220220
* @template T
221221
* @param callable(TResource): T $callback Function that receives the connection resource
222+
* @param int $retries Number of retry attempts if the callback fails (default: 0)
222223
* @return T Return value from the callback
224+
* @throws \Throwable If all retry attempts fail, throws the last exception
223225
*/
224-
public function use(callable $callback): mixed
226+
public function use(callable $callback, int $retries = 0): mixed
225227
{
226-
$start = microtime(true);
227-
$connection = null;
228-
try {
229-
$connection = $this->pop();
230-
return $callback($connection->getResource());
231-
} finally {
232-
if ($connection !== null) {
233-
$this->telemetryUseDuration->record(microtime(true) - $start, $this->telemetryAttributes);
228+
$lastException = null;
229+
230+
for ($attempt = 0; $attempt <= $retries; $attempt++) {
231+
$start = microtime(true);
232+
$connection = null;
233+
234+
try {
235+
$connection = $this->pop();
236+
$result = $callback($connection->getResource());
234237
$this->reclaim($connection);
238+
$this->telemetryUseDuration->record(microtime(true) - $start, $this->telemetryAttributes);
239+
return $result;
240+
} catch (\Throwable $e) {
241+
if ($connection !== null) {
242+
$this->destroy($connection);
243+
}
244+
$this->telemetryUseDuration->record(microtime(true) - $start, $this->telemetryAttributes);
245+
$lastException = $e;
246+
if ($attempt === $retries) {
247+
throw $lastException;
248+
}
235249
}
236250
}
251+
252+
throw $lastException;
237253
}
238254

239255
/**

tests/Pools/Scopes/PoolTestScope.php

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,4 +367,129 @@ public function testPoolTelemetry(): void
367367
});
368368
});
369369
}
370+
371+
public function testPoolUseWithRetrySuccess(): void
372+
{
373+
$this->execute(function (): void {
374+
$i = 0;
375+
$pool = new Pool($this->getAdapter(), 'testRetry', 2, function () use (&$i) {
376+
$i++;
377+
return "connection-{$i}";
378+
});
379+
380+
$attempts = 0;
381+
$result = $pool->use(function ($resource) use (&$attempts) {
382+
$attempts++;
383+
384+
// Fail on first two attempts, succeed on third
385+
if ($attempts < 3) {
386+
throw new Exception("Simulated connection failure");
387+
}
388+
389+
return "success: {$resource}";
390+
}, 3); // Allow up to 3 retries (4 total attempts)
391+
392+
$this->assertEquals(3, $attempts);
393+
$this->assertEquals("success: connection-3", $result);
394+
395+
// Pool should have connections available (destroyed failed ones, created new)
396+
$this->assertGreaterThan(0, $pool->count());
397+
});
398+
399+
$this->execute(function (): void {
400+
$pool = new Pool($this->getAdapter(), 'testIntermittent', 5, fn () => 'resource');
401+
402+
$callCount = 0;
403+
404+
$result = $pool->use(function ($resource) use (&$callCount) {
405+
$callCount++;
406+
407+
// Fail on odd attempts, succeed on even
408+
if ($callCount % 2 === 1) {
409+
throw new Exception("Odd attempt failure");
410+
}
411+
412+
return "success on attempt {$callCount}";
413+
}, 5); // Allow 5 retries
414+
415+
$this->assertEquals("success on attempt 2", $result);
416+
$this->assertEquals(2, $callCount); // Should succeed on second attempt
417+
});
418+
}
419+
420+
public function testPoolUseWithRetryFailure(): void
421+
{
422+
$this->execute(function (): void {
423+
$pool = new Pool($this->getAdapter(), 'testRetryFail', 3, fn () => 'x');
424+
425+
$attempts = 0;
426+
427+
try {
428+
$pool->use(function ($resource) use (&$attempts) {
429+
$attempts++;
430+
throw new Exception("Persistent failure");
431+
}, 2); // Allow up to 2 retries (3 total attempts)
432+
433+
$this->fail('Expected exception was not thrown');
434+
} catch (Exception $e) {
435+
$this->assertEquals("Persistent failure", $e->getMessage());
436+
$this->assertEquals(3, $attempts); // Should have tried 3 times (initial + 2 retries)
437+
}
438+
});
439+
}
440+
441+
public function testPoolUseWithoutRetry(): void
442+
{
443+
$this->execute(function (): void {
444+
$pool = new Pool($this->getAdapter(), 'testNoRetry', 2, fn () => 'x');
445+
446+
$attempts = 0;
447+
448+
try {
449+
$pool->use(function ($resource) use (&$attempts) {
450+
$attempts++;
451+
throw new Exception("First attempt failure");
452+
}); // No retries (default)
453+
454+
$this->fail('Expected exception was not thrown');
455+
} catch (Exception $e) {
456+
$this->assertEquals("First attempt failure", $e->getMessage());
457+
$this->assertEquals(1, $attempts); // Should only try once
458+
}
459+
});
460+
}
461+
462+
public function testPoolUseRetryDestroysFailedConnections(): void
463+
{
464+
$this->execute(function (): void {
465+
$i = 0;
466+
$pool = new Pool($this->getAdapter(), 'testDestroyOnRetry', 3, function () use (&$i) {
467+
$i++;
468+
return "connection-{$i}";
469+
});
470+
471+
$attempts = 0;
472+
$seenResources = [];
473+
474+
$pool->use(function ($resource) use (&$attempts, &$seenResources) {
475+
$attempts++;
476+
$seenResources[] = $resource;
477+
478+
// Fail twice, succeed on third
479+
if ($attempts < 3) {
480+
throw new Exception("Connection failed");
481+
}
482+
483+
return "success";
484+
}, 3);
485+
486+
// Should have created 3 connections (one for each attempt)
487+
$this->assertEquals(3, $i);
488+
$this->assertEquals(3, $attempts);
489+
490+
// Each attempt should have gotten a different connection (failed ones were destroyed)
491+
$this->assertCount(3, array_unique($seenResources));
492+
$this->assertEquals(['connection-1', 'connection-2', 'connection-3'], $seenResources);
493+
});
494+
}
370495
}

0 commit comments

Comments
 (0)