Lines
89.13%
82 / 92
Functions and Methods
57.14%
8 / 14
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| AppLifecycle | 89.13% | 82 / 92 | 57.14% | 8 / 14 | 45.37 | 0.00% | 0 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| constructApps | 100.00% | 16 / 16 | 100.00% | 1 / 1 | 6 | |||||
| initializeApps | 92.30% | 12 / 13 | 0.00% | 0 / 1 | 7.02 | |||||
| listSlots | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | |||||
| listAppIdsInState | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | |||||
| markStopping | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| disposeAppsBefore | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 3 | |||||
| abandonUnfinishedApps | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 2 | |||||
| isStopRequested | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| moveToPhase | 66.66% | 2 / 3 | 0.00% | 0 / 1 | 2.15 | |||||
| constructApp | 50.00% | 5 / 10 | 0.00% | 0 / 1 | 6.00 | |||||
| initializeApp | 100.00% | 22 / 22 | 100.00% | 1 / 1 | 9 | |||||
| disposeApp | 85.71% | 6 / 7 | 0.00% | 0 / 1 | 3.03 | |||||
| failApp | 80.00% | 4 / 5 | 0.00% | 0 / 1 | 2.03 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Stewart\Runtime\Worker; | |
| 6 | ||
| 7 | use Amp\Cancellation; | |
| 8 | use Amp\CancelledException; | |
| 9 | use LogicException; | |
| 10 | use Stewart\Contracts\App; | |
| 11 | use Stewart\Contracts\App\Collection\AppIdCollection; | |
| 12 | use Stewart\Contracts\Exception\StewartException; | |
| 13 | use Stewart\Runtime\Container\AppContainerBuilder; | |
| 14 | use Stewart\Runtime\Container\AppRuntimeServices; | |
| 15 | use Stewart\Runtime\Container\TypedContainer; | |
| 16 | use Stewart\Runtime\Exception\AppError; | |
| 17 | use Stewart\Runtime\Exception\AppException; | |
| 18 | use Stewart\Runtime\Ipc\Message\Bootstrap; | |
| 19 | use Stewart\Runtime\Ipc\WorkerApp; | |
| 20 | use Stewart\Runtime\Lifecycle\AppFailurePhase; | |
| 21 | use Stewart\Runtime\Lifecycle\AppLifecyclePhase; | |
| 22 | use Stewart\Runtime\Lifecycle\AppState; | |
| 23 | use Stewart\Runtime\Model\ResourceScope; | |
| 24 | use Throwable; | |
| 25 | ||
| 26 | use function Amp\async; | |
| 27 | use function Amp\Future\await; | |
| 28 | ||
| 29 | final class AppLifecycle | |
| 30 | { | |
| 31 | /** @var list<AppSlot> */ | |
| 32 | private readonly array $slots; | |
| 33 | ||
| 34 | private AppLifecyclePhase $phase = AppLifecyclePhase::Starting; | |
| 35 | ||
| 36 | public function __construct( | |
| 37 | private readonly Bootstrap $bootstrap, | |
| 38 | private readonly AppRuntimeServices $runtime, | |
| 39 | private readonly AppResources $resources, | |
| 40 | private readonly AppFailureReporter $failures, | |
| 41 | private readonly AppContainerBuilder $containers, | |
| 42 | ) { | |
| 43 | $this->slots = $this->bootstrap->apps->collection->mapToList(static fn(WorkerApp $definition): AppSlot => new AppSlot($definition)); | |
| 44 | } | |
| 45 | ||
| 46 | public function constructApps(): void | |
| 47 | { | |
| 48 | try { | |
| 49 | $build = $this->containers->buildAppContainer( | |
| 50 | apps: $this->bootstrap->apps->collection, | |
| 51 | runtime: $this->runtime, | |
| 52 | servicesFile: $this->bootstrap->settings->servicesFile, | |
| 53 | workerId: $this->bootstrap->workerId, | |
| 54 | ); | |
| 55 | } catch (StewartException $e) { | |
| 56 | foreach ($this->slots as $slot) { | |
| 57 | $this->failApp($slot, AppFailurePhase::Construct, $e); | |
| 58 | } | |
| 59 | ||
| 60 | return; | |
| 61 | } | |
| 62 | ||
| 63 | foreach ($this->slots as $slot) { | |
| 64 | $failure = $build->failures->find($slot->id()); | |
| 65 | ||
| 66 | if ($failure !== null) { | |
| 67 | $this->failApp($slot, AppFailurePhase::Construct, $failure->error); | |
| 68 | } elseif ($slot->isInState(AppState::Declared)) { | |
| 69 | $this->constructApp($build->container, $slot); | |
| 70 | } | |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | public function initializeApps(): void | |
| 75 | { | |
| 76 | $attempts = []; | |
| 77 | ||
| 78 | foreach ($this->slots as $slot) { | |
| 79 | if ($slot->isInState(AppState::Declared) || $slot->isInState(AppState::Constructing)) { | |
| 80 | throw new LogicException(\sprintf('App "%s" is still being constructed.', $slot->id())); | |
| 81 | } | |
| 82 | ||
| 83 | if (!$slot->isInState(AppState::Constructed)) { | |
| 84 | continue; | |
| 85 | } | |
| 86 | ||
| 87 | if ($this->isStopRequested()) { | |
| 88 | $slot->moveTo(AppState::Skipped); | |
| 89 | ||
| 90 | continue; | |
| 91 | } | |
| 92 | ||
| 93 | $slot->moveTo(AppState::Initializing); | |
| 94 | $attempts[] = async(fn() => $this->initializeApp($slot)); | |
| 95 | } | |
| 96 | ||
| 97 | await($attempts); | |
| 98 | ||
| 99 | if (!$this->isStopRequested()) { | |
| 100 | $this->resources->activateScope(ResourceScope::shared()); | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | /** @return list<AppSlot> */ | |
| 105 | public function listSlots(): array | |
| 106 | { | |
| 107 | return $this->slots; | |
| 108 | } | |
| 109 | ||
| 110 | public function listAppIdsInState(AppState $state): AppIdCollection | |
| 111 | { | |
| 112 | $inState = array_filter($this->slots, static fn(AppSlot $slot): bool => $slot->isInState($state)); | |
| 113 | ||
| 114 | return AppIdCollection::fromList(array_map(static fn(AppSlot $slot) => $slot->id(), $inState)); | |
| 115 | } | |
| 116 | ||
| 117 | public function markStopping(): void | |
| 118 | { | |
| 119 | $this->moveToPhase(AppLifecyclePhase::Stopping); | |
| 120 | } | |
| 121 | ||
| 122 | /** @throws CancelledException */ | |
| 123 | public function disposeAppsBefore(Cancellation $deadline): void | |
| 124 | { | |
| 125 | $disposals = []; | |
| 126 | ||
| 127 | foreach ($this->slots as $slot) { | |
| 128 | if ($slot->needsDisposal()) { | |
| 129 | $disposals[] = async(fn() => $this->disposeApp($slot)); | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | await($disposals, $deadline); | |
| 134 | } | |
| 135 | ||
| 136 | public function abandonUnfinishedApps(): AppIdCollection | |
| 137 | { | |
| 138 | $this->moveToPhase(AppLifecyclePhase::Closed); | |
| 139 | ||
| 140 | $abandoned = array_filter($this->slots, static fn(AppSlot $slot): bool => !$slot->state->isTerminal()); | |
| 141 | ||
| 142 | foreach ($abandoned as $slot) { | |
| 143 | $slot->moveTo(AppState::Abandoned); | |
| 144 | } | |
| 145 | ||
| 146 | return AppIdCollection::fromList(array_map(static fn(AppSlot $slot) => $slot->id(), $abandoned)); | |
| 147 | } | |
| 148 | ||
| 149 | private function isStopRequested(): bool | |
| 150 | { | |
| 151 | return $this->phase !== AppLifecyclePhase::Starting; | |
| 152 | } | |
| 153 | ||
| 154 | private function moveToPhase(AppLifecyclePhase $phase): void | |
| 155 | { | |
| 156 | if (!$this->phase->canEnter($phase)) { | |
| 157 | throw new LogicException(\sprintf('App lifecycle cannot move from %s to %s.', $this->phase->name, $phase->name)); | |
| 158 | } | |
| 159 | ||
| 160 | $this->phase = $phase; | |
| 161 | } | |
| 162 | ||
| 163 | private function constructApp(TypedContainer $services, AppSlot $slot): void | |
| 164 | { | |
| 165 | if ($this->isStopRequested()) { | |
| 166 | $slot->moveTo(AppState::Skipped); | |
| 167 | ||
| 168 | return; | |
| 169 | } | |
| 170 | ||
| 171 | $slot->moveTo(AppState::Constructing); | |
| 172 | ||
| 173 | try { | |
| 174 | $app = $services->resolveService(App::class, AppContainerBuilder::buildAppServiceId($slot->id())); | |
| 175 | } catch (Throwable $e) { | |
| 176 | $this->failApp($slot, AppFailurePhase::Construct, $e); | |
| 177 | ||
| 178 | return; | |
| 179 | } | |
| 180 | ||
| 181 | if (!$slot->isInState(AppState::Abandoned)) { | |
| 182 | $slot->markConstructed($app); | |
| 183 | } | |
| 184 | } | |
| 185 | ||
| 186 | private function initializeApp(AppSlot $slot): void | |
| 187 | { | |
| 188 | $attempt = async(function () use ($slot): void { | |
| 189 | $slot->app()->initialize(); | |
| 190 | ||
| 191 | if ($slot->isInState(AppState::Failed)) { | |
| 192 | // initialize() finished after the timeout failed it; dispose so it releases what it opened. | |
| 193 | $this->disposeApp($slot); | |
| 194 | } | |
| 195 | }); | |
| 196 | ||
| 197 | $timeout = $this->bootstrap->settings->initializeTimeout; | |
| 198 | $deadline = $timeout === null ? null : $this->runtime->timers->timeout($timeout); | |
| 199 | ||
| 200 | try { | |
| 201 | $attempt->await($deadline); | |
| 202 | } catch (Throwable $e) { | |
| 203 | if ($e instanceof CancelledException && $timeout !== null && $deadline?->isRequested() === true) { | |
| 204 | $attempt->ignore(); | |
| 205 | $e = AppException::createForReason(AppError::InitializeTimedOut, $e, appId: $slot->id()->value, timeout: (string) $timeout); | |
| 206 | } | |
| 207 | ||
| 208 | $this->failApp($slot, AppFailurePhase::Initialize, $e); | |
| 209 | ||
| 210 | return; | |
| 211 | } | |
| 212 | ||
| 213 | if ($slot->isInState(AppState::Abandoned)) { | |
| 214 | return; | |
| 215 | } | |
| 216 | ||
| 217 | if ($this->isStopRequested()) { | |
| 218 | $slot->moveTo(AppState::Initialized); | |
| 219 | $this->disposeApp($slot); | |
| 220 | ||
| 221 | return; | |
| 222 | } | |
| 223 | ||
| 224 | $slot->moveTo(AppState::Running); | |
| 225 | $this->resources->activateScope($slot->scope()); | |
| 226 | } | |
| 227 | ||
| 228 | private function disposeApp(AppSlot $slot): void | |
| 229 | { | |
| 230 | if (!$slot->claimDisposal()) { | |
| 231 | return; | |
| 232 | } | |
| 233 | ||
| 234 | $this->resources->releaseScope($slot->scope()); | |
| 235 | ||
| 236 | try { | |
| 237 | $slot->app()->dispose(); | |
| 238 | } catch (Throwable $e) { | |
| 239 | $this->failures->report($slot->scope(), AppFailurePhase::Dispose, $e); | |
| 240 | } | |
| 241 | ||
| 242 | $slot->markDisposed(); | |
| 243 | } | |
| 244 | ||
| 245 | private function failApp(AppSlot $slot, AppFailurePhase $phase, Throwable $error): void | |
| 246 | { | |
| 247 | if ($slot->isInState(AppState::Abandoned)) { | |
| 248 | return; | |
| 249 | } | |
| 250 | ||
| 251 | $slot->moveTo(AppState::Failed); | |
| 252 | $this->resources->releaseScope($slot->scope()); | |
| 253 | $this->failures->report($slot->scope(), $phase, $error); | |
| 254 | } | |
| 255 | } |