Lines 92.50% 37 / 40
Methods 87.50% 14 / 16
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 isLive 100.00% 1 / 1 100.00% 1 / 1 2
 isSpawning 0.00% 0 / 1 0.00% 0 / 1 2
 isStopping 100.00% 1 / 1 100.00% 1 / 1 1
 getOutboxStatus 0.00% 0 / 2 0.00% 0 / 1 6
 beginSpawn 100.00% 2 / 2 100.00% 1 / 1 1
 markSpawnFailed 100.00% 1 / 1 100.00% 1 / 1 1
 recordSpawnedHandle 100.00% 7 / 7 100.00% 1 / 1 2
 markGone 100.00% 2 / 2 100.00% 1 / 1 1
 beginStopping 100.00% 4 / 4 100.00% 1 / 1 1
 recordScheduledRestart 100.00% 4 / 4 100.00% 1 / 1 1
 markQuarantined 100.00% 2 / 2 100.00% 1 / 1 1
 stop 100.00% 4 / 4 100.00% 1 / 1 2
 cancelReadyDeadline 100.00% 2 / 2 100.00% 1 / 1 1
 moveTo 100.00% 3 / 3 100.00% 1 / 1 4
 cancelRestart 100.00% 3 / 3 100.00% 1 / 1 1
13final class WorkerSlotState
14{
15    public private(set) WorkerPhase $phase = WorkerPhase::Stopped;
16
17    public private(set) ?WorkerHandle $handle = null;
18
19    private ?TimerHandle $readyDeadline = null;
20
21    private ?TimerHandle $restartTimer = null;
22
23    public private(set) ?Instant $restartDueAt = null;
24
25    public private(set) int $restarts = 0;
26
27    public private(set) int $quarantines = 0;
28
29    private OutboxStatus $retiredOutboxes;
30
31    public function __construct(
32        public readonly WorkerSlot $slot,
33        public readonly WorkerPoolListener $listener,
34    ) {
35        $this->retiredOutboxes = OutboxStatus::zero();
36    }
37
38    public function isLive(WorkerHandle $handle): bool
39    {
40        return $this->phase === WorkerPhase::Live && $this->handle === $handle;
41    }
42
43    public function isSpawning(): bool
44    {
45        return $this->phase === WorkerPhase::Spawning;
46    }
47
48    public function isStopping(): bool
49    {
50        return $this->phase === WorkerPhase::Stopping;
51    }
52
53    public function getOutboxStatus(): OutboxStatus
54    {
55        $total = $this->retiredOutboxes->plus($this->handle?->getOutboxStatus() ?? OutboxStatus::zero());
56
57        return $this->phase === WorkerPhase::Live ? $total : $total->withoutQueued();
58    }
59
60    public function beginSpawn(): void
61    {
62        $this->moveTo(WorkerPhase::Spawning);
63        $this->cancelRestart();
64    }
65
66    public function markSpawnFailed(): void
67    {
68        $this->moveTo(WorkerPhase::Stopped, from: WorkerPhase::Spawning);
69    }
70
71    public function recordSpawnedHandle(WorkerHandle $handle, ?TimerHandle $readyDeadline): ?WorkerHandle
72    {
73        $this->moveTo(WorkerPhase::Live);
74
75        $previous = $this->handle;
76
77        if ($previous !== null) {
78            $this->retiredOutboxes = $this->retiredOutboxes->plus($previous->getOutboxStatus()->withoutQueued());
79        }
80
81        $this->handle = $handle;
82        $this->readyDeadline = $readyDeadline;
83
84        return $previous;
85    }
86
87    public function markGone(): void
88    {
89        $this->moveTo(WorkerPhase::Stopped, from: WorkerPhase::Live);
90        $this->cancelReadyDeadline();
91    }
92
93    public function beginStopping(): WorkerHandle
94    {
95        $this->moveTo(WorkerPhase::Stopping);
96        $this->cancelReadyDeadline();
97        ++$this->restarts;
98
99        return $this->handle ?? throw new LogicException(\sprintf('Worker %d is stopping without a process.', $this->slot->workerId->value));
100    }
101
102    public function recordScheduledRestart(TimerHandle $timer, Instant $dueAt): void
103    {
104        $this->moveTo(WorkerPhase::RestartScheduled);
105        $this->restartTimer = $timer;
106        $this->restartDueAt = $dueAt;
107        ++$this->restarts;
108    }
109
110    public function markQuarantined(): void
111    {
112        $this->moveTo(WorkerPhase::Quarantined);
113        ++$this->quarantines;
114    }
115
116    public function stop(): void
117    {
118        if ($this->phase !== WorkerPhase::Stopped) {
119            $this->moveTo(WorkerPhase::Stopped);
120        }
121
122        $this->cancelRestart();
123        $this->cancelReadyDeadline();
124    }
125
126    public function cancelReadyDeadline(): void
127    {
128        $this->readyDeadline?->cancel();
129        $this->readyDeadline = null;
130    }
131
132    private function moveTo(WorkerPhase $next, ?WorkerPhase $from = null): void
133    {
134        if (!$this->phase->canEnter($next) || ($from !== null && $this->phase !== $from)) {
135            throw new LogicException(\sprintf('Worker %d cannot move from %s to %s.', $this->slot->workerId->value, $this->phase->value, $next->value));
136        }
137
138        $this->phase = $next;
139    }
140
141    private function cancelRestart(): void
142    {
143        $this->restartTimer?->cancel();
144        $this->restartTimer = null;
145        $this->restartDueAt = null;
146    }
147}