advanceViewBuildOnce(...$arguments) * @method void assertBuildBufferExistsOrHalt(...$arguments) * @method ?bool attemptRelaxSqlModeForBuildConnection(...$arguments) * @method bool bufferIntegrityPassesForPromote(...$arguments) * @method string buildHaltTransientKey(...$arguments) * @method string buildViewDoneCountQuery(...$arguments) * @method string builtWatermarkOptionName(...$arguments) * @method int bumpMutationWatermark(...$arguments) * @method int bumpStageNoProgressStreak(...$arguments) * @method string capturedPrefixForLog(...$arguments) * @method void capturePrefixAtBuildStart(...$arguments) * @method void claimForegroundViewBuildLease(...$arguments) * @method string classifyAndHandleStageFailure(...$arguments) * @method array classifySessionVariableWarnings(...$arguments) * @method string classifyStageFailure(...$arguments) * @method void clearActiveBuildStartedWatermark(...$arguments) * @method void clearAdminMutationGateOptions(...$arguments) * @method void clearAllProgressOptions(...$arguments) * @method void clearPhpEnvironmentProbeCache(...$arguments) * @method void clearPrefixAtStageOne(...$arguments) * @method void clearSessionVariablesProbeCache(...$arguments) * @method void clearSqlModeProbeCache(...$arguments) * @method void clearStagedBuildDegradedState(...$arguments) * @method void clearViewBuildOpenStageForShutdown(...$arguments) * @method void clearViewDoneHardStaleNotice(...$arguments) * @method ABJ_404_Solution_Clock clock(...$arguments) * @method int countLiveRedirects(...$arguments) * @method int countViewBuildRows(...$arguments) * @method string describeBuildProgressForNotice(...$arguments) * @method string describeDegradedNotice(...$arguments) * @method string describeStagedSqlFailure(...$arguments) * @method array detectAndAdjustSqlMode(...$arguments) * @method float detectHostStagedQueryLimitSeconds(...$arguments) * @method string doTableNameReplacements(...$arguments) * @method void dropDeletemeTable(...$arguments) * @method void dropTransientBuffersIfPresent(...$arguments) * @method void dropTransientStagedTables(...$arguments) * @method void ensureConnection(...$arguments) * @method void ensureFallbackLockNoticeAndLog(...$arguments) * @method int extendedTimeoutForKilledNonBatchedStage(...$arguments) * @method array fetchSessionVariablesRowOrEmpty(...$arguments) * @method string filesystemEnvironmentProbeOptionName(...$arguments) * @method bool forceRestartViewBuild(...$arguments) * @method bool foregroundViewBuildLeaseActive(...$arguments) * @method string formatPhpMemoryBytesHuman(...$arguments) * @method bool gateAbortIfMutationWatermarkAdvanced(...$arguments) * @method string getColumnCollationString(...$arguments) * @method int getCronStuckHours(...$arguments) * @method string getLowercasePrefix(...$arguments) * @method array getViewBuildProgress(...$arguments) * @method array getViewBuildProgressFingerprint(...$arguments) * @method int getViewDoneBuiltAtTimestamp(...$arguments) * @method bool haltIfPrefixChangedSinceStageOne(...$arguments) * @method string humanBatchProgress(...$arguments) * @method float intelligentStagedQueryTimeoutSeconds(...$arguments) * @method void invalidateViewDoneServeableCache(...$arguments) * @method bool isBuildHaltedForHostFailure(...$arguments) * @method bool isCurrentStageOptionName(...$arguments) * @method bool isNamedLockUnsupportedError(...$arguments) * @method bool isResumableStagedKill(...$arguments) * @method bool isStageMarkedSkipped(...$arguments) * @method bool isTransientConnectionError(...$arguments) * @method string lastBuildStartedWatermarkOptionName(...$arguments) * @method string legacyStartedWatermarkOptionName(...$arguments) * @method string localizeOrDefaultViewBuildNotice(...$arguments) * @method bool logsHitsTableExists(...$arguments) * @method void logTimedViewBuildStage(...$arguments) * @method void logViewBuildProgressOptionWrite(...$arguments) * @method void logViewBuildShutdownDiagnostics(...$arguments) * @method void markBuildHaltedForHostFailure(...$arguments) * @method void markBuildStage(...$arguments) * @method void markStageSkippedForHostFailure(...$arguments) * @method void markViewBuildStageCompleted(...$arguments) * @method void markViewBuildStageStarted(...$arguments) * @method void markViewDoneBuildCompleted(...$arguments) * @method void markViewDoneInvalidatedByAdminMutation(...$arguments) * @method int maxBuildBufferId(...$arguments) * @method void maybeRaiseViewDoneHardStaleNotice(...$arguments) * @method bool mutationWatermarkAdvancedSinceBuildStart(...$arguments) * @method int mutationWatermarkObservedByAdminAction(...$arguments) * @method int mutationWatermarkObservedByAdminActionAt(...$arguments) * @method string mutationWatermarkObservedByAdminActionAtOptionName(...$arguments) * @method string mutationWatermarkObservedByAdminActionOptionName(...$arguments) * @method string normalizePathPrefix(...$arguments) * @method bool optionReadBackMatches(...$arguments) * @method int parsePhpMemoryLimitToBytes(...$arguments) * @method bool pathFallsWithinAny(...$arguments) * @method void performFreshStartCleanup(...$arguments) * @method array phpDisabledFunctionsList(...$arguments) * @method string phpEnvironmentProbeOptionName(...$arguments) * @method float phpTimeRemainingSeconds(...$arguments) * @method string prefixAtStageOneOptionName(...$arguments) * @method array probeFilesystemEnvironmentForBuild(...$arguments) * @method float probeFloatFromValues(...$arguments) * @method int probeIntFromValues(...$arguments) * @method int probeMemoryLimitForS9(...$arguments) * @method array probePhpEnvironmentForBuild(...$arguments) * @method array probeSessionVariablesAtS1Entry(...$arguments) * @method bool probeSetTimeLimitAvailability(...$arguments) * @method array probeSqlModeForBuild(...$arguments) * @method string probeStringFromValues(...$arguments) * @method string progressOptionName(...$arguments) * @method void publishBuiltWatermarkFromActiveBuildStartedWatermark(...$arguments) * @method array queryAndGetResults(...$arguments) * @method int readActiveBuildStartedWatermark(...$arguments) * @method array> readFromViewDone(...$arguments) * @method int readProgressOption(...$arguments) * @method int readWatermarkOption(...$arguments) * @method void rebuildViewDoneInBackground(...$arguments) * @method bool reconcilePostStageElevenState(...$arguments) * @method string reconcileStagedTablesAtRunnerStartup(...$arguments) * @method int recordStageBatchKilled(...$arguments) * @method void registerViewBuildShutdownDiagnostics(...$arguments) * @method bool releaseAndReacquireBetweenStages(...$arguments) * @method void releaseViewBuildLock(...$arguments) * @method void resetStageNoProgressStreak(...$arguments) * @method string resolveColumnCollationForStagedBuild(...$arguments) * @method void runForceRestartCleanupInsideLock(...$arguments) * @method bool runIdRangeBatchedUpdate(...$arguments) * @method int runInsertBatch(...$arguments) * @method mixed runNonBatchedStageWithKillStreakEscape(...$arguments) * @method array{ran: bool, reason: string, progress: array} runPageLoadFallbackAdvance(...$arguments) * @method int runRedirectsForViewCountStaged(...$arguments) * @method array> runRedirectsForViewStaged(...$arguments) * @method bool runS11SwapWithPreRenameWatermarkRecheck(...$arguments) * @method bool runStagedBuildOnce(...$arguments) * @method bool runStagedBuildStages6Through11(...$arguments) * @method void runStagedSqlFile(...$arguments) * @method void runStagedSqlFileTolerantOfDuplicateKey(...$arguments) * @method mixed runTimedViewBuildStage(...$arguments) * @method int safeCurrentMutationWatermark(...$arguments) * @method string sanitizeUrlBeforeInsert(...$arguments) * @method void scheduleViewDoneRebuild(...$arguments) * @method string sessionVariablesProbeOptionName(...$arguments) * @method void setFilesystemEnvAdminNotice(...$arguments) * @method void setLowMemoryLimitAdminNotice(...$arguments) * @method void setSessionEnvAdminNotice(...$arguments) * @method void setStagedBuildDegradedNotice(...$arguments) * @method void setStagedBuildHaltNotice(...$arguments) * @method void setViewBuildCronStuckNotice(...$arguments) * @method void setViewBuildScheduleFailedNotice(...$arguments) * @method void setViewDoneHardStaleNotice(...$arguments) * @method array splitOpenBasedirPaths(...$arguments) * @method string sqlModeProbeOptionName(...$arguments) * @method void stageAddPreJoinIndexes(...$arguments) * @method void stageAddSortIndexes(...$arguments) * @method void stageCreateBuildTable(...$arguments) * @method array stagedQueryOptions(...$arguments) * @method bool stagedTableExists(...$arguments) * @method bool stageInsertRedirectsBatched(...$arguments) * @method string stageNoProgressStreakOptionName(...$arguments) * @method void stageRenameSwap(...$arguments) * @method string stageSkipOptionName(...$arguments) * @method void stageUpdateExternal(...$arguments) * @method void stageUpdateHits(...$arguments) * @method void stageUpdateHome(...$arguments) * @method bool stageUpdatePostsBatched(...$arguments) * @method void stageUpdateSpecial(...$arguments) * @method bool stageUpdateTermsBatched(...$arguments) * @method void stampStartedWatermarksAtS1Entry(...$arguments) * @method void sweepStaleRebuildTransients(...$arguments) * @method string transientFallbackLockOptionName(...$arguments) * @method bool verifyBuildLockSerializesWriter(...$arguments) * @method bool verifyOptionWriteCoherent(...$arguments) * @method bool verifyPrefixUnchangedSinceStageOne(...$arguments) * @method int viewBuildBatchSize(...$arguments) * @method int viewBuildBatchSizeForStage(...$arguments) * @method array viewBuildOnlyTranslations(...$arguments) * @method float viewBuildPerStageBudgetSeconds(...$arguments) * @method string viewBuildTableName(...$arguments) * @method string viewDeletemeTableName(...$arguments) * @method int viewDoneBuiltAt(...$arguments) * @method int viewDoneBuiltWatermark(...$arguments) * @method int viewDoneDataBuiltAt(...$arguments) * @method string viewDoneDataBuiltAtOptionName(...$arguments) * @method string viewDoneFreshnessOptionName(...$arguments) * @method bool viewDoneHasRows(...$arguments) * @method bool viewDoneIsFresh(...$arguments) * @method bool viewDoneIsServeable(...$arguments) * @method int viewDoneMutationInvalidatedAt(...$arguments) * @method string viewDoneMutationInvalidatedAtOptionName(...$arguments) * @method bool viewDoneTableExists(...$arguments) * @method string viewDoneTableName(...$arguments) * @method void writeProgressOption(...$arguments) * @method void writeWatermarkOption(...$arguments) */ class ABJ_404_Solution_ViewBuildAdaptive extends ABJ_404_Solution_ViewBuildCollaborator { /** * Request-lifetime cache of the host's per-statement timeout, in * seconds. -1 means "not yet probed", 0 means "no host limit", > 0 * is the limit the host enforces. Probed lazily by * detectHostStagedQueryLimitSeconds(). * * @var float */ private $hostStagedQueryLimitSecondsCache = -1.0; /** * Per-stage batch size with adaptive-shrink memory. Reads the * persisted shrink option for this stage (s2_batch_size / * s4_batch_size / s5_batch_size); falls back to the global default * when none is set. The persisted value survives across requests so * a host that has already shown it cannot handle 2000-row batches * keeps using the smaller size for the rest of the build. * * @param string $stageShortKey One of 's2_batch_size', 's4_batch_size', 's5_batch_size'. * @return int Always >= VIEW_BUILD_MIN_BATCH_SIZE. */ public function viewBuildBatchSizeForStage(string $stageShortKey): int { $defaultSize = $this->viewBuildBatchSize(); $persisted = $this->readProgressOption($stageShortKey, 0); $effective = $persisted > 0 ? $persisted : $defaultSize; return max(ABJ_404_Solution_ViewBuildConfig::VIEW_BUILD_MIN_BATCH_SIZE, $effective); } /** * Record that a batch in stage $stageShortKey was killed by the host * (resumable kill class: max_statement_time exceeded, gone-away, * lock-wait). Halves the batch size, floors at * VIEW_BUILD_MIN_BATCH_SIZE, persists. Subsequent ticks pick up the * smaller size via viewBuildBatchSizeForStage(). * * @param string $stageShortKey * @return int the new batch size. */ public function recordStageBatchKilled(string $stageShortKey): int { $current = $this->viewBuildBatchSizeForStage($stageShortKey); $shrunk = (int)max( ABJ_404_Solution_ViewBuildConfig::VIEW_BUILD_MIN_BATCH_SIZE, (int)floor($current / 2) ); $this->writeProgressOption($stageShortKey, $shrunk); return $shrunk; } /** * Seconds of PHP request time remaining before max_execution_time * fires. PHP_INT_MAX when no limit is set (CLI / unbounded cron). * * Used by the batched stages to decide whether there is room to * start another batch at our full per-query limit. If not, the stage * yields via the wall-clock path (no batch attempt, no shrink) and * the next request resumes with a fresh PHP time budget. * * @return float */ public function phpTimeRemainingSeconds(): float { $limit = (int)ini_get('max_execution_time'); if ($limit <= 0) { return (float)PHP_INT_MAX; } $start = isset($_SERVER['REQUEST_TIME_FLOAT']) && is_numeric($_SERVER['REQUEST_TIME_FLOAT']) ? (float)$_SERVER['REQUEST_TIME_FLOAT'] : (float)microtime(true); $elapsed = max(0.0, microtime(true) - $start); return max(0.0, (float)$limit - $elapsed); } /** * Probe the host for its session-level per-statement timeout. Reads * the MariaDB session variable max_statement_time (seconds, decimal) * first, then falls back to MySQL max_execution_time (milliseconds). * Returns 0.0 when no host limit is set. Cached on the instance for * the request lifetime so the build pays the SHOW VARIABLES cost * once, not once per stage. * * @return float Seconds, or 0.0 for "no host limit". */ public function detectHostStagedQueryLimitSeconds(): float { if ($this->hostStagedQueryLimitSecondsCache >= 0.0) { return $this->hostStagedQueryLimitSecondsCache; } $limitSeconds = 0.0; $result = $this->queryAndGetResults( "SHOW SESSION VARIABLES LIKE 'max_statement_time'", array('log_errors' => false) ); $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array(); if (!empty($rows) && is_array($rows[0])) { $value = $rows[0]['Value'] ?? ($rows[0]['value'] ?? null); if ($value !== null && is_numeric($value) && (float)$value > 0.0) { $limitSeconds = (float)$value; } } if ($limitSeconds <= 0.0) { $result = $this->queryAndGetResults( "SHOW SESSION VARIABLES LIKE 'max_execution_time'", array('log_errors' => false) ); $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array(); if (!empty($rows) && is_array($rows[0])) { $value = $rows[0]['Value'] ?? ($rows[0]['value'] ?? null); if ($value !== null && is_numeric($value) && (int)$value > 0) { $limitSeconds = ((int)$value) / 1000.0; } } } $this->hostStagedQueryLimitSecondsCache = max(0.0, $limitSeconds); return $this->hostStagedQueryLimitSecondsCache; } /** * Compute the per-query timeout we hint to the database for a * single staged-build query. The result is the smallest of: * * - our per-stage budget minus a 2s margin (so the hint fires * before PHP max_execution_time can interrupt the request) * - the host max_statement_time minus 1s (so the hint fires * before the host silent kill, giving us a clean classifiable * error rather than a dropped connection) * * Floored at 1s. The whole point of the function is to fire OUR * kill before the host's; with the prior 5s floor, on hosts with * `max_statement_time = 3` we would emit a 5s hint that the host * pre-empts at 3s, defeating the classifiable-kill design. 1s is * the smallest sensible floor (sub-second queries are noise) but * still lets the function honor genuinely-tight host limits. * (2026-05-08, deadline-math-audit-2026-05-08.md concern #2.) * * Our-limit floor stays at 5s: that one represents "this query * is so small that the per-stage budget overhead dominates" and * has nothing to do with the host kill. The host-limit code path * uses 1s. * * When the host has no limit set, only the per-stage budget applies. * * @return float Seconds. */ public function intelligentStagedQueryTimeoutSeconds(): float { $ourLimit = max(5.0, (float)$this->viewBuildPerStageBudgetSeconds() - 2.0); $hostLimit = $this->detectHostStagedQueryLimitSeconds(); if ($hostLimit > 0.0) { return max(1.0, min($ourLimit, $hostLimit - 1.0)); } return $ourLimit; } /** * Per-query timeout for the next attempt of a non-batched stage that * has been killed at least once already. When the persisted streak is * 0 (no prior kill on this stage in the current build) returns the * normal intelligent timeout; otherwise returns an extended timeout * that intentionally overrides the host's session max_statement_time. * * The override works because MariaDB 10.1+ honors * `SET STATEMENT max_statement_time=N FOR ` even when N is * larger than the session limit: SET STATEMENT scopes the override * to the wrapped statement only. Without this, S3 / S9 / S10 -- the * non-batched stages -- would hit the host limit and retry with the * same timeout forever, looping with no escape valve (the batched * stages have one in the form of adaptive batch shrink; non-batched * stages don't, so we extend in the time dimension instead). * * Bounded by: * - VIEW_BUILD_NON_BATCHED_KILL_RETRY_CAP_SECONDS (absolute ceiling) * - phpTimeRemainingSeconds() - 2.0 (so the request returns inside * PHP max_execution_time even if the query still gets killed) * - max(1.0, ...) so we never ship a non-positive hint * * @param string $stageKillStreakOptKey Progress option key name, * e.g. 's3_kill_streak'. Production callers register the key in * the staged trait's progress option name map. * @return int Seconds. */ public function extendedTimeoutForKilledNonBatchedStage(string $stageKillStreakOptKey): int { $streak = $this->readProgressOption($stageKillStreakOptKey, 0); if ($streak <= 0) { return (int)round($this->intelligentStagedQueryTimeoutSeconds()); } $cap = (float)ABJ_404_Solution_ViewBuildConfig::VIEW_BUILD_NON_BATCHED_KILL_RETRY_CAP_SECONDS; $phpRemaining = max(1.0, $this->phpTimeRemainingSeconds() - 2.0); return (int)round(max(1.0, min($cap, $phpRemaining))); } }