Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 17 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
| RacingActionRequestStore | |
0.00% |
0 / 17 |
|
0.00% |
0 / 8 |
156 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| arm | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| transition | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
| save | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| auditTrail | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| pendingChanges | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| accepted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace LambdaTwelve\OneRecord\Testing; |
| 6 | |
| 7 | use Closure; |
| 8 | use LambdaTwelve\OneRecord\Api\ActionRequest; |
| 9 | use LambdaTwelve\OneRecord\Api\ActionRequestType; |
| 10 | use LambdaTwelve\OneRecord\Api\RequestStatus; |
| 11 | use LambdaTwelve\OneRecord\Rdf\Iri; |
| 12 | use LambdaTwelve\OneRecord\Server\Spi\ActionRequestStore; |
| 13 | use LambdaTwelve\OneRecord\Server\Spi\AuditTrailQuery; |
| 14 | use LambdaTwelve\OneRecord\Server\Spi\StoreException; |
| 15 | use LambdaTwelve\OneRecord\Server\Spi\Volatile; |
| 16 | |
| 17 | /** |
| 18 | * Stages the race every host must prove it survives: another worker decides |
| 19 | * the same action request first. Decorates any ActionRequestStore; arm it with |
| 20 | * a request and the next transition() of that request either throws the |
| 21 | * status conflict outright, or first runs your callback (flip your own row |
| 22 | * to the competing status there) and then lets the real compare-and-set lose |
| 23 | * on its own. Fires once, then behaves like the store it wraps. |
| 24 | */ |
| 25 | final class RacingActionRequestStore implements ActionRequestStore, Volatile |
| 26 | { |
| 27 | private ?Iri $armed = null; |
| 28 | |
| 29 | /** @var ?Closure(ActionRequest): void */ |
| 30 | private ?Closure $before = null; |
| 31 | |
| 32 | public int $racesLost = 0; |
| 33 | |
| 34 | public function __construct(private readonly ActionRequestStore $inner) {} |
| 35 | |
| 36 | /** |
| 37 | * @param ?callable(ActionRequest): void $before what the competing worker did; null throws the conflict directly |
| 38 | */ |
| 39 | public function arm(Iri $request, ?callable $before = null): void |
| 40 | { |
| 41 | $this->armed = $request; |
| 42 | $this->before = $before === null ? null : $before(...); |
| 43 | } |
| 44 | |
| 45 | public function transition(ActionRequest $request, RequestStatus $expectedCurrent): void |
| 46 | { |
| 47 | if ($this->armed !== null && $this->armed->equals($request->iri)) { |
| 48 | $before = $this->before; |
| 49 | $this->armed = null; |
| 50 | $this->before = null; |
| 51 | $this->racesLost++; |
| 52 | if ($before === null) { |
| 53 | throw StoreException::statusConflict($request->iri, $expectedCurrent->shortName(), 'another worker decided first'); |
| 54 | } |
| 55 | $before($request); |
| 56 | } |
| 57 | $this->inner->transition($request, $expectedCurrent); |
| 58 | } |
| 59 | |
| 60 | public function save(ActionRequest $request): void |
| 61 | { |
| 62 | $this->inner->save($request); |
| 63 | } |
| 64 | |
| 65 | public function get(Iri $iri): ?ActionRequest |
| 66 | { |
| 67 | return $this->inner->get($iri); |
| 68 | } |
| 69 | |
| 70 | public function auditTrail(Iri $logisticsObject, AuditTrailQuery $query): array |
| 71 | { |
| 72 | return $this->inner->auditTrail($logisticsObject, $query); |
| 73 | } |
| 74 | |
| 75 | public function pendingChanges(Iri $logisticsObject): array |
| 76 | { |
| 77 | return $this->inner->pendingChanges($logisticsObject); |
| 78 | } |
| 79 | |
| 80 | public function accepted(ActionRequestType $type): array |
| 81 | { |
| 82 | return $this->inner->accepted($type); |
| 83 | } |
| 84 | } |