PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / ViewBuildStageRunner.php

ViewBuildStageRunner.php in 404 Solution 4.2.0, at includes/ViewBuildStageRunner.php

579 lines 28.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Per-stage runner / telemetry / shutdown-diagnostics infrastructure for the
9 * staged getRedirectsForView build pipeline.
10 *
11 * Extracted from {@see ABJ_404_Solution_DataAccess_ViewQueriesStagedTrait}
12 * so the orchestrator (runStagedBuildOnce) stays under the modularity cap
13 * without losing the timing/log/inflight wiring each stage relies on.
14 *
15 * What lives here:
16 * - runTimedViewBuildStage(): the wrapper every stage callback flows
17 * through. Times the stage, catches throwables, dispatches them to the
18 * HostFailurePolicy classifier, and emits the per-stage log line.
19 * - runNonBatchedStageWithKillStreakEscape(): the variant used by the
20 * non-batched stages (S3 / S9 / S10) that bumps a kill-streak counter
21 * and swaps in an extended per-query timeout on retry.
22 * - markViewBuildStageStarted / markViewBuildStageCompleted: persist
23 * per-stage entry/exit markers to the progress option registry so a
24 * resumable build can pick up where the previous request left off.
25 * - registerViewBuildShutdownDiagnostics / logViewBuildShutdownDiagnostics
26 * / clearViewBuildOpenStageForShutdown: register_shutdown_function
27 * diagnostics that surface a WARN line when PHP dies mid-stage so the
28 * post-mortem records which stage was open and the last PHP error.
29 * - markBuildStage(): writes the inflight-stage transient that the AJAX
30 * progress poller reads, including the "batch X/Y" inner-loop marker
31 * captured into $lastBatchProgressDetail so a mid-stage yield doesn't
32 * drop back to a bare "yielded in N ms" between ticks.
33 *
34 * Composed alongside the orchestrator trait into ABJ_404_Solution_DataAccess
35 * so the cross-trait calls ($this->readProgressOption, $this->logger,
36 * $this->classifyAndHandleStageFailure, $this->resetStageNoProgressStreak,
37 * $this->extendedTimeoutForKilledNonBatchedStage, $this->stagedQueryTimeoutSeconds)
38 * resolve through the shared composing class.
39 *
40 * @property ABJ_404_Solution_DatabaseCore $dbCore
41 * @property ABJ_404_Solution_Functions $f
42 * @property ABJ_404_Solution_Logging $logger
43 * @property ABJ_404_Solution_ViewReadService|null $viewReadService
44 * @property ABJ_404_Solution_LogsRepository|null $logsRepo
45 * @property int $stagedQueryTimeoutSeconds
46 * @property string $lastBatchProgressDetail
47 * @property bool $viewBuildStageOpenForShutdown
48 * @property int $viewBuildShutdownStageNumber
49 * @property string $viewBuildShutdownStageKey
50 * @property bool|null $namedLockSupportedThisRequest
51 * @property bool $fallbackLockLoggedThisRequest
52 * @property bool $usingTransientFallbackLock
53 * @property string $lastNamedLockUnsupportedReason
54 * @property string $lastNamedLockUnsupportedError
55 * @method void abortStagedBuildForMutationWatermarkAdvance(...$arguments)
56 * @method bool acquireTransientFallbackLock(...$arguments)
57 * @method bool acquireViewBuildLock(...$arguments)
58 * @method string activeBuildStartedWatermarkOptionName(...$arguments)
59 * @method bool adminMutationGateBlocks(...$arguments)
60 * @method array<mixed> advanceViewBuildOnce(...$arguments)
61 * @method void assertBuildBufferExistsOrHalt(...$arguments)
62 * @method ?bool attemptRelaxSqlModeForBuildConnection(...$arguments)
63 * @method bool bufferIntegrityPassesForPromote(...$arguments)
64 * @method string buildHaltTransientKey(...$arguments)
65 * @method string buildViewDoneCountQuery(...$arguments)
66 * @method string builtWatermarkOptionName(...$arguments)
67 * @method int bumpMutationWatermark(...$arguments)
68 * @method int bumpStageNoProgressStreak(...$arguments)
69 * @method string capturedPrefixForLog(...$arguments)
70 * @method void capturePrefixAtBuildStart(...$arguments)
71 * @method void claimForegroundViewBuildLease(...$arguments)
72 * @method string classifyAndHandleStageFailure(...$arguments)
73 * @method array<mixed> classifySessionVariableWarnings(...$arguments)
74 * @method string classifyStageFailure(...$arguments)
75 * @method void clearActiveBuildStartedWatermark(...$arguments)
76 * @method void clearAdminMutationGateOptions(...$arguments)
77 * @method void clearAllProgressOptions(...$arguments)
78 * @method void clearPhpEnvironmentProbeCache(...$arguments)
79 * @method void clearPrefixAtStageOne(...$arguments)
80 * @method void clearSessionVariablesProbeCache(...$arguments)
81 * @method void clearSqlModeProbeCache(...$arguments)
82 * @method void clearStagedBuildDegradedState(...$arguments)
83 * @method void clearViewBuildOpenStageForShutdown(...$arguments)
84 * @method void clearViewDoneHardStaleNotice(...$arguments)
85 * @method ABJ_404_Solution_Clock clock(...$arguments)
86 * @method int countLiveRedirects(...$arguments)
87 * @method int countViewBuildRows(...$arguments)
88 * @method string describeBuildProgressForNotice(...$arguments)
89 * @method string describeDegradedNotice(...$arguments)
90 * @method string describeStagedSqlFailure(...$arguments)
91 * @method array<mixed> detectAndAdjustSqlMode(...$arguments)
92 * @method float detectHostStagedQueryLimitSeconds(...$arguments)
93 * @method string doTableNameReplacements(...$arguments)
94 * @method void dropDeletemeTable(...$arguments)
95 * @method void dropTransientBuffersIfPresent(...$arguments)
96 * @method void dropTransientStagedTables(...$arguments)
97 * @method void ensureConnection(...$arguments)
98 * @method void ensureFallbackLockNoticeAndLog(...$arguments)
99 * @method int extendedTimeoutForKilledNonBatchedStage(...$arguments)
100 * @method array<mixed> fetchSessionVariablesRowOrEmpty(...$arguments)
101 * @method string filesystemEnvironmentProbeOptionName(...$arguments)
102 * @method bool forceRestartViewBuild(...$arguments)
103 * @method bool foregroundViewBuildLeaseActive(...$arguments)
104 * @method string formatPhpMemoryBytesHuman(...$arguments)
105 * @method bool gateAbortIfMutationWatermarkAdvanced(...$arguments)
106 * @method string getColumnCollationString(...$arguments)
107 * @method int getCronStuckHours(...$arguments)
108 * @method string getLowercasePrefix(...$arguments)
109 * @method array<string, mixed> getViewBuildProgress(...$arguments)
110 * @method array<mixed> getViewBuildProgressFingerprint(...$arguments)
111 * @method int getViewDoneBuiltAtTimestamp(...$arguments)
112 * @method bool haltIfPrefixChangedSinceStageOne(...$arguments)
113 * @method string humanBatchProgress(...$arguments)
114 * @method float intelligentStagedQueryTimeoutSeconds(...$arguments)
115 * @method void invalidateViewDoneServeableCache(...$arguments)
116 * @method bool isBuildHaltedForHostFailure(...$arguments)
117 * @method bool isCurrentStageOptionName(...$arguments)
118 * @method bool isNamedLockUnsupportedError(...$arguments)
119 * @method bool isResumableStagedKill(...$arguments)
120 * @method bool isStageMarkedSkipped(...$arguments)
121 * @method bool isTransientConnectionError(...$arguments)
122 * @method string lastBuildStartedWatermarkOptionName(...$arguments)
123 * @method string legacyStartedWatermarkOptionName(...$arguments)
124 * @method string localizeOrDefaultViewBuildNotice(...$arguments)
125 * @method bool logsHitsTableExists(...$arguments)
126 * @method void logTimedViewBuildStage(...$arguments)
127 * @method void logViewBuildProgressOptionWrite(...$arguments)
128 * @method void logViewBuildShutdownDiagnostics(...$arguments)
129 * @method void markBuildHaltedForHostFailure(...$arguments)
130 * @method void markBuildStage(...$arguments)
131 * @method void markStageSkippedForHostFailure(...$arguments)
132 * @method void markViewBuildStageCompleted(...$arguments)
133 * @method void markViewBuildStageStarted(...$arguments)
134 * @method void markViewDoneBuildCompleted(...$arguments)
135 * @method void markViewDoneInvalidatedByAdminMutation(...$arguments)
136 * @method int maxBuildBufferId(...$arguments)
137 * @method void maybeRaiseViewDoneHardStaleNotice(...$arguments)
138 * @method bool mutationWatermarkAdvancedSinceBuildStart(...$arguments)
139 * @method int mutationWatermarkObservedByAdminAction(...$arguments)
140 * @method int mutationWatermarkObservedByAdminActionAt(...$arguments)
141 * @method string mutationWatermarkObservedByAdminActionAtOptionName(...$arguments)
142 * @method string mutationWatermarkObservedByAdminActionOptionName(...$arguments)
143 * @method string normalizePathPrefix(...$arguments)
144 * @method bool optionReadBackMatches(...$arguments)
145 * @method int parsePhpMemoryLimitToBytes(...$arguments)
146 * @method bool pathFallsWithinAny(...$arguments)
147 * @method void performFreshStartCleanup(...$arguments)
148 * @method array<mixed> phpDisabledFunctionsList(...$arguments)
149 * @method string phpEnvironmentProbeOptionName(...$arguments)
150 * @method float phpTimeRemainingSeconds(...$arguments)
151 * @method string prefixAtStageOneOptionName(...$arguments)
152 * @method array<mixed> probeFilesystemEnvironmentForBuild(...$arguments)
153 * @method float probeFloatFromValues(...$arguments)
154 * @method int probeIntFromValues(...$arguments)
155 * @method int probeMemoryLimitForS9(...$arguments)
156 * @method array<mixed> probePhpEnvironmentForBuild(...$arguments)
157 * @method array<mixed> probeSessionVariablesAtS1Entry(...$arguments)
158 * @method bool probeSetTimeLimitAvailability(...$arguments)
159 * @method array<mixed> probeSqlModeForBuild(...$arguments)
160 * @method string probeStringFromValues(...$arguments)
161 * @method string progressOptionName(...$arguments)
162 * @method void publishBuiltWatermarkFromActiveBuildStartedWatermark(...$arguments)
163 * @method array<mixed> queryAndGetResults(...$arguments)
164 * @method int readActiveBuildStartedWatermark(...$arguments)
165 * @method array<int, array<string, mixed>> readFromViewDone(...$arguments)
166 * @method int readProgressOption(...$arguments)
167 * @method int readWatermarkOption(...$arguments)
168 * @method void rebuildViewDoneInBackground(...$arguments)
169 * @method bool reconcilePostStageElevenState(...$arguments)
170 * @method string reconcileStagedTablesAtRunnerStartup(...$arguments)
171 * @method int recordStageBatchKilled(...$arguments)
172 * @method void registerViewBuildShutdownDiagnostics(...$arguments)
173 * @method bool releaseAndReacquireBetweenStages(...$arguments)
174 * @method void releaseViewBuildLock(...$arguments)
175 * @method void resetStageNoProgressStreak(...$arguments)
176 * @method string resolveColumnCollationForStagedBuild(...$arguments)
177 * @method void runForceRestartCleanupInsideLock(...$arguments)
178 * @method bool runIdRangeBatchedUpdate(...$arguments)
179 * @method int runInsertBatch(...$arguments)
180 * @method mixed runNonBatchedStageWithKillStreakEscape(...$arguments)
181 * @method array{ran: bool, reason: string, progress: array<string, mixed>} runPageLoadFallbackAdvance(...$arguments)
182 * @method int runRedirectsForViewCountStaged(...$arguments)
183 * @method array<int, array<string, mixed>> runRedirectsForViewStaged(...$arguments)
184 * @method bool runS11SwapWithPreRenameWatermarkRecheck(...$arguments)
185 * @method bool runStagedBuildOnce(...$arguments)
186 * @method bool runStagedBuildStages6Through11(...$arguments)
187 * @method void runStagedSqlFile(...$arguments)
188 * @method void runStagedSqlFileTolerantOfDuplicateKey(...$arguments)
189 * @method mixed runTimedViewBuildStage(...$arguments)
190 * @method int safeCurrentMutationWatermark(...$arguments)
191 * @method string sanitizeUrlBeforeInsert(...$arguments)
192 * @method void scheduleViewDoneRebuild(...$arguments)
193 * @method string sessionVariablesProbeOptionName(...$arguments)
194 * @method void setFilesystemEnvAdminNotice(...$arguments)
195 * @method void setLowMemoryLimitAdminNotice(...$arguments)
196 * @method void setSessionEnvAdminNotice(...$arguments)
197 * @method void setStagedBuildDegradedNotice(...$arguments)
198 * @method void setStagedBuildHaltNotice(...$arguments)
199 * @method void setViewBuildCronStuckNotice(...$arguments)
200 * @method void setViewBuildScheduleFailedNotice(...$arguments)
201 * @method void setViewDoneHardStaleNotice(...$arguments)
202 * @method array<mixed> splitOpenBasedirPaths(...$arguments)
203 * @method string sqlModeProbeOptionName(...$arguments)
204 * @method void stageAddPreJoinIndexes(...$arguments)
205 * @method void stageAddSortIndexes(...$arguments)
206 * @method void stageCreateBuildTable(...$arguments)
207 * @method array<string, mixed> stagedQueryOptions(...$arguments)
208 * @method bool stagedTableExists(...$arguments)
209 * @method bool stageInsertRedirectsBatched(...$arguments)
210 * @method string stageNoProgressStreakOptionName(...$arguments)
211 * @method void stageRenameSwap(...$arguments)
212 * @method string stageSkipOptionName(...$arguments)
213 * @method void stageUpdateExternal(...$arguments)
214 * @method void stageUpdateHits(...$arguments)
215 * @method void stageUpdateHome(...$arguments)
216 * @method bool stageUpdatePostsBatched(...$arguments)
217 * @method void stageUpdateSpecial(...$arguments)
218 * @method bool stageUpdateTermsBatched(...$arguments)
219 * @method void stampStartedWatermarksAtS1Entry(...$arguments)
220 * @method void sweepStaleRebuildTransients(...$arguments)
221 * @method string transientFallbackLockOptionName(...$arguments)
222 * @method bool verifyBuildLockSerializesWriter(...$arguments)
223 * @method bool verifyOptionWriteCoherent(...$arguments)
224 * @method bool verifyPrefixUnchangedSinceStageOne(...$arguments)
225 * @method int viewBuildBatchSize(...$arguments)
226 * @method int viewBuildBatchSizeForStage(...$arguments)
227 * @method array<mixed> viewBuildOnlyTranslations(...$arguments)
228 * @method float viewBuildPerStageBudgetSeconds(...$arguments)
229 * @method string viewBuildTableName(...$arguments)
230 * @method string viewDeletemeTableName(...$arguments)
231 * @method int viewDoneBuiltAt(...$arguments)
232 * @method int viewDoneBuiltWatermark(...$arguments)
233 * @method int viewDoneDataBuiltAt(...$arguments)
234 * @method string viewDoneDataBuiltAtOptionName(...$arguments)
235 * @method string viewDoneFreshnessOptionName(...$arguments)
236 * @method bool viewDoneHasRows(...$arguments)
237 * @method bool viewDoneIsFresh(...$arguments)
238 * @method bool viewDoneIsServeable(...$arguments)
239 * @method int viewDoneMutationInvalidatedAt(...$arguments)
240 * @method string viewDoneMutationInvalidatedAtOptionName(...$arguments)
241 * @method bool viewDoneTableExists(...$arguments)
242 * @method string viewDoneTableName(...$arguments)
243 * @method void writeProgressOption(...$arguments)
244 * @method void writeWatermarkOption(...$arguments)
245 */
246 class ABJ_404_Solution_ViewBuildStageRunner extends ABJ_404_Solution_ViewBuildCollaborator {
247
248 /** @var bool Process-local guard so shutdown diagnostics register once. */
249 private static $viewBuildShutdownLoggerRegistered = false;
250
251 /** @return void */
252 public static function resetViewBuildShutdownLoggerRegistration(): void {
253 self::$viewBuildShutdownLoggerRegistered = false;
254 }
255
256 /** @var bool True while a stage has started but has not reached normal logging. */
257 private $viewBuildStageOpenForShutdown = false;
258
259 /** @var int Stage currently open for shutdown diagnostics. */
260 private $viewBuildShutdownStageNumber = 0;
261
262 /** @var string Stage key currently open for shutdown diagnostics. */
263 private $viewBuildShutdownStageKey = '';
264
265 /**
266 * Most recent "batch X/Y" progress detail captured from the inner loops of
267 * resumable stages (S2/S4/S5). Preserved across the per-stage yield log so
268 * the user-visible status doesn't drop from "batch 1.28M/1.97M (yielded)"
269 * back to a bare "yielded in N ms" right before the next tick resumes.
270 *
271 * Reset to '' at the start of every runTimedViewBuildStage() invocation.
272 *
273 * @var string
274 */
275 private $lastBatchProgressDetail = '';
276
277 /**
278 * Update the inflight stage transient + AJAX-context global so the
279 * client-side progress poller can render which sub-stage of the staged
280 * build is currently running.
281 *
282 * Best-effort: when there's no AJAX context (background cron, CLI), this
283 * is a no-op. Never let a transient-write failure mask the real query
284 * error we're about to raise.
285 *
286 * @param string $stageKey Sub-stage key, e.g. 'staged_build_s2_insert'.
287 * @param string $detail Optional mid-stage progress detail, e.g. 'batch 4/12'.
288 * @return void
289 */
290 public function markBuildStage(string $stageKey, string $detail = ''): void {
291 if (!class_exists('ABJ_404_Solution_ViewUpdater')) {
292 return;
293 }
294 // Capture inner-loop batch markers ("batch 1282000/1971286",
295 // "batch ... (yielded)", "batch killed at size N; shrunk ...") so
296 // logTimedViewBuildStage() can preserve them in the per-stage yield
297 // marker. Skip strings that already contain ", yielded" so the final
298 // yield write does not loop back into the captured detail.
299 if ($detail !== ''
300 && strncmp($detail, 'batch ', 6) === 0
301 && strpos($detail, ', yielded') === false) {
302 $this->lastBatchProgressDetail = $detail;
303 }
304 $label = $detail !== '' ? ($stageKey . ':' . $detail) : $stageKey;
305 // The class is autoloaded by Loader.php; markInflightStage is a
306 // best-effort no-op when no AJAX context exists.
307 \ABJ_404_Solution_ViewUpdater::markInflightStage($label);
308 }
309
310 /**
311 * Run one staged view-build step and write a clear per-stage timing line.
312 *
313 * The build can span several HTTP requests. For resumable stages that yield
314 * mid-stage (S2/S4/S5), this records the time spent in the current tick and
315 * marks the status as yielded; the final tick for that stage is logged as
316 * completed.
317 *
318 * @param int $stageNumber 1-based staged build number.
319 * @param string $stageKey Stable stage key used by AJAX progress.
320 * @param callable $callback Stage work to execute.
321 * @return mixed
322 */
323 public function runTimedViewBuildStage(int $stageNumber, string $stageKey, callable $callback) {
324 $started = microtime(true);
325 // Reset per-stage so a yield marker for this stage cannot accidentally
326 // pick up a prior stage's batch detail. Inner loops (S2/S4/S5)
327 // populate this via markBuildStage() as they emit "batch X/Y" lines.
328 $this->lastBatchProgressDetail = '';
329 try {
330 $this->markViewBuildStageStarted($stageNumber, $stageKey);
331 // Public extension point. Sites can hook this for telemetry, custom
332 // progress dashboards, or chaos-testing the build's resume contract.
333 // The do_action call is inside the try so a callback that throws
334 // (test injection, host kill simulator) is treated identically to
335 // a real SQL error from the stage callback below.
336 if (function_exists('do_action')) {
337 do_action('abj404_view_build_stage_starting', $stageNumber, $stageKey);
338 }
339 $result = $callback();
340 } catch (\Throwable $e) {
341 // B17 (Bruno 2026-05-13): when the host kills our connection
342 // mid-stage (wait_timeout < build duration, MySQL errno 2006 /
343 // 2013, "MySQL server has gone away" / "Lost connection during
344 // query"), explicitly reconnect BEFORE the classifier and its
345 // option-write side effects run. queryAndGetResults() already
346 // calls ensureConnection() on its own ingress, so this is
347 // belt-and-suspenders for the catch-block path: if a future
348 // refactor moved any catch-block option write outside the DAO,
349 // a still-broken handle would silently lose the progress /
350 // streak / notice updates the classifier depends on. The
351 // explicit reconnect also pins the "resume from last completed
352 // stage, not S1" contract at the stage runner level rather
353 // than at the DAO level. ensureConnection() is idempotent
354 // (returns true when already connected) so the cost on the
355 // non-connection-drop paths is one mysqli_ping per stage exit.
356 if ($this->isTransientConnectionError($e->getMessage())) {
357 $this->ensureConnection();
358 }
359 // Catch-block classification + side effects (skip / halt / streak)
360 // live on the HostFailurePolicy trait so this orchestrator stays
361 // focused on stage sequencing. classifyAndHandleStageFailure()
362 // returns one of: 'resumable_yield', 'skipped', 'halted',
363 // 'completed' (post-S11 reconcile), or 'rethrow'.
364 $outcome = $this->classifyAndHandleStageFailure($stageNumber, $stageKey, $e->getMessage(), $started);
365 if ($outcome === 'resumable_yield') {
366 return false;
367 }
368 if ($outcome === 'skipped') {
369 return 'skipped';
370 }
371 if ($outcome === 'halted') {
372 return 'halted';
373 }
374 if ($outcome === 'completed') {
375 return null;
376 }
377 $this->logTimedViewBuildStage($stageNumber, $stageKey, 'error', $started);
378 throw $e;
379 }
380
381 $status = 'completed';
382 if ($result === false) {
383 $status = 'yielded';
384 } else if ($result === 'skipped') {
385 $status = 'skipped';
386 }
387 // Wall-clock yield (false return) implies the stage's batch loop ran
388 // far enough to exhaust the per-stage budget, which is observable
389 // forward progress. Reset the no-progress streak so legitimate
390 // long-running batched stages do not eventually trip the halt.
391 // Completion / skip likewise reset.
392 $this->resetStageNoProgressStreak($stageNumber);
393 if ($status === 'completed' || $status === 'skipped') {
394 $this->markViewBuildStageCompleted($stageNumber);
395 }
396 $this->logTimedViewBuildStage($stageNumber, $stageKey, $status, $started);
397 return $result;
398 }
399
400 /**
401 * Persist stage-start metadata before a stage does work. This survives
402 * PHP/request death where the completion marker and catch block never run.
403 *
404 * @param int $stageNumber
405 * @param string $stageKey
406 * @return void
407 */
408 public function markViewBuildStageStarted(int $stageNumber, string $stageKey): void {
409 $now = time();
410 if ($this->readProgressOption('started_at', 0) === 0) {
411 $this->writeProgressOption('started_at', $now);
412 }
413 $this->writeProgressOption('last_started_stage', $stageNumber);
414 $this->writeProgressOption('last_started_at', $now);
415 $this->viewBuildStageOpenForShutdown = true;
416 $this->viewBuildShutdownStageNumber = $stageNumber;
417 $this->viewBuildShutdownStageKey = $stageKey;
418 $this->logger->debugMessage(sprintf(
419 '[staged] build stage %d/11 %s starting',
420 $stageNumber,
421 $stageKey
422 ));
423 }
424
425 /**
426 * @param int $stageNumber
427 * @return void
428 */
429 public function markViewBuildStageCompleted(int $stageNumber): void {
430 $this->writeProgressOption('last_completed_stage', $stageNumber);
431 $this->writeProgressOption('last_completed_at', time());
432 }
433
434 /** @return void */
435 public function clearViewBuildOpenStageForShutdown(): void {
436 $this->viewBuildStageOpenForShutdown = false;
437 $this->viewBuildShutdownStageNumber = 0;
438 $this->viewBuildShutdownStageKey = '';
439 }
440
441 /** @return void */
442 public function registerViewBuildShutdownDiagnostics(): void {
443 if (self::$viewBuildShutdownLoggerRegistered || !function_exists('register_shutdown_function')) {
444 return;
445 }
446 self::$viewBuildShutdownLoggerRegistered = true;
447 register_shutdown_function(function () {
448 $this->logViewBuildShutdownDiagnostics();
449 });
450 }
451
452 /** @return void */
453 public function logViewBuildShutdownDiagnostics(): void {
454 if (!$this->viewBuildStageOpenForShutdown) {
455 return;
456 }
457 $stageNumber = $this->viewBuildShutdownStageNumber > 0
458 ? $this->viewBuildShutdownStageNumber
459 : $this->readProgressOption('last_started_stage', 0);
460 if ($stageNumber <= 0) {
461 return;
462 }
463 $lastCompleted = $this->readProgressOption('last_completed_stage', 0);
464 if ($lastCompleted >= $stageNumber) {
465 return;
466 }
467
468 $errorText = 'none';
469 if (function_exists('error_get_last')) {
470 $lastError = error_get_last();
471 if (is_array($lastError)) {
472 $message = isset($lastError['message']) && is_scalar($lastError['message'])
473 ? (string)$lastError['message'] : '';
474 $file = isset($lastError['file']) && is_scalar($lastError['file'])
475 ? (string)$lastError['file'] : '';
476 $line = isset($lastError['line']) && is_scalar($lastError['line'])
477 ? (string)$lastError['line'] : '';
478 $errorText = trim($message . ($file !== '' ? ' in ' . $file : '') . ($line !== '' ? ':' . $line : ''));
479 if ($errorText === '') {
480 $errorText = 'error_get_last returned an empty error';
481 }
482 }
483 }
484
485 $this->logger->warn(sprintf(
486 '[staged] shutdown while build stage %d/11 %s was still open; '
487 . 'last_completed_stage=%d; fatal_context=%s',
488 $stageNumber,
489 $this->viewBuildShutdownStageKey,
490 $lastCompleted,
491 substr($errorText, 0, 240)
492 ));
493 }
494
495 /**
496 * Run a non-batched stage (S3 / S9 / S10) with the kill-streak
497 * escape valve applied. Behaves like runTimedViewBuildStage() except:
498 *
499 * - Before invoking the stage, looks up the persisted kill streak
500 * for $streakOptKey. If >= 1, swaps in an extended per-query
501 * timeout (extendedTimeoutForKilledNonBatchedStage) so the
502 * SET STATEMENT max_statement_time hint can exceed the host's
503 * session limit on retry.
504 * - On `false` return (resumable kill), increments the streak so
505 * the next request resumes with the extended timeout already in
506 * effect.
507 * - On any non-`false` return (completed or 'skipped'), resets the
508 * streak to 0 -- the next rebuild starts fresh.
509 *
510 * The original $stagedQueryTimeoutSeconds is restored before
511 * returning so subsequent stages run with their own intelligent
512 * timeout, not the extended one (which was only meant for the
513 * stuck non-batched stage).
514 *
515 * @param int $stageNumber 1-based staged build number.
516 * @param string $stageKey Stable stage key for AJAX progress.
517 * @param string $streakOptKey Progress option key, e.g. 's3_kill_streak'.
518 * @param callable $callback
519 * @return mixed Forwards runTimedViewBuildStage's return value:
520 * typically true|null on completion, false on
521 * resumable kill, 'skipped' when the callback
522 * self-skips (S9 with no logs_hits table). Callers
523 * only check `=== false` so the broader type is fine.
524 */
525 public function runNonBatchedStageWithKillStreakEscape(
526 int $stageNumber,
527 string $stageKey,
528 string $streakOptKey,
529 callable $callback
530 ) {
531 $savedTimeout = $this->stagedQueryTimeoutSeconds;
532 $this->stagedQueryTimeoutSeconds = $this->extendedTimeoutForKilledNonBatchedStage($streakOptKey);
533 try {
534 $result = $this->runTimedViewBuildStage($stageNumber, $stageKey, $callback);
535 } finally {
536 $this->stagedQueryTimeoutSeconds = $savedTimeout;
537 }
538 if ($result === false) {
539 $this->writeProgressOption(
540 $streakOptKey,
541 $this->readProgressOption($streakOptKey, 0) + 1
542 );
543 } else {
544 $this->writeProgressOption($streakOptKey, 0);
545 }
546 return $result;
547 }
548
549 /**
550 * @param int $stageNumber
551 * @param string $stageKey
552 * @param string $status
553 * @param float $started
554 * @return void
555 */
556 public function logTimedViewBuildStage(int $stageNumber, string $stageKey, string $status, float $started): void {
557 $elapsedMs = (int)round((microtime(true) - $started) * 1000);
558 $markerDetail = $status . ' in ' . $elapsedMs . ' ms';
559 // Preserve mid-stage batch progress in the user-visible yield marker
560 // so the polled status does not drop from "batch 1282000/1971286
561 // (yielded; tight time)" back to a bare "yielded in N ms" between
562 // ticks. Only applied to yield-class statuses; "completed" already
563 // reads cleanly without batch context.
564 if (($status === 'yielded' || $status === 'killed_resumable')
565 && $this->lastBatchProgressDetail !== '') {
566 $markerDetail = $this->lastBatchProgressDetail . ', ' . $markerDetail;
567 }
568 $this->markBuildStage($stageKey, $markerDetail);
569 $this->logger->debugMessage(sprintf(
570 '[staged] build stage %d/11 %s %s in %d ms',
571 $stageNumber,
572 $stageKey,
573 $status,
574 $elapsedMs
575 ));
576 $this->clearViewBuildOpenStageForShutdown();
577 }
578 }
579