28s) start to risk PHP killing the request at * max_execution_time on shared hosts that default to 30s; the build * still resumes safely on the next request via MAX(id) on the buffer, * but a graceful yield is preferable. */ const VIEW_BUILD_PER_STAGE_BUDGET_SECONDS = 28; /** * After this many seconds with no progress, an abandoned partial build * is considered stale: the buffer table and high-water options are * dropped on the next entry and the build restarts from scratch. * * Bumped from 600 to 3600 (2026-05-08, deadline-math-audit-2026-05-08.md * concern #3). On Bruno-scale installs (484K redirects, slow shared * host) a full build can legitimately take longer than 10 minutes; if * the user closes the browser mid-build, the prior 600s TTL would * discard the partial buffer on the next visit and force a fresh * restart, so the build never converged across sessions. 3600s (1 * hour) is long enough to survive a normal user session gap while * still bounding stale-buffer disk cost. */ const VIEW_BUILD_RESUME_TTL_SECONDS = 3600; const VIEW_BUILD_FOREGROUND_LEASE_SECONDS = 120; /** * Upper-bound staleness threshold. When viewDoneIsServeable() serves * data older than this, an admin notice fires telling the admin the * redirects table data is out of date. The fix that lets view_done * serve stale post-invalidate data without blocking would otherwise * let arbitrarily old data render silently if the rebuild never * completes (cron stuck, repeated invalidation racing the build, * floor-kill streak halt). This notice is the honest upper bound: * stale-but-present is fine; stale-and-unsignalled is not. * * 24h matches the dedup TTL in VIEW_BUILD_DEGRADED_NOTICE_TTL_SECONDS * and the cron-stuck threshold in scheduleViewDoneRebuild() so the * three notice families speak in the same time units. */ const VIEW_DONE_HARD_STALE_NOTICE_AGE_SECONDS = 86400; /** * Sanity cap on how long an admin-initiated mutation flag (set by * markViewDoneInvalidatedByAdminMutation()) can keep view_done * unserveable. While the flag is active, viewDoneIsServeable() returns * false so the AJAX gate returns viewBuildPending and the JS poller * waits for a build that covers the mutation. If the build never * completes (cron broken, DB locked) the gate falls back to fbc270d8 * stale-serving at this timeout so the admin redirects page is not * blocked indefinitely. * * Five minutes is a comfortable upper bound for staged rebuilds on * Bruno/Troy-grade installs (~11 stages, multi-second per stage). * Above this, the admin sees stale data plus the hard-stale notice. */ const VIEW_DONE_MUTATION_INVALIDATED_SANITY_SECONDS = 300; /** * Cap on the per-query SET STATEMENT max_statement_time hint applied * to a non-batched stage (S3 / S9 / S10) on retry after a kill. The * hint is also bounded by the request's remaining PHP execution time * minus a 2s safety margin -- this constant is the absolute ceiling * regardless of how much PHP time is left. * * 240s matches the JS poller's no-progress deadline: if a single * non-batched query needs longer than that, the user-facing UI gives * up anyway, so giving the query more time would only delay the * eventual failure. */ const VIEW_BUILD_NON_BATCHED_KILL_RETRY_CAP_SECONDS = 240; /** * Floor-kill streak threshold. After this many consecutive kills at * VIEW_BUILD_MIN_BATCH_SIZE on the same stage, the build halts: the * host cannot finish the plugin's smallest unit of work, retrying * will only loop forever and trip the JS poller's no-progress * deadline. Surfaces as a "host_unfit" admin notice. */ const VIEW_BUILD_FLOOR_KILL_STREAK_HALT_THRESHOLD = 5; /** * TTL for the deduplicated admin notice transients raised when a * stage is permanently skipped or the build halts. One notice per * 24h per failure type per the self-healing reliability rules in * CLAUDE.md (notices on the plugin's own admin screen, never email, * never wp-admin-wide banner). */ const VIEW_BUILD_DEGRADED_NOTICE_TTL_SECONDS = 86400; /** * Per-stage failure policy used by the host-failure classifier. When * a stage's callback raises an error that the classifier identifies * as a permanent host-side constraint (access denied, read-only, * disk-full, quota), this map decides whether the build can degrade * gracefully past the stage ('optional': skip + advance) or must * stop retrying and surface a critical notice ('critical': halt). * * Optional stages (skippable on permanent failure): * - S3 (ALTER ADD INDEX on view_build): build runs slower without * the index, but completes correctly. * - S9 (CREATE TEMPORARY hits aggregate): hit-count column is * null/0 but the rest of the redirect listing is intact. * - S10 (ALTER ADD sort indexes): sorted reads slower, correct. * * Critical stages (halt on permanent failure): * - S1 (create build buffer): nothing can run without the buffer. * - S2 (insert redirects): empty buffer means empty published view. * - S4-S8 (UPDATE-JOIN against wp_posts/wp_terms/external/special): * resolved fields are mandatory for the rendered view; partial * resolution would produce a broken admin screen. * - S11 (RENAME swap): without the swap, view_done is never * published and the build is wasted work. * * @var array */ private const STAGE_FAILURE_POLICY = array( 1 => 'critical', 2 => 'critical', 3 => 'optional', 4 => 'critical', 5 => 'critical', 6 => 'critical', 7 => 'critical', 8 => 'critical', 9 => 'optional', 10 => 'optional', 11 => 'critical', ); /** * Look up the per-stage failure policy. Stages not registered in * STAGE_FAILURE_POLICY default to 'critical' (fail safely: any * unknown stage that fails permanently halts rather than silently * skipping data the user expects). * * @param int $stageNumber 1-based staged build number. * @return string 'optional' or 'critical'. */ public static function stageFailurePolicy(int $stageNumber): string { return self::STAGE_FAILURE_POLICY[$stageNumber] ?? 'critical'; } /** Recommended floor for memory_limit (128M) in the PHP env probe. */ const PHP_MEMORY_LIMIT_RECOMMENDED_BYTES = 134217728; /** Floor for free space on @@tmpdir's volume before warning (100MB). */ const PHP_TMPDIR_FREE_FLOOR_BYTES = 104857600; /** * Out-of-range thresholds for the operational + DDL-safety MySQL session * variables probed at S1 entry. Centralized here (rather than on the * trait) because PHP < 8.2 forbids constants in trait bodies and the * plugin supports 7.4+. */ const SESSION_PROBE_THRESHOLDS = array( 'innodb_lock_wait_timeout_min' => 30, 'tmp_table_size_min' => 16777216, 'max_heap_table_size_min' => 16777216, 'long_query_time_min' => 1.0, 'innodb_buffer_pool_size_min' => 268435456, 'wait_timeout_min' => 600, 'interactive_timeout_min' => 600, 'thread_stack_min' => 196608, 'open_files_limit_min' => 1024, 'innodb_online_alter_log_max_size_min' => 134217728, ); /** * Transient gate key for the c384 on-page-load fallback advance. * One inline advance per gate window; bursts of admin sub-requests * (prefetch, browser refresh, multiple tabs) inside the window * short-circuit so the page-load cost cannot compound. Defined * here (rather than on the consuming trait) because PHP < 8.2 * forbids constants in trait bodies. */ const PAGE_LOAD_FALLBACK_GATE_KEY = 'abj404_page_load_fallback_advance'; /** * Seconds the page-load fallback gate transient survives. 60s is * short enough that an attentive admin sees real per-load progress * (one stage per minute of navigation) and long enough that a * burst of clicks within a single working moment does not stack * inline build work. */ const PAGE_LOAD_FALLBACK_GATE_SECONDS = 60; /** * Per-stage budget seconds during a page-load fallback advance. * The admin is blocking on the response, so a single tick must * not exceed roughly the human-perceived "loading" tolerance. * Picked at 2.0s because that matches the c377 page-load contract * and stays well under the WordPress admin heartbeat default. The * fallback registers this as a ceiling via add_filter on * abj404_view_build_per_stage_budget_seconds (clamping with min(), * not overwriting, so any operator-set smaller budget wins). */ const PAGE_LOAD_FALLBACK_BUDGET_SECONDS = 2.0; private function __construct() {} }