Lines 40.90% 9 / 22
Functions and Methods 40.00% 4 / 10
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
AppActivityRecorder 40.90% 9 / 22 40.00% 4 / 10 61.42 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 handlerFailed 100.00% 4 / 4 100.00% 1 / 1 2
 eventDelivered 0.00% 0 / 1 0.00% 0 / 1 2
 eventSuppressed 0.00% 0 / 1 0.00% 0 / 1 2
 eventDropped 0.00% 0 / 8 0.00% 0 / 1 6
 scheduledRunStarted 0.00% 0 / 1 0.00% 0 / 1 2
 scheduledRunSuppressed 0.00% 0 / 1 0.00% 0 / 1 2
 scheduledRunFailed 100.00% 3 / 3 100.00% 1 / 1 2
 scheduleFailed 0.00% 0 / 1 0.00% 0 / 1 2
 isWaitAbandonedByStop 100.00% 1 / 1 100.00% 1 / 1 3
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Runtime\Worker;
6
7use Stewart\Contracts\Exception\WaitError;
8use Stewart\Contracts\Exception\WaitException;
9use Stewart\Contracts\Schedule\ScheduledRun;
10use Stewart\Runtime\Dispatch\DispatchListener;
11use Stewart\Runtime\Dispatch\RegisteredSubscription;
12use Stewart\Runtime\Lifecycle\AppFailurePhase;
13use Stewart\Runtime\Logging\EveryNthOccurrence;
14use Stewart\Runtime\Model\ResourceScope;
15use Stewart\Runtime\Schedule\ScheduleListener;
16use Stewart\Runtime\Schedule\ScheduleOrigin;
17use Stewart\Runtime\Scope\ScopeLifecycle;
18use Throwable;
19
20final readonly class AppActivityRecorder implements DispatchListener, ScheduleListener
21{
22    private EveryNthOccurrence $dropReports;
23
24    public function __construct(
25        private AppFailureReporter $failures,
26        private AppActivityCounters $activityCounters,
27        private WorkerLogger $logger,
28        private ScopeLifecycle $scopes,
29    ) {
30        $this->dropReports = EveryNthOccurrence::forRepeatedWarnings();
31    }
32
33    public function handlerFailed(RegisteredSubscription $subscription, Throwable $error): void
34    {
35        if ($this->isWaitAbandonedByStop($subscription->scope, $error)) {
36            return;
37        }
38
39        $origin = \sprintf('subscription %s (%s)', $subscription->id, $subscription->describeMatch());
40
41        $this->failures->report($subscription->scope, AppFailurePhase::Handler, $error, $origin);
42    }
43
44    public function eventDelivered(RegisteredSubscription $subscription): void
45    {
46        $this->activityCounters->findOrCreateActivityForScope($subscription->scope)->recordDeliveredEvent();
47    }
48
49    public function eventSuppressed(RegisteredSubscription $subscription): void
50    {
51        $this->activityCounters->findOrCreateActivityForScope($subscription->scope)->recordSuppressedWork();
52    }
53
54    public function eventDropped(RegisteredSubscription $subscription, int $droppedSoFar): void
55    {
56        $this->activityCounters->findOrCreateActivityForScope($subscription->scope)->recordDroppedEvent();
57
58        if (!$this->dropReports->includesOccurrence($droppedSoFar)) {
59            return;
60        }
61
62        $this->logger->forScope($subscription->scope)->warning('Handler is falling behind; dropping queued events', [
63            'subscription' => $subscription->id->value,
64            'selector' => $subscription->describeMatch(),
65            'dropped' => $droppedSoFar,
66        ]);
67    }
68
69    public function scheduledRunStarted(ScheduleOrigin $origin, ScheduledRun $run): void
70    {
71        $this->activityCounters->findOrCreateActivityForScope($origin->scope)->recordScheduleRun();
72    }
73
74    public function scheduledRunSuppressed(ScheduleOrigin $origin, ScheduledRun $run): void
75    {
76        $this->activityCounters->findOrCreateActivityForScope($origin->scope)->recordSuppressedWork();
77    }
78
79    public function scheduledRunFailed(ScheduleOrigin $origin, Throwable $error): void
80    {
81        if ($this->isWaitAbandonedByStop($origin->scope, $error)) {
82            return;
83        }
84
85        $this->failures->report($origin->scope, AppFailurePhase::Handler, $error, $origin->label());
86    }
87
88    public function scheduleFailed(ScheduleOrigin $origin, Throwable $error): void
89    {
90        $this->failures->report($origin->scope, AppFailurePhase::Schedule, $error, $origin->label());
91    }
92
93    // Stopping an app abandons its pending waits; that is part of the stop, not a handler failure.
94    private function isWaitAbandonedByStop(ResourceScope $scope, Throwable $error): bool
95    {
96        return $error instanceof WaitException && $error->reason === WaitError::Abandoned && $this->scopes->findOrCreateStateOf($scope)->isClosed();
97    }
98}