Lines 96.00% 48 / 50
Methods 81.81% 9 / 11
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 forApp 100.00% 1 / 1 100.00% 1 / 1 1
 forScope 100.00% 1 / 1 100.00% 1 / 1 1
 buildMetricsKey 100.00% 1 / 1 100.00% 1 / 1 2
 isSharedScope 100.00% 1 / 1 100.00% 1 / 1 1
 recordActivityReport 100.00% 10 / 10 100.00% 1 / 1 1
 recordCall 100.00% 1 / 1 100.00% 1 / 1 1
 recordExposedCommand 0.00% 0 / 1 0.00% 0 / 1 2
 recordLastFailure 100.00% 1 / 1 100.00% 1 / 1 1
 buildAppStatus 100.00% 27 / 27 100.00% 1 / 1 6
 buildExposedCommandStats 80.00% 4 / 5 0.00% 0 / 1 3.07
24final class AppRunningTotals
25{
26    private ?AppState $state = null;
27
28    private ?Instant $reportedAt = null;
29
30    private int $subscriptions = 0;
31
32    private int $schedules = 0;
33
34    private int $delivered = 0;
35
36    private int $subscriptionDropped = 0;
37
38    private int $scheduleRuns = 0;
39
40    private int $publishes = 0;
41
42    private int $failures = 0;
43
44    private int $suppressed = 0;
45
46    private ?FailureReport $lastFailure = null;
47
48    /** @var array<string, ServiceCallStatsRecorder> */
49    private array $calls = [];
50
51    /** @var array<string, int> */
52    private array $exposedCommandCounts = [];
53
54    private function __construct(
55        private readonly string $id,
56        private readonly ?AppId $appId,
57        private readonly string $class,
58        private readonly ?WorkerId $workerId,
59    ) {}
60
61    public static function forApp(AppId $appId, string $class, ?WorkerId $workerId): self
62    {
63        return new self($appId->value, $appId, $class, $workerId);
64    }
65
66    public static function forScope(ResourceScope $scope, WorkerId $workerId): self
67    {
68        return new self($scope->wireValue(), $scope->appId, '', $workerId);
69    }
70
71    // The shared services.php scope exists once per worker, so its totals are kept per worker.
72    public static function buildMetricsKey(ResourceScope $scope, WorkerId $workerId): string
73    {
74        return $scope->isShared() ? $scope->wireValue() . '#' . $workerId->value : $scope->wireValue();
75    }
76
77    public function isSharedScope(): bool
78    {
79        return $this->appId === null;
80    }
81
82    public function recordActivityReport(AppActivityReport $report, ?AppActivityReport $previous, Instant $at): void
83    {
84        $this->state = $report->state;
85        $this->reportedAt = $at;
86        $this->subscriptions = $report->subscriptions;
87        $this->schedules = $report->schedules;
88        $this->delivered += max(0, $report->delivered - ($previous->delivered ?? 0));
89        $this->subscriptionDropped += max(0, $report->subscriptionDropped - ($previous->subscriptionDropped ?? 0));
90        $this->scheduleRuns += max(0, $report->scheduleRuns - ($previous->scheduleRuns ?? 0));
91        $this->publishes += max(0, $report->publishes - ($previous->publishes ?? 0));
92        $this->failures += max(0, $report->failures - ($previous->failures ?? 0));
93        $this->suppressed += max(0, $report->suppressed - ($previous->suppressed ?? 0));
94    }
95
96    public function recordCall(ServiceCallOutcome $outcome, ?Duration $latency): void
97    {
98        ($this->calls[$outcome->value] ??= new ServiceCallStatsRecorder($outcome))->recordCall($latency);
99    }
100
101    public function recordExposedCommand(ExposedCommandOutcome $outcome): void
102    {
103        $this->exposedCommandCounts[$outcome->value] = ($this->exposedCommandCounts[$outcome->value] ?? 0) + 1;
104    }
105
106    public function recordLastFailure(FailureReport $failure): void
107    {
108        $this->lastFailure = $failure;
109    }
110
111    public function buildAppStatus(AppPauseRegistry $pausedApps, ExposureLink $exposures): AppStatus
112    {
113        $calls = $this->calls;
114        ksort($calls);
115        $pause = $this->appId === null ? null : $pausedApps->findPause($this->appId);
116        $configPauseOverride = $this->appId === null ? null : $pausedApps->findConfigPauseOverride($this->appId);
117
118        return new AppStatus(
119            id: $this->id,
120            class: $this->class,
121            workerId: $this->workerId?->value,
122            state: $this->state,
123            pause: $pause === null ? null : new AppPauseStatus($pause->since, $pause->source),
124            configPauseOverride: $configPauseOverride === null ? null : new AppPauseStatus($configPauseOverride->since, $configPauseOverride->source),
125            reportedAt: $this->reportedAt,
126            subscriptions: $this->subscriptions,
127            schedules: $this->schedules,
128            exposedEntities: $this->appId === null ? 0 : $exposures->countExposuresOfApp($this->appId),
129            counters: new AppCounters(
130                delivered: $this->delivered,
131                subscriptionDropped: $this->subscriptionDropped,
132                scheduleRuns: $this->scheduleRuns,
133                publishes: $this->publishes,
134                failures: $this->failures,
135                suppressed: $this->suppressed,
136            ),
137            serviceCalls: array_values(array_map(static fn(ServiceCallStatsRecorder $recorder): ServiceCallStats => $recorder->buildServiceCallStats(), $calls)),
138            lastFailure: $this->lastFailure,
139            exposedCommands: $this->buildExposedCommandStats(),
140        );
141    }
142
143    /** @return list<ExposedCommandStats> */
144    private function buildExposedCommandStats(): array
145    {
146        $stats = [];
147
148        foreach (ExposedCommandOutcome::cases() as $outcome) {
149            if (isset($this->exposedCommandCounts[$outcome->value])) {
150                $stats[] = new ExposedCommandStats($outcome, $this->exposedCommandCounts[$outcome->value]);
151            }
152        }
153
154        return $stats;
155    }
156}