Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions lib/Exceptions/CoordinatedOperationCleanupFailedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace PHPNomad\Database\Exceptions;

use Throwable;

/** An operation failed and its cleanup could not establish a safe outcome. */
class CoordinatedOperationCleanupFailedException extends CoordinatedOperationOutcomeUnknownException
{
private Throwable $operationFailure;

/** Retain the cleanup failure as previous and the earlier operation failure separately. */
public function __construct(Throwable $operationFailure, Throwable $cleanupFailure)
{
$this->operationFailure = $operationFailure;

parent::__construct(
'The coordinated operation failed and cleanup could not be confirmed.',
0,
$cleanupFailure
);
}

/** Return the exact earlier failure that required cleanup. */
public function getOperationFailure(): Throwable
{
return $this->operationFailure;
}
}
50 changes: 50 additions & 0 deletions tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace PHPNomad\Database\Tests\Unit\Contracts;

use Error;
use PHPNomad\Database\Exceptions\CoordinatedOperationCleanupFailedException;
use PHPNomad\Database\Exceptions\CoordinatedOperationConflictException;
use PHPNomad\Database\Exceptions\CoordinatedOperationOutcomeUnknownException;
use PHPNomad\Datastore\Exceptions\DatastoreErrorException;
use PHPNomad\Database\Tests\TestCase;
use RuntimeException;

/** Keeps both causes without depending on a storage adapter. Each adapter tests its cleanup behavior. */
final class CoordinationCleanupFailureContractTest extends TestCase
{
/** @dataProvider causeKinds */
public function testBothOriginalFailuresRemainInspectableAsAnUnknownOutcome(bool $operationIsError, bool $cleanupIsError): void
{
$operation = $operationIsError ? new Error('Private operation detail') : new RuntimeException('Private operation detail');
$cleanup = $cleanupIsError ? new Error('Private cleanup detail') : new RuntimeException('Private cleanup detail');
$failure = new CoordinatedOperationCleanupFailedException($operation, $cleanup);

self::assertInstanceOf(CoordinatedOperationOutcomeUnknownException::class, $failure);
self::assertInstanceOf(DatastoreErrorException::class, $failure);
self::assertNotInstanceOf(CoordinatedOperationConflictException::class, $failure);
self::assertSame($operation, $failure->getOperationFailure());
self::assertSame($cleanup, $failure->getPrevious());
self::assertSame('The coordinated operation failed and cleanup could not be confirmed.', $failure->getMessage());
self::assertSame(0, $failure->getCode());
self::assertNull($operation->getPrevious());
self::assertNull($cleanup->getPrevious());

$otherOperation = new RuntimeException('Another operation');
$otherCleanup = new RuntimeException('Another cleanup');
$other = new CoordinatedOperationCleanupFailedException($otherOperation, $otherCleanup);
self::assertSame($otherOperation, $other->getOperationFailure());
self::assertSame($otherCleanup, $other->getPrevious());
self::assertSame($operation, $failure->getOperationFailure());
self::assertSame($cleanup, $failure->getPrevious());
}

/** @return array<string, array{bool, bool}> */
public static function causeKinds(): array
{
return [
'exceptions' => [false, false], 'operation error' => [true, false],
'cleanup error' => [false, true], 'both errors' => [true, true],
];
}
}
Loading