Lines 93.33% 28 / 30
Methods 88.88% 8 / 9
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 2 / 2 100.00% 1 / 1 1
 trackStartup 100.00% 1 / 1 100.00% 1 / 1 1
 startupCancellation 100.00% 1 / 1 100.00% 1 / 1 1
 isStopping 100.00% 1 / 1 100.00% 1 / 1 1
 awaitStopped 100.00% 1 / 1 100.00% 1 / 1 1
 stopWithConfiguredGrace 100.00% 1 / 1 100.00% 1 / 1 1
 stopAfterBrokerLoss 100.00% 2 / 2 100.00% 1 / 1 1
 stop 100.00% 14 / 14 100.00% 1 / 1 3
 stopAppsBefore 71.42% 5 / 7 0.00% 0 / 1 3.21
20final class WorkerShutdown
21{
22    public private(set) ?string $reason = null;
23
24    /** @var DeferredFuture<null> */
25    private readonly DeferredFuture $stopped;
26
27    private readonly DeferredCancellation $halt;
28
29    /** @var Future<mixed>|null */
30    private ?Future $startup = null;
31
32    public function __construct(
33        private readonly AppLifecycle $apps,
34        private readonly AppResources $resources,
35        private readonly PendingRequests $pending,
36        private readonly Deadlines $deadlines,
37        private readonly LoggerInterface $logger,
38        private readonly WorkerSettings $workerSettings,
39    ) {
40        $this->stopped = new DeferredFuture();
41        $this->halt = new DeferredCancellation();
42    }
43
44    /** @param Future<mixed> $startup */
45    public function trackStartup(Future $startup): void
46    {
47        $this->startup = $startup;
48    }
49
50    public function startupCancellation(): Cancellation
51    {
52        return $this->halt->getCancellation();
53    }
54
55    public function isStopping(): bool
56    {
57        return $this->reason !== null;
58    }
59
60    public function awaitStopped(): void
61    {
62        $this->stopped->getFuture()->await();
63    }
64
65    public function stopWithConfiguredGrace(string $reason): void
66    {
67        $this->stop($reason, $this->workerSettings->shutdownGrace);
68    }
69
70    public function stopAfterBrokerLoss(string $reason): void
71    {
72        $this->pending->failAll('the broker is gone');
73        $this->stopWithConfiguredGrace($reason);
74    }
75
76    public function stop(string $reason, Duration $grace): void
77    {
78        if ($this->reason !== null) {
79            return;
80        }
81
82        $this->reason = $reason;
83        $this->apps->markStopping();
84        $this->halt->cancel();
85        $deadline = $this->deadlines->timeout($grace);
86
87        async(function () use ($deadline, $grace): void {
88            $inTime = $this->stopAppsBefore($deadline);
89            $abandoned = $this->apps->abandonUnfinishedApps();
90            $this->resources->releaseAll();
91
92            if (!$inTime) {
93                $this->logger->warning('Apps did not stop within the grace period', ['grace' => (string) $grace, 'apps' => $abandoned->toStrings()]);
94            }
95
96            $this->stopped->complete();
97        })->ignore();
98    }
99
100    private function stopAppsBefore(Cancellation $deadline): bool
101    {
102        try {
103            $this->apps->disposeAppsBefore($deadline);
104            // Apps still in initialize() dispose themselves when it returns.
105            $this->startup?->await($deadline);
106        } catch (CancelledException) {
107            return false;
108        } catch (Throwable $e) {
109            $this->logger->error('Worker failed while stopping its apps', ['exception' => $e]);
110        }
111
112        return true;
113    }
114}