Lines
100.00%
48 / 48
Methods
100.00%
11 / 11
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| restoreStoredOverrides | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 2 | ||
| pauseApp | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| resumeApp | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| resetApp | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 5 | ||
| assertAppLoaded | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | ||
| resetStoredOnlyApp | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 2 | ||
| overridePause | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 3 | ||
| createOverride | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 5 | ||
| createNotLoadedException | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | ||
| broadcastPausedApps | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| 22 | final readonly class AppPauseService | |
| 23 | { | |
| 24 | public function __construct( | |
| 25 | private AppCatalog $apps, | |
| 26 | private AppPauseRegistry $pausedApps, | |
| 27 | private AppPauseOverrideStore $overrides, | |
| 28 | private WorkerSlotRegistry $slots, | |
| 29 | private Clock $clock, | |
| 30 | private LoggerInterface $logger, | |
| 31 | ) {} | |
| 32 | ||
| 33 | /** @throws StoreException */ | |
| 34 | public function restoreStoredOverrides(): void | |
| 35 | { | |
| 36 | $stored = $this->overrides->loadOverrides(); | |
| 37 | $this->pausedApps->restoreOverrides($stored->filter(fn(AppPauseOverride $override): bool => $this->apps->enabled->find($override->appId) !== null)); | |
| 38 | $unknown = $stored->filter(fn(AppPauseOverride $override): bool => !$this->apps->knownIds->containsId($override->appId)); | |
| 39 | ||
| 40 | if ($unknown->count() > 0) { | |
| 41 | $this->logger->warning('Stored pause overrides name apps that no longer exist; stewart app:reset <app> removes them', [ | |
| 42 | 'apps' => implode(', ', $unknown->mapToList(static fn(AppPauseOverride $override): string => $override->appId->value)), | |
| 43 | ]); | |
| 44 | } | |
| 45 | } | |
| 46 | ||
| 47 | /** @throws AppException */ | |
| 48 | public function pauseApp(AppId $appId, AppPauseSource $source): AppPauseOutcome | |
| 49 | { | |
| 50 | return $this->overridePause($appId, true, $source); | |
| 51 | } | |
| 52 | ||
| 53 | /** @throws AppException */ | |
| 54 | public function resumeApp(AppId $appId, AppPauseSource $source): AppPauseOutcome | |
| 55 | { | |
| 56 | return $this->overridePause($appId, false, $source); | |
| 57 | } | |
| 58 | ||
| 59 | /** @throws AppException|StoreException */ | |
| 60 | public function resetApp(AppId $appId, AppPauseSource $source): AppPauseResetOutcome | |
| 61 | { | |
| 62 | if ($this->apps->enabled->find($appId) === null) { | |
| 63 | return $this->resetStoredOnlyApp($appId, $source); | |
| 64 | } | |
| 65 | ||
| 66 | $wasPaused = $this->pausedApps->isPaused($appId); | |
| 67 | $removed = $this->pausedApps->forgetOverride($appId); | |
| 68 | $paused = $this->pausedApps->isPaused($appId); | |
| 69 | ||
| 70 | if ($paused !== $wasPaused) { | |
| 71 | $this->broadcastPausedApps(); | |
| 72 | } | |
| 73 | ||
| 74 | if ($removed) { | |
| 75 | $this->logger->info('App pause override removed', ['app' => $appId->value, 'source' => $source->value, 'paused' => $paused]); | |
| 76 | } | |
| 77 | ||
| 78 | $persistence = $this->overrides->removeOverride($appId); | |
| 79 | ||
| 80 | return new AppPauseResetOutcome($removed, $paused ? AppStateAfterReset::PausedByConfig : AppStateAfterReset::Running, $persistence); | |
| 81 | } | |
| 82 | ||
| 83 | /** @throws AppException */ | |
| 84 | public function assertAppLoaded(AppId $appId): void | |
| 85 | { | |
| 86 | if ($this->apps->enabled->find($appId) === null) { | |
| 87 | throw $this->createNotLoadedException($appId); | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | /** @throws AppException|StoreException */ | |
| 92 | private function resetStoredOnlyApp(AppId $appId, AppPauseSource $source): AppPauseResetOutcome | |
| 93 | { | |
| 94 | if (!$this->overrides->hasOverride($appId)) { | |
| 95 | throw $this->createNotLoadedException($appId); | |
| 96 | } | |
| 97 | ||
| 98 | $persistence = $this->overrides->removeOverride($appId); | |
| 99 | $this->logger->info('App pause override removed', ['app' => $appId->value, 'source' => $source->value]); | |
| 100 | ||
| 101 | return new AppPauseResetOutcome(true, AppStateAfterReset::NotLoaded, $persistence); | |
| 102 | } | |
| 103 | ||
| 104 | /** @throws AppException */ | |
| 105 | private function overridePause(AppId $appId, bool $paused, AppPauseSource $source): AppPauseOutcome | |
| 106 | { | |
| 107 | $this->assertAppLoaded($appId); | |
| 108 | $override = $this->createOverride($appId, $paused, $source); | |
| 109 | $changed = $this->pausedApps->recordOverride($override); | |
| 110 | ||
| 111 | if ($changed) { | |
| 112 | $this->broadcastPausedApps(); | |
| 113 | $this->logger->info($paused ? 'App paused' : 'App resumed', ['app' => $appId->value, 'source' => $source->value]); | |
| 114 | } | |
| 115 | ||
| 116 | $persistence = $this->overrides->saveOverride($override); | |
| 117 | ||
| 118 | return new AppPauseOutcome($changed, $persistence); | |
| 119 | } | |
| 120 | ||
| 121 | private function createOverride(AppId $appId, bool $paused, AppPauseSource $source): AppPauseOverride | |
| 122 | { | |
| 123 | $existing = $this->pausedApps->findOverride($appId); | |
| 124 | ||
| 125 | if ($existing?->paused === $paused) { | |
| 126 | return $existing; | |
| 127 | } | |
| 128 | ||
| 129 | // Pinning a config pause keeps its start and source, so status still says why the app is paused. | |
| 130 | $configPause = $existing === null && $paused ? $this->pausedApps->findPause($appId) : null; | |
| 131 | ||
| 132 | return $configPause === null | |
| 133 | ? new AppPauseOverride($appId, $paused, $this->clock->getNow(), $source) | |
| 134 | : new AppPauseOverride($appId, true, $configPause->since, $configPause->source); | |
| 135 | } | |
| 136 | ||
| 137 | private function createNotLoadedException(AppId $appId): AppException | |
| 138 | { | |
| 139 | return $this->apps->knownIds->containsId($appId) ? AppException::createForReason( | |
| 140 | AppError::Disabled, | |
| 141 | appId: $appId->value, | |
| 142 | ) : AppException::unknown($appId); | |
| 143 | } | |
| 144 | ||
| 145 | private function broadcastPausedApps(): void | |
| 146 | { | |
| 147 | $this->slots->broadcast(new PausedAppsChanged(AppIdsFragment::fromCollection($this->pausedApps->listPausedAppIds()))); | |
| 148 | } | |
| 149 | } |