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 / ViewBuildAdaptive.php

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

428 lines 21.5 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 * Adaptive-runtime helpers for the staged view-build pipeline.
9 *
10 * Two responsibilities, both keyed off behavior the build only learns at
11 * runtime on the actual host:
12 *
13 * 1. Adaptive batch size: when a host kills a batched stage's per-query
14 * (max_statement_time exceeded, lock-wait, gone-away), halve the
15 * batch size for that stage and persist it. Subsequent ticks of the
16 * same build use the smaller size, so a slow shared host eventually
17 * converges to a batch size it can actually finish.
18 *
19 * 2. Intelligent per-query timeout: probe the host's session-level
20 * max_statement_time (MariaDB) or max_execution_time (MySQL) once
21 * per request, then size our own per-query SET STATEMENT hint to
22 * fire just before the host's silent kill would. This converts a
23 * generic connection drop into a clean classifiable kill the
24 * pipeline can resume from.
25 *
26 * Sibling to ABJ_404_Solution_DataAccess_ViewQueriesStagedTrait; both are
27 * mixed into ABJ_404_Solution_DataAccess. Private members declared here
28 * are visible to the staged-build trait inside the composing class.
29 *
30 * @property ABJ_404_Solution_DatabaseCore $dbCore
31 * @property ABJ_404_Solution_Functions $f
32 * @property ABJ_404_Solution_Logging $logger
33 * @property ABJ_404_Solution_ViewReadService|null $viewReadService
34 * @property ABJ_404_Solution_LogsRepository|null $logsRepo
35 * @property int $stagedQueryTimeoutSeconds
36 * @property string $lastBatchProgressDetail
37 * @property bool $viewBuildStageOpenForShutdown
38 * @property int $viewBuildShutdownStageNumber
39 * @property string $viewBuildShutdownStageKey
40 * @property bool|null $namedLockSupportedThisRequest
41 * @property bool $fallbackLockLoggedThisRequest
42 * @property bool $usingTransientFallbackLock
43 * @property string $lastNamedLockUnsupportedReason
44 * @property string $lastNamedLockUnsupportedError
45 * @method void abortStagedBuildForMutationWatermarkAdvance(...$arguments)
46 * @method bool acquireTransientFallbackLock(...$arguments)
47 * @method bool acquireViewBuildLock(...$arguments)
48 * @method string activeBuildStartedWatermarkOptionName(...$arguments)
49 * @method bool adminMutationGateBlocks(...$arguments)
50 * @method array<mixed> advanceViewBuildOnce(...$arguments)
51 * @method void assertBuildBufferExistsOrHalt(...$arguments)
52 * @method ?bool attemptRelaxSqlModeForBuildConnection(...$arguments)
53 * @method bool bufferIntegrityPassesForPromote(...$arguments)
54 * @method string buildHaltTransientKey(...$arguments)
55 * @method string buildViewDoneCountQuery(...$arguments)
56 * @method string builtWatermarkOptionName(...$arguments)
57 * @method int bumpMutationWatermark(...$arguments)
58 * @method int bumpStageNoProgressStreak(...$arguments)
59 * @method string capturedPrefixForLog(...$arguments)
60 * @method void capturePrefixAtBuildStart(...$arguments)
61 * @method void claimForegroundViewBuildLease(...$arguments)
62 * @method string classifyAndHandleStageFailure(...$arguments)
63 * @method array<mixed> classifySessionVariableWarnings(...$arguments)
64 * @method string classifyStageFailure(...$arguments)
65 * @method void clearActiveBuildStartedWatermark(...$arguments)
66 * @method void clearAdminMutationGateOptions(...$arguments)
67 * @method void clearAllProgressOptions(...$arguments)
68 * @method void clearPhpEnvironmentProbeCache(...$arguments)
69 * @method void clearPrefixAtStageOne(...$arguments)
70 * @method void clearSessionVariablesProbeCache(...$arguments)
71 * @method void clearSqlModeProbeCache(...$arguments)
72 * @method void clearStagedBuildDegradedState(...$arguments)
73 * @method void clearViewBuildOpenStageForShutdown(...$arguments)
74 * @method void clearViewDoneHardStaleNotice(...$arguments)
75 * @method ABJ_404_Solution_Clock clock(...$arguments)
76 * @method int countLiveRedirects(...$arguments)
77 * @method int countViewBuildRows(...$arguments)
78 * @method string describeBuildProgressForNotice(...$arguments)
79 * @method string describeDegradedNotice(...$arguments)
80 * @method string describeStagedSqlFailure(...$arguments)
81 * @method array<mixed> detectAndAdjustSqlMode(...$arguments)
82 * @method float detectHostStagedQueryLimitSeconds(...$arguments)
83 * @method string doTableNameReplacements(...$arguments)
84 * @method void dropDeletemeTable(...$arguments)
85 * @method void dropTransientBuffersIfPresent(...$arguments)
86 * @method void dropTransientStagedTables(...$arguments)
87 * @method void ensureConnection(...$arguments)
88 * @method void ensureFallbackLockNoticeAndLog(...$arguments)
89 * @method int extendedTimeoutForKilledNonBatchedStage(...$arguments)
90 * @method array<mixed> fetchSessionVariablesRowOrEmpty(...$arguments)
91 * @method string filesystemEnvironmentProbeOptionName(...$arguments)
92 * @method bool forceRestartViewBuild(...$arguments)
93 * @method bool foregroundViewBuildLeaseActive(...$arguments)
94 * @method string formatPhpMemoryBytesHuman(...$arguments)
95 * @method bool gateAbortIfMutationWatermarkAdvanced(...$arguments)
96 * @method string getColumnCollationString(...$arguments)
97 * @method int getCronStuckHours(...$arguments)
98 * @method string getLowercasePrefix(...$arguments)
99 * @method array<string, mixed> getViewBuildProgress(...$arguments)
100 * @method array<mixed> getViewBuildProgressFingerprint(...$arguments)
101 * @method int getViewDoneBuiltAtTimestamp(...$arguments)
102 * @method bool haltIfPrefixChangedSinceStageOne(...$arguments)
103 * @method string humanBatchProgress(...$arguments)
104 * @method float intelligentStagedQueryTimeoutSeconds(...$arguments)
105 * @method void invalidateViewDoneServeableCache(...$arguments)
106 * @method bool isBuildHaltedForHostFailure(...$arguments)
107 * @method bool isCurrentStageOptionName(...$arguments)
108 * @method bool isNamedLockUnsupportedError(...$arguments)
109 * @method bool isResumableStagedKill(...$arguments)
110 * @method bool isStageMarkedSkipped(...$arguments)
111 * @method bool isTransientConnectionError(...$arguments)
112 * @method string lastBuildStartedWatermarkOptionName(...$arguments)
113 * @method string legacyStartedWatermarkOptionName(...$arguments)
114 * @method string localizeOrDefaultViewBuildNotice(...$arguments)
115 * @method bool logsHitsTableExists(...$arguments)
116 * @method void logTimedViewBuildStage(...$arguments)
117 * @method void logViewBuildProgressOptionWrite(...$arguments)
118 * @method void logViewBuildShutdownDiagnostics(...$arguments)
119 * @method void markBuildHaltedForHostFailure(...$arguments)
120 * @method void markBuildStage(...$arguments)
121 * @method void markStageSkippedForHostFailure(...$arguments)
122 * @method void markViewBuildStageCompleted(...$arguments)
123 * @method void markViewBuildStageStarted(...$arguments)
124 * @method void markViewDoneBuildCompleted(...$arguments)
125 * @method void markViewDoneInvalidatedByAdminMutation(...$arguments)
126 * @method int maxBuildBufferId(...$arguments)
127 * @method void maybeRaiseViewDoneHardStaleNotice(...$arguments)
128 * @method bool mutationWatermarkAdvancedSinceBuildStart(...$arguments)
129 * @method int mutationWatermarkObservedByAdminAction(...$arguments)
130 * @method int mutationWatermarkObservedByAdminActionAt(...$arguments)
131 * @method string mutationWatermarkObservedByAdminActionAtOptionName(...$arguments)
132 * @method string mutationWatermarkObservedByAdminActionOptionName(...$arguments)
133 * @method string normalizePathPrefix(...$arguments)
134 * @method bool optionReadBackMatches(...$arguments)
135 * @method int parsePhpMemoryLimitToBytes(...$arguments)
136 * @method bool pathFallsWithinAny(...$arguments)
137 * @method void performFreshStartCleanup(...$arguments)
138 * @method array<mixed> phpDisabledFunctionsList(...$arguments)
139 * @method string phpEnvironmentProbeOptionName(...$arguments)
140 * @method float phpTimeRemainingSeconds(...$arguments)
141 * @method string prefixAtStageOneOptionName(...$arguments)
142 * @method array<mixed> probeFilesystemEnvironmentForBuild(...$arguments)
143 * @method float probeFloatFromValues(...$arguments)
144 * @method int probeIntFromValues(...$arguments)
145 * @method int probeMemoryLimitForS9(...$arguments)
146 * @method array<mixed> probePhpEnvironmentForBuild(...$arguments)
147 * @method array<mixed> probeSessionVariablesAtS1Entry(...$arguments)
148 * @method bool probeSetTimeLimitAvailability(...$arguments)
149 * @method array<mixed> probeSqlModeForBuild(...$arguments)
150 * @method string probeStringFromValues(...$arguments)
151 * @method string progressOptionName(...$arguments)
152 * @method void publishBuiltWatermarkFromActiveBuildStartedWatermark(...$arguments)
153 * @method array<mixed> queryAndGetResults(...$arguments)
154 * @method int readActiveBuildStartedWatermark(...$arguments)
155 * @method array<int, array<string, mixed>> readFromViewDone(...$arguments)
156 * @method int readProgressOption(...$arguments)
157 * @method int readWatermarkOption(...$arguments)
158 * @method void rebuildViewDoneInBackground(...$arguments)
159 * @method bool reconcilePostStageElevenState(...$arguments)
160 * @method string reconcileStagedTablesAtRunnerStartup(...$arguments)
161 * @method int recordStageBatchKilled(...$arguments)
162 * @method void registerViewBuildShutdownDiagnostics(...$arguments)
163 * @method bool releaseAndReacquireBetweenStages(...$arguments)
164 * @method void releaseViewBuildLock(...$arguments)
165 * @method void resetStageNoProgressStreak(...$arguments)
166 * @method string resolveColumnCollationForStagedBuild(...$arguments)
167 * @method void runForceRestartCleanupInsideLock(...$arguments)
168 * @method bool runIdRangeBatchedUpdate(...$arguments)
169 * @method int runInsertBatch(...$arguments)
170 * @method mixed runNonBatchedStageWithKillStreakEscape(...$arguments)
171 * @method array{ran: bool, reason: string, progress: array<string, mixed>} runPageLoadFallbackAdvance(...$arguments)
172 * @method int runRedirectsForViewCountStaged(...$arguments)
173 * @method array<int, array<string, mixed>> runRedirectsForViewStaged(...$arguments)
174 * @method bool runS11SwapWithPreRenameWatermarkRecheck(...$arguments)
175 * @method bool runStagedBuildOnce(...$arguments)
176 * @method bool runStagedBuildStages6Through11(...$arguments)
177 * @method void runStagedSqlFile(...$arguments)
178 * @method void runStagedSqlFileTolerantOfDuplicateKey(...$arguments)
179 * @method mixed runTimedViewBuildStage(...$arguments)
180 * @method int safeCurrentMutationWatermark(...$arguments)
181 * @method string sanitizeUrlBeforeInsert(...$arguments)
182 * @method void scheduleViewDoneRebuild(...$arguments)
183 * @method string sessionVariablesProbeOptionName(...$arguments)
184 * @method void setFilesystemEnvAdminNotice(...$arguments)
185 * @method void setLowMemoryLimitAdminNotice(...$arguments)
186 * @method void setSessionEnvAdminNotice(...$arguments)
187 * @method void setStagedBuildDegradedNotice(...$arguments)
188 * @method void setStagedBuildHaltNotice(...$arguments)
189 * @method void setViewBuildCronStuckNotice(...$arguments)
190 * @method void setViewBuildScheduleFailedNotice(...$arguments)
191 * @method void setViewDoneHardStaleNotice(...$arguments)
192 * @method array<mixed> splitOpenBasedirPaths(...$arguments)
193 * @method string sqlModeProbeOptionName(...$arguments)
194 * @method void stageAddPreJoinIndexes(...$arguments)
195 * @method void stageAddSortIndexes(...$arguments)
196 * @method void stageCreateBuildTable(...$arguments)
197 * @method array<string, mixed> stagedQueryOptions(...$arguments)
198 * @method bool stagedTableExists(...$arguments)
199 * @method bool stageInsertRedirectsBatched(...$arguments)
200 * @method string stageNoProgressStreakOptionName(...$arguments)
201 * @method void stageRenameSwap(...$arguments)
202 * @method string stageSkipOptionName(...$arguments)
203 * @method void stageUpdateExternal(...$arguments)
204 * @method void stageUpdateHits(...$arguments)
205 * @method void stageUpdateHome(...$arguments)
206 * @method bool stageUpdatePostsBatched(...$arguments)
207 * @method void stageUpdateSpecial(...$arguments)
208 * @method bool stageUpdateTermsBatched(...$arguments)
209 * @method void stampStartedWatermarksAtS1Entry(...$arguments)
210 * @method void sweepStaleRebuildTransients(...$arguments)
211 * @method string transientFallbackLockOptionName(...$arguments)
212 * @method bool verifyBuildLockSerializesWriter(...$arguments)
213 * @method bool verifyOptionWriteCoherent(...$arguments)
214 * @method bool verifyPrefixUnchangedSinceStageOne(...$arguments)
215 * @method int viewBuildBatchSize(...$arguments)
216 * @method int viewBuildBatchSizeForStage(...$arguments)
217 * @method array<mixed> viewBuildOnlyTranslations(...$arguments)
218 * @method float viewBuildPerStageBudgetSeconds(...$arguments)
219 * @method string viewBuildTableName(...$arguments)
220 * @method string viewDeletemeTableName(...$arguments)
221 * @method int viewDoneBuiltAt(...$arguments)
222 * @method int viewDoneBuiltWatermark(...$arguments)
223 * @method int viewDoneDataBuiltAt(...$arguments)
224 * @method string viewDoneDataBuiltAtOptionName(...$arguments)
225 * @method string viewDoneFreshnessOptionName(...$arguments)
226 * @method bool viewDoneHasRows(...$arguments)
227 * @method bool viewDoneIsFresh(...$arguments)
228 * @method bool viewDoneIsServeable(...$arguments)
229 * @method int viewDoneMutationInvalidatedAt(...$arguments)
230 * @method string viewDoneMutationInvalidatedAtOptionName(...$arguments)
231 * @method bool viewDoneTableExists(...$arguments)
232 * @method string viewDoneTableName(...$arguments)
233 * @method void writeProgressOption(...$arguments)
234 * @method void writeWatermarkOption(...$arguments)
235 */
236 class ABJ_404_Solution_ViewBuildAdaptive extends ABJ_404_Solution_ViewBuildCollaborator {
237
238 /**
239 * Request-lifetime cache of the host's per-statement timeout, in
240 * seconds. -1 means "not yet probed", 0 means "no host limit", > 0
241 * is the limit the host enforces. Probed lazily by
242 * detectHostStagedQueryLimitSeconds().
243 *
244 * @var float
245 */
246 private $hostStagedQueryLimitSecondsCache = -1.0;
247
248 /**
249 * Per-stage batch size with adaptive-shrink memory. Reads the
250 * persisted shrink option for this stage (s2_batch_size /
251 * s4_batch_size / s5_batch_size); falls back to the global default
252 * when none is set. The persisted value survives across requests so
253 * a host that has already shown it cannot handle 2000-row batches
254 * keeps using the smaller size for the rest of the build.
255 *
256 * @param string $stageShortKey One of 's2_batch_size', 's4_batch_size', 's5_batch_size'.
257 * @return int Always >= VIEW_BUILD_MIN_BATCH_SIZE.
258 */
259 public function viewBuildBatchSizeForStage(string $stageShortKey): int {
260 $defaultSize = $this->viewBuildBatchSize();
261 $persisted = $this->readProgressOption($stageShortKey, 0);
262 $effective = $persisted > 0 ? $persisted : $defaultSize;
263 return max(ABJ_404_Solution_ViewBuildConfig::VIEW_BUILD_MIN_BATCH_SIZE, $effective);
264 }
265
266 /**
267 * Record that a batch in stage $stageShortKey was killed by the host
268 * (resumable kill class: max_statement_time exceeded, gone-away,
269 * lock-wait). Halves the batch size, floors at
270 * VIEW_BUILD_MIN_BATCH_SIZE, persists. Subsequent ticks pick up the
271 * smaller size via viewBuildBatchSizeForStage().
272 *
273 * @param string $stageShortKey
274 * @return int the new batch size.
275 */
276 public function recordStageBatchKilled(string $stageShortKey): int {
277 $current = $this->viewBuildBatchSizeForStage($stageShortKey);
278 $shrunk = (int)max(
279 ABJ_404_Solution_ViewBuildConfig::VIEW_BUILD_MIN_BATCH_SIZE,
280 (int)floor($current / 2)
281 );
282 $this->writeProgressOption($stageShortKey, $shrunk);
283 return $shrunk;
284 }
285
286 /**
287 * Seconds of PHP request time remaining before max_execution_time
288 * fires. PHP_INT_MAX when no limit is set (CLI / unbounded cron).
289 *
290 * Used by the batched stages to decide whether there is room to
291 * start another batch at our full per-query limit. If not, the stage
292 * yields via the wall-clock path (no batch attempt, no shrink) and
293 * the next request resumes with a fresh PHP time budget.
294 *
295 * @return float
296 */
297 public function phpTimeRemainingSeconds(): float {
298 $limit = (int)ini_get('max_execution_time');
299 if ($limit <= 0) {
300 return (float)PHP_INT_MAX;
301 }
302 $start = isset($_SERVER['REQUEST_TIME_FLOAT']) && is_numeric($_SERVER['REQUEST_TIME_FLOAT'])
303 ? (float)$_SERVER['REQUEST_TIME_FLOAT']
304 : (float)microtime(true);
305 $elapsed = max(0.0, microtime(true) - $start);
306 return max(0.0, (float)$limit - $elapsed);
307 }
308
309 /**
310 * Probe the host for its session-level per-statement timeout. Reads
311 * the MariaDB session variable max_statement_time (seconds, decimal)
312 * first, then falls back to MySQL max_execution_time (milliseconds).
313 * Returns 0.0 when no host limit is set. Cached on the instance for
314 * the request lifetime so the build pays the SHOW VARIABLES cost
315 * once, not once per stage.
316 *
317 * @return float Seconds, or 0.0 for "no host limit".
318 */
319 public function detectHostStagedQueryLimitSeconds(): float {
320 if ($this->hostStagedQueryLimitSecondsCache >= 0.0) {
321 return $this->hostStagedQueryLimitSecondsCache;
322 }
323 $limitSeconds = 0.0;
324
325 $result = $this->queryAndGetResults(
326 "SHOW SESSION VARIABLES LIKE 'max_statement_time'",
327 array('log_errors' => false)
328 );
329 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
330 if (!empty($rows) && is_array($rows[0])) {
331 $value = $rows[0]['Value'] ?? ($rows[0]['value'] ?? null);
332 if ($value !== null && is_numeric($value) && (float)$value > 0.0) {
333 $limitSeconds = (float)$value;
334 }
335 }
336
337 if ($limitSeconds <= 0.0) {
338 $result = $this->queryAndGetResults(
339 "SHOW SESSION VARIABLES LIKE 'max_execution_time'",
340 array('log_errors' => false)
341 );
342 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
343 if (!empty($rows) && is_array($rows[0])) {
344 $value = $rows[0]['Value'] ?? ($rows[0]['value'] ?? null);
345 if ($value !== null && is_numeric($value) && (int)$value > 0) {
346 $limitSeconds = ((int)$value) / 1000.0;
347 }
348 }
349 }
350
351 $this->hostStagedQueryLimitSecondsCache = max(0.0, $limitSeconds);
352 return $this->hostStagedQueryLimitSecondsCache;
353 }
354
355 /**
356 * Compute the per-query timeout we hint to the database for a
357 * single staged-build query. The result is the smallest of:
358 *
359 * - our per-stage budget minus a 2s margin (so the hint fires
360 * before PHP max_execution_time can interrupt the request)
361 * - the host max_statement_time minus 1s (so the hint fires
362 * before the host silent kill, giving us a clean classifiable
363 * error rather than a dropped connection)
364 *
365 * Floored at 1s. The whole point of the function is to fire OUR
366 * kill before the host's; with the prior 5s floor, on hosts with
367 * `max_statement_time = 3` we would emit a 5s hint that the host
368 * pre-empts at 3s, defeating the classifiable-kill design. 1s is
369 * the smallest sensible floor (sub-second queries are noise) but
370 * still lets the function honor genuinely-tight host limits.
371 * (2026-05-08, deadline-math-audit-2026-05-08.md concern #2.)
372 *
373 * Our-limit floor stays at 5s: that one represents "this query
374 * is so small that the per-stage budget overhead dominates" and
375 * has nothing to do with the host kill. The host-limit code path
376 * uses 1s.
377 *
378 * When the host has no limit set, only the per-stage budget applies.
379 *
380 * @return float Seconds.
381 */
382 public function intelligentStagedQueryTimeoutSeconds(): float {
383 $ourLimit = max(5.0, (float)$this->viewBuildPerStageBudgetSeconds() - 2.0);
384 $hostLimit = $this->detectHostStagedQueryLimitSeconds();
385 if ($hostLimit > 0.0) {
386 return max(1.0, min($ourLimit, $hostLimit - 1.0));
387 }
388 return $ourLimit;
389 }
390
391 /**
392 * Per-query timeout for the next attempt of a non-batched stage that
393 * has been killed at least once already. When the persisted streak is
394 * 0 (no prior kill on this stage in the current build) returns the
395 * normal intelligent timeout; otherwise returns an extended timeout
396 * that intentionally overrides the host's session max_statement_time.
397 *
398 * The override works because MariaDB 10.1+ honors
399 * `SET STATEMENT max_statement_time=N FOR <query>` even when N is
400 * larger than the session limit: SET STATEMENT scopes the override
401 * to the wrapped statement only. Without this, S3 / S9 / S10 -- the
402 * non-batched stages -- would hit the host limit and retry with the
403 * same timeout forever, looping with no escape valve (the batched
404 * stages have one in the form of adaptive batch shrink; non-batched
405 * stages don't, so we extend in the time dimension instead).
406 *
407 * Bounded by:
408 * - VIEW_BUILD_NON_BATCHED_KILL_RETRY_CAP_SECONDS (absolute ceiling)
409 * - phpTimeRemainingSeconds() - 2.0 (so the request returns inside
410 * PHP max_execution_time even if the query still gets killed)
411 * - max(1.0, ...) so we never ship a non-positive hint
412 *
413 * @param string $stageKillStreakOptKey Progress option key name,
414 * e.g. 's3_kill_streak'. Production callers register the key in
415 * the staged trait's progress option name map.
416 * @return int Seconds.
417 */
418 public function extendedTimeoutForKilledNonBatchedStage(string $stageKillStreakOptKey): int {
419 $streak = $this->readProgressOption($stageKillStreakOptKey, 0);
420 if ($streak <= 0) {
421 return (int)round($this->intelligentStagedQueryTimeoutSeconds());
422 }
423 $cap = (float)ABJ_404_Solution_ViewBuildConfig::VIEW_BUILD_NON_BATCHED_KILL_RETRY_CAP_SECONDS;
424 $phpRemaining = max(1.0, $this->phpTimeRemainingSeconds() - 2.0);
425 return (int)round(max(1.0, min($cap, $phpRemaining)));
426 }
427 }
428