Lines 97.82% 45 / 46
Functions and Methods 80.00% 4 / 5
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
ReleasePoller 97.82% 45 / 46 80.00% 4 / 5 15 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 startPolling 100.00% 16 / 16 100.00% 1 / 1 4
 stopPolling 100.00% 4 / 4 100.00% 1 / 1 1
 pollForRelease 100.00% 11 / 11 100.00% 1 / 1 5
 deployLatestRelease 92.85% 13 / 14 0.00% 0 / 1 4.01
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Runtime\Broker\Deploy;
6
7use Amp\Cancellation;
8use Amp\CancelledException;
9use Amp\DeferredCancellation;
10use Psr\Log\LoggerInterface;
11use Stewart\Contracts\Time\Clock;
12use Stewart\Contracts\Time\Timers;
13use Stewart\Runtime\Broker\BrokerRun;
14use Stewart\Runtime\Config\GitDeployConfig;
15use Stewart\Runtime\Exception\DeployException;
16use Stewart\Runtime\Time\RepeatingTimer;
17use Throwable;
18
19use function Amp\async;
20
21final class ReleasePoller
22{
23    private ?RepeatingTimer $timer = null;
24
25    private ?DeferredCancellation $stop = null;
26
27    private bool $polling = false;
28
29    public function __construct(
30        private readonly GitDeployConfig $gitDeploy,
31        private readonly RunningReleaseDetector $runningRelease,
32        private readonly ReleaseScript $script,
33        private readonly DeployState $state,
34        private readonly BrokerRun $run,
35        private readonly Timers $timers,
36        private readonly Clock $clock,
37        private readonly LoggerInterface $logger,
38    ) {}
39
40    public function startPolling(): void
41    {
42        $interval = $this->gitDeploy->poll->findDuration();
43
44        if ($interval === null || $this->timer !== null) {
45            return;
46        }
47
48        $commit = $this->runningRelease->findRunningCommit();
49
50        if ($commit === null) {
51            $this->logger->warning('deploy.git.poll is set, but Stewart is not running from a release directory, so new commits are not deployed.');
52
53            return;
54        }
55
56        $this->state->recordRunningCommit($commit);
57        $this->stop = new DeferredCancellation();
58        $this->timer = new RepeatingTimer(
59            $this->timers,
60            $interval,
61            $this->pollForRelease(...),
62            fn(Throwable $e) => $this->logger->warning('Polling for a new commit failed', ['exception' => $e]),
63        );
64        $this->timer->start();
65    }
66
67    public function stopPolling(): void
68    {
69        $this->timer?->stop();
70        $this->timer = null;
71        $this->stop?->cancel();
72        $this->stop = null;
73    }
74
75    public function pollForRelease(): void
76    {
77        if ($this->polling || $this->stop === null) {
78            return;
79        }
80
81        $this->polling = true;
82        $cancellation = $this->stop->getCancellation();
83
84        async(function () use ($cancellation): void {
85            try {
86                $this->deployLatestRelease($cancellation);
87            } catch (CancelledException) {
88            } catch (Throwable $e) {
89                $this->logger->warning('Could not prepare the latest commit; trying again at the next poll', ['exception' => $e]);
90            } finally {
91                $this->polling = false;
92            }
93        })->ignore();
94    }
95
96    /**
97     * @throws DeployException
98     * @throws CancelledException
99     */
100    private function deployLatestRelease(Cancellation $cancellation): void
101    {
102        $release = $this->script->prepareLatestRelease($cancellation);
103        $this->state->recordPoll($this->clock->getNow());
104
105        if ($release->state !== ReleaseState::Prepared) {
106            return;
107        }
108
109        $failure = $this->script->findCheckFailure($release->commit, $cancellation);
110
111        if ($failure !== null) {
112            $this->state->recordFailure(new ReleaseFailure($release->commit, $failure, $this->clock->getNow()));
113            $this->logger->error('A new commit failed stewart check; the running release stays', ['commit' => $release->commit->value, 'reason' => $failure]);
114
115            return;
116        }
117
118        if (!$this->run->isRunning()) {
119            return;
120        }
121
122        $this->script->stageRelease($release->commit, $cancellation);
123        $this->logger->info('Deploying a new commit', ['commit' => $release->commit->value]);
124        $this->run->stopToRestart('deploying commit ' . $release->commit->value);
125    }
126}