Lines 88.88% 32 / 36
Functions and Methods 60.00% 3 / 5
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
BrokerLifecycle 88.88% 32 / 36 60.00% 3 / 5 13.23 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 run 100.00% 26 / 26 100.00% 1 / 1 5
 startHttpListeners 100.00% 2 / 2 100.00% 1 / 1 2
 stopHttpListeners 50.00% 2 / 4 0.00% 0 / 1 4.12
 stopControlPlane 33.33% 1 / 3 0.00% 0 / 1 3.19
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Runtime\Broker;
6
7use Psr\Log\LoggerInterface;
8use Stewart\Runtime\Broker\Deploy\ReleasePoller;
9use Stewart\Runtime\Broker\Http\Collection\HttpListenerCollection;
10use Stewart\Runtime\Broker\Mqtt\MqttLink;
11use Stewart\Runtime\Broker\Mqtt\MqttMessageRouter;
12use Stewart\Runtime\Lifecycle\BrokerStopOutcome;
13use Stewart\Runtime\Time\ProcessTimeZone;
14use Stewart\Store\StoreBackend;
15use Throwable;
16
17final readonly class BrokerLifecycle
18{
19    public function __construct(
20        private HaSession $session,
21        private BrokerHaEvents $haEvents,
22        private BrokerRun $run,
23        private SignalHandlers $signals,
24        private LoopErrorLogger $loopErrors,
25        private WorkerStartup $workers,
26        private GeneratedCodeDrift $drift,
27        private ConnectionTracker $connection,
28        private ProcessTimeZone $processTimeZone,
29        private ControlPlane $control,
30        private HttpListenerCollection $httpListeners,
31        private MqttLink $mqtt,
32        private MqttMessageRouter $mqttRouter,
33        private AppPauseService $pauses,
34        private ReleasePoller $releases,
35        private LoggerInterface $logger,
36        private ?StoreBackend $store = null,
37    ) {}
38
39    /** @throws Throwable */
40    public function run(): BrokerStopOutcome
41    {
42        $this->loopErrors->install();
43        $this->run->start();
44        $this->signals->install();
45
46        try {
47            // Before Home Assistant: an unreachable store is a config error, not an outage.
48            $this->store?->probe();
49            $this->pauses->restoreStoredOverrides();
50
51            // A stop can land while the previous step suspends; it has already closed what would start here.
52            if ($this->run->isRunning()) {
53                $this->control->start();
54                $this->startHttpListeners();
55                $this->mqtt->startInBackground($this->mqttRouter);
56            }
57
58            if ($this->run->isRunning()) {
59                $this->session->open($this->haEvents);
60            }
61
62            if ($this->run->isRunning()) {
63                $this->processTimeZone->useAsProcessDefault($this->session->getTimeZone());
64                $this->connection->markConnected();
65                $this->drift->warnIfGeneratedCodeDrifted();
66                $this->workers->startWorkers();
67                $this->releases->startPolling();
68            }
69
70            return $this->run->awaitStopped();
71        } catch (Throwable $e) {
72            $this->run->stop('fatal error');
73
74            throw $e;
75        } finally {
76            $this->releases->stopPolling();
77            $this->stopHttpListeners();
78            $this->stopControlPlane();
79            $this->signals->removeAll();
80            $this->loopErrors->restorePrevious();
81        }
82    }
83
84    /** @throws Throwable */
85    private function startHttpListeners(): void
86    {
87        foreach ($this->httpListeners as $listener) {
88            $listener->start();
89        }
90    }
91
92    private function stopHttpListeners(): void
93    {
94        foreach ($this->httpListeners as $listener) {
95            try {
96                $listener->stop();
97            } catch (Throwable $e) {
98                $this->logger->error('Could not stop an HTTP listener while shutting down', ['exception' => $e]);
99            }
100        }
101    }
102
103    private function stopControlPlane(): void
104    {
105        try {
106            $this->control->stop();
107        } catch (Throwable $e) {
108            $this->logger->error('Could not stop the control plane while shutting down', ['exception' => $e]);
109        }
110    }
111}