Skip to content

Commit b24db97

Browse files
committed
feat: remove custom port allocators and add clean up on shutdown
1 parent 9e3f558 commit b24db97

20 files changed

Lines changed: 219 additions & 617 deletions

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Or run everything:
3737
composer test
3838
```
3939

40+
4041
## Pre-commit hook
4142

4243
The pre-commit hook runs these checks (in this order):

src/Autoload.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
require_once __DIR__.'/Foundation/Bootstrap/Helpers.php';
88
require_once __DIR__.'/Foundation/Bootstrap/Plugin.php';
99
require_once __DIR__.'/Foundation/Bootstrap/Expectations.php';
10+
require_once __DIR__.'/Foundation/Bootstrap/Cleanup.php';
1011

1112
registerPluginUses();
1213
registerStorageExpectations();
14+
registerManagedContainersShutdownCleanup();

src/Container/ContainerBuilder.php

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,10 @@
66

77
use Closure;
88
use Eznix86\PestPluginTestContainers\Container\PortMapping\FixedPortSequenceGenerator;
9-
use Eznix86\PestPluginTestContainers\Container\PortMapping\PortAllocator;
10-
use Eznix86\PestPluginTestContainers\Container\PortMapping\ProtocolAwareRandomUniquePortAllocator;
11-
use Eznix86\PestPluginTestContainers\Container\PortMapping\SaferRandomUniquePortGenerator;
12-
use Eznix86\PestPluginTestContainers\Container\PortMapping\WorkerPortAllocator;
13-
use Eznix86\PestPluginTestContainers\Container\PortMapping\WorkerPortGenerator;
149
use Eznix86\PestPluginTestContainers\Container\Reuse\ReusableContainerResolver;
1510
use Eznix86\PestPluginTestContainers\Container\Reuse\ReuseOptions;
1611
use Eznix86\PestPluginTestContainers\Container\Reuse\WorkerTokenResolver;
1712
use InvalidArgumentException;
18-
use RuntimeException;
1913
use Testcontainers\Container\GenericContainer;
2014
use Testcontainers\Container\HttpMethod;
2115
use Testcontainers\ContainerClient\DockerContainerClient;
@@ -26,23 +20,18 @@
2620

2721
final readonly class ContainerBuilder
2822
{
23+
private const string MANAGED_BY_LABEL_KEY = 'pest-plugin-testcontainers.managed';
24+
25+
private const string MANAGED_BY_LABEL_VALUE = '1';
26+
2927
private const int START_MAX_ATTEMPTS = 6;
3028

3129
private const int START_RETRY_BASE_DELAY_MICROSECONDS = 500_000;
3230

3331
private const int START_RETRY_MAX_DELAY_MICROSECONDS = 5_000_000;
3432

35-
/**
36-
* @var list<string>
37-
*/
38-
private const array TRANSIENT_DOCKER_START_ERROR_MARKERS = [
39-
'server error',
40-
];
41-
4233
private GenericContainer $container;
4334

44-
private PortAllocator $portAllocator;
45-
4635
private ReuseOptions $reuseOptions;
4736

4837
private ReusableContainerResolver $reusableContainerResolver;
@@ -68,15 +57,10 @@ public function __construct(
6857
callable $registerContainer,
6958
callable $skipTest,
7059
) {
71-
$this->container = new GenericContainer($image);
72-
73-
if ($this->isRunningInParallel()) {
74-
$this->container->withPortGenerator(new WorkerPortGenerator);
75-
$this->portAllocator = new WorkerPortAllocator;
76-
} else {
77-
$this->container->withPortGenerator(new SaferRandomUniquePortGenerator);
78-
$this->portAllocator = new ProtocolAwareRandomUniquePortAllocator;
79-
}
60+
$this->container = (new GenericContainer($image))
61+
->withLabels([
62+
self::MANAGED_BY_LABEL_KEY => self::MANAGED_BY_LABEL_VALUE,
63+
]);
8064

8165
$this->reuseOptions = new ReuseOptions;
8266
$this->reusableContainerResolver = new ReusableContainerResolver;
@@ -85,11 +69,6 @@ public function __construct(
8569
$this->skipTest = Closure::fromCallable($skipTest);
8670
}
8771

88-
private function isRunningInParallel(): bool
89-
{
90-
return ($_SERVER['PEST_PARALLEL'] ?? $_ENV['PEST_PARALLEL'] ?? '0') === '1';
91-
}
92-
9372
/**
9473
* @param list<int|string>|array<int|string, int|string> $ports
9574
*/
@@ -98,10 +77,8 @@ public function ports(array $ports): self
9877
if (array_is_list($ports)) {
9978
/** @var list<int|string> $containerPorts */
10079
$containerPorts = array_map($this->normalizeContainerPort(...), $ports);
101-
/** @var list<int> $hostPorts */
102-
$hostPorts = array_map($this->portAllocator->allocateForContainerPort(...), $containerPorts);
10380

104-
$this->configurePortMapping($containerPorts, $hostPorts);
81+
$this->container->withExposedPorts(...$containerPorts);
10582

10683
return $this;
10784
}
@@ -329,9 +306,18 @@ public function start(): StartedContainer
329306
if ($reusedContainer instanceof StartedContainer) {
330307
return ($this->registerContainer)($reusedContainer);
331308
}
309+
310+
try {
311+
$recreatedContainer = $this->startContainerWithRetry();
312+
$recreatedContainer->skipAutoCleanup();
313+
314+
return ($this->registerContainer)($recreatedContainer);
315+
} catch (Throwable $recreateException) {
316+
$exception = $recreateException;
317+
}
332318
}
333319

334-
($this->skipTest)('Container startup issue: '.$exception->getMessage());
320+
($this->skipTest)('Container startup issue: '.$this->describeThrowable($exception));
335321
}
336322
}
337323

@@ -343,38 +329,54 @@ private function startContainerWithRetry(): StartedContainer
343329
try {
344330
return new StartedContainer($this->container->start());
345331
} catch (Throwable $exception) {
346-
if (! $this->isTransientDockerStartError($exception)) {
347-
throw $exception;
348-
}
349-
350-
$this->cleanupFailedContainerAfterTransientStartError();
332+
$this->cleanupFailedContainerAfterStartError();
351333
$lastException = $exception;
352334

353335
if ($attempt < self::START_MAX_ATTEMPTS - 1) {
354336
usleep($this->startRetryDelayForAttempt($attempt));
355337
}
356338
}
357339
}
358-
359-
throw new RuntimeException(
360-
'Container failed to start after retrying transient Docker errors.',
361-
previous: $lastException,
362-
);
340+
throw $lastException;
363341
}
364342

365-
private function isTransientDockerStartError(Throwable $exception): bool
343+
private function describeThrowable(Throwable $exception): string
366344
{
345+
$descriptions = [];
346+
367347
for ($current = $exception; $current instanceof Throwable; $current = $current->getPrevious()) {
368-
$message = strtolower($current->getMessage());
348+
$message = trim($current->getMessage());
349+
350+
if (method_exists($current, 'getErrorResponse')) {
351+
$errorResponse = $current->getErrorResponse();
352+
353+
if (is_object($errorResponse) && method_exists($errorResponse, 'getMessage')) {
354+
$dockerMessage = $errorResponse->getMessage();
369355

370-
foreach (self::TRANSIENT_DOCKER_START_ERROR_MARKERS as $marker) {
371-
if (str_contains($message, $marker)) {
372-
return true;
356+
if (is_string($dockerMessage) && $dockerMessage !== '' && ! str_contains($message, $dockerMessage)) {
357+
$message = trim($message.' | docker: '.$dockerMessage);
358+
}
373359
}
374360
}
361+
362+
if (method_exists($current, 'getResponse')) {
363+
$response = $current->getResponse();
364+
365+
if (is_object($response) && method_exists($response, 'getStatusCode')) {
366+
$statusCode = $response->getStatusCode();
367+
$reason = method_exists($response, 'getReasonPhrase') ? $response->getReasonPhrase() : '';
368+
369+
if (is_int($statusCode)) {
370+
$httpPart = sprintf('HTTP %d%s', $statusCode, is_string($reason) && $reason !== '' ? ' '.$reason : '');
371+
$message = trim($message.' | '.$httpPart);
372+
}
373+
}
374+
}
375+
376+
$descriptions[] = sprintf('%s: %s', $current::class, $message === '' ? '(empty message)' : $message);
375377
}
376378

377-
return false;
379+
return implode(' <- ', $descriptions);
378380
}
379381

380382
private function startRetryDelayForAttempt(int $attempt): int
@@ -384,7 +386,7 @@ private function startRetryDelayForAttempt(int $attempt): int
384386
return min($delay, self::START_RETRY_MAX_DELAY_MICROSECONDS);
385387
}
386388

387-
private function cleanupFailedContainerAfterTransientStartError(): void
389+
private function cleanupFailedContainerAfterStartError(): void
388390
{
389391
try {
390392
$containerId = $this->container->getId();

src/Container/PortMapping/PortAllocator.php

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/Container/PortMapping/PortAvailabilityChecker.php

Lines changed: 0 additions & 74 deletions
This file was deleted.

src/Container/PortMapping/ProtocolAwareRandomUniquePortAllocator.php

Lines changed: 0 additions & 60 deletions
This file was deleted.

src/Container/PortMapping/SaferRandomPortGenerator.php

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)