PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.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 / view-build / ViewBuildConfig.php

ViewBuildConfig.php in 404 Solution 4.3.0, at includes/view-build/ViewBuildConfig.php

262 lines 11.8 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 * Tunable constants for the staged getRedirectsForView rebuild pipeline.
9 *
10 * These would naturally live as `const` declarations on the stage pipeline,
11 * but PHP 7.4 (the
12 * declared minimum, see Requires PHP in 404-solution.php) does not allow
13 * constants inside a trait body, that arrived in PHP 8.2. They sat on
14 * ABJ_404_Solution_DataAccess for one release (4.1.14) and pushed that
15 * file past the 1500-line cap, so they were extracted here.
16 *
17 * Override at runtime via the corresponding define() (for build-stage
18 * timeout tuning, see ABJ404_VIEW_BUILD_PER_STAGE_BUDGET_SECONDS) or via
19 * the matching abj404_view_build_* filters where wired.
20 */
21 final class ABJ_404_Solution_ViewBuildConfig {
22
23 const VIEW_DONE_FRESHNESS_TTL_SECONDS = 120;
24 const VIEW_DONE_BUILD_LOCK_NAME = 'abj404_view_build';
25
26 /**
27 * Safety-net TTL for the option-row used as the fallback build lock on
28 * managed/sharded MySQL providers (PlanetScale, Vitess, ProxySQL splits)
29 * where session-scoped GET_LOCK is unavailable. Sized at one full
30 * advance-tick budget plus headroom: a single request never holds the
31 * lock longer than VIEW_BUILD_PER_STAGE_BUDGET_SECONDS, but a crashed
32 * request must not strand the lock indefinitely. Production code
33 * always releases the option in `releaseViewBuildLock`; this TTL only
34 * applies when the releasing request died (PHP fatal, OOM, lost
35 * connection) and the next worker needs to take over.
36 */
37 const VIEW_BUILD_TRANSIENT_LOCK_TTL_SECONDS = 600;
38
39 /**
40 * Default batch size for the resumable bulk INSERT (S2) and per-id-range
41 * UPDATEs (S4/S5). Tuned to fit comfortably within a single per-query
42 * timeout on a slow shared host (5 to 10s typical). Override via define()
43 * or the abj404_view_build_batch_size filter.
44 */
45 const VIEW_BUILD_DEFAULT_BATCH_SIZE = 2000;
46
47 /**
48 * Floor for the adaptive batch shrink. When a stage's batch query is
49 * killed by the host (max_statement_time, lock-wait), the runtime halves
50 * the per-stage batch size and persists the new value so the next tick
51 * resumes at the smaller size. Floor at this value so we never spin on
52 * a 1-row batch that adds N database round-trips per row.
53 *
54 * Lowered from 50 to 10 (2026-05-08, deadline-math-audit-2026-05-08.md
55 * concern #1). The previous 50 was high enough that on the tightest
56 * shared hosts (max_statement_time = 3, slow disk, big wp_posts JOIN)
57 * EVERY batch at floor size would still be killed, locking the build
58 * into a runaway shrink loop. The fingerprint never advances because
59 * killed batches yield before writing high-water, so the JS poller
60 * trips its no-progress deadline (240s) and gives up. 10 is small
61 * enough to actually finish on hosts where 50 cannot, slow but
62 * progressing.
63 */
64 const VIEW_BUILD_MIN_BATCH_SIZE = 10;
65
66 /**
67 * Max wall-clock time a single request will spend executing batches in
68 * any one stage before yielding so the request can finish. Resumable
69 * builds pick up the remaining batches on the next request (driven by
70 * WP-Cron or by JS poll-triggered re-requests).
71 *
72 * Set to match VIEW_SNAPSHOT_WARMUP_STAGE_TIMEOUT_SECONDS (28s) so a
73 * single staged query has the full warmup query timeout to complete
74 * within one budget tick. The prior 10s value caused Bruno Martinez's
75 * 484K-row install (May 2026) to yield mid-stage before any single
76 * INSERT batch could finish on a slow shared host, so the build never
77 * made forward progress within the JS poller's deadline.
78 *
79 * Higher values (>28s) start to risk PHP killing the request at
80 * max_execution_time on shared hosts that default to 30s; the build
81 * still resumes safely on the next request via MAX(id) on the buffer,
82 * but a graceful yield is preferable.
83 */
84 const VIEW_BUILD_PER_STAGE_BUDGET_SECONDS = 28;
85
86 /**
87 * After this many seconds with no progress, an abandoned partial build
88 * is considered stale: the buffer table and high-water options are
89 * dropped on the next entry and the build restarts from scratch.
90 *
91 * Bumped from 600 to 3600 (2026-05-08, deadline-math-audit-2026-05-08.md
92 * concern #3). On Bruno-scale installs (484K redirects, slow shared
93 * host) a full build can legitimately take longer than 10 minutes; if
94 * the user closes the browser mid-build, the prior 600s TTL would
95 * discard the partial buffer on the next visit and force a fresh
96 * restart, so the build never converged across sessions. 3600s (1
97 * hour) is long enough to survive a normal user session gap while
98 * still bounding stale-buffer disk cost.
99 */
100 const VIEW_BUILD_RESUME_TTL_SECONDS = 3600;
101
102 const VIEW_BUILD_FOREGROUND_LEASE_SECONDS = 120;
103
104 /**
105 * Cap on the per-query SET STATEMENT max_statement_time hint applied
106 * to a non-batched stage (S3 / S9 / S10) on retry after a kill. The
107 * hint is also bounded by the request's remaining PHP execution time
108 * minus a 2s safety margin -- this constant is the absolute ceiling
109 * regardless of how much PHP time is left.
110 *
111 * 240s matches the JS poller's no-progress deadline: if a single
112 * non-batched query needs longer than that, the user-facing UI gives
113 * up anyway, so giving the query more time would only delay the
114 * eventual failure.
115 */
116 const VIEW_BUILD_NON_BATCHED_KILL_RETRY_CAP_SECONDS = 240;
117
118 /**
119 * Floor-kill streak threshold. After this many consecutive kills at
120 * VIEW_BUILD_MIN_BATCH_SIZE on the same stage, the build halts: the
121 * host cannot finish the plugin's smallest unit of work, retrying
122 * will only loop forever and trip the JS poller's no-progress
123 * deadline. Surfaces as a "host_unfit" admin notice.
124 */
125 const VIEW_BUILD_FLOOR_KILL_STREAK_HALT_THRESHOLD = 5;
126
127 /**
128 * TTL for the deduplicated admin notice transients raised when a
129 * stage is permanently skipped or the build halts. One notice per
130 * 24h per failure type per the self-healing reliability rules in
131 * CLAUDE.md (notices on the plugin's own admin screen, never email,
132 * never wp-admin-wide banner).
133 */
134 const VIEW_BUILD_DEGRADED_NOTICE_TTL_SECONDS = 86400;
135
136 /**
137 * Per-stage failure policy used by the host-failure classifier. When
138 * a stage's callback raises an error that the classifier identifies
139 * as a permanent host-side constraint (access denied, read-only,
140 * disk-full, quota), this map decides whether the build can degrade
141 * gracefully past the stage ('optional': skip + advance) or must
142 * stop retrying and surface a critical notice ('critical': halt).
143 *
144 * Optional stages (skippable on permanent failure):
145 * - S3 (ALTER ADD INDEX on view_build): build runs slower without
146 * the index, but completes correctly.
147 * - S9 (CREATE TEMPORARY hits aggregate): hit-count column is
148 * null/0 but the rest of the redirect listing is intact.
149 * - S10 (ALTER ADD sort indexes): sorted reads slower, correct.
150 *
151 * Critical stages (halt on permanent failure):
152 * - S1 (create build buffer): nothing can run without the buffer.
153 * - S2 (insert redirects): empty buffer means empty published view.
154 * - S4-S8 (UPDATE-JOIN against wp_posts/wp_terms/external/special):
155 * resolved fields are mandatory for the rendered view; partial
156 * resolution would produce a broken admin screen.
157 * - S11 (RENAME swap): without the swap, view_done is never
158 * published and the build is wasted work.
159 *
160 * @var array<int, string>
161 */
162 private const STAGE_FAILURE_POLICY = array(
163 1 => 'critical',
164 2 => 'critical',
165 3 => 'optional',
166 4 => 'critical',
167 5 => 'critical',
168 6 => 'critical',
169 7 => 'critical',
170 8 => 'critical',
171 9 => 'optional',
172 10 => 'optional',
173 11 => 'critical',
174 );
175
176 /**
177 * Look up the per-stage failure policy. Stages not registered in
178 * STAGE_FAILURE_POLICY default to 'critical' (fail safely: any
179 * unknown stage that fails permanently halts rather than silently
180 * skipping data the user expects).
181 *
182 * @param int $stageNumber 1-based staged build number.
183 * @return string 'optional' or 'critical'.
184 */
185 public static function stageFailurePolicy(int $stageNumber): string {
186 return self::STAGE_FAILURE_POLICY[$stageNumber] ?? 'critical';
187 }
188
189 /**
190 * Total number of stages in the staged view-build pipeline. This is the
191 * single source of truth for the "of N" denominator in every build-progress
192 * payload (`progress.of`). All progress producers (pending response, advance
193 * loop, freshness short-circuit, paused gate) must report this same total so
194 * the value is internally consistent across the AJAX contract. Derived from
195 * STAGE_FAILURE_POLICY so adding/removing a stage updates the total in one
196 * place.
197 *
198 * @return int Count of staged build stages (currently 11).
199 */
200 public static function totalStages(): int {
201 return count(self::STAGE_FAILURE_POLICY);
202 }
203
204 /** Recommended floor for memory_limit (128M) in the PHP env probe. */
205 const PHP_MEMORY_LIMIT_RECOMMENDED_BYTES = 134217728;
206
207 /** Floor for free space on @@tmpdir's volume before warning (100MB). */
208 const PHP_TMPDIR_FREE_FLOOR_BYTES = 104857600;
209
210 /**
211 * Out-of-range thresholds for the operational + DDL-safety MySQL session
212 * variables probed at S1 entry. Centralized here (rather than on the
213 * trait) because PHP < 8.2 forbids constants in trait bodies and the
214 * plugin supports 7.4+.
215 */
216 const SESSION_PROBE_THRESHOLDS = array(
217 'innodb_lock_wait_timeout_min' => 30,
218 'tmp_table_size_min' => 16777216,
219 'max_heap_table_size_min' => 16777216,
220 'long_query_time_min' => 1.0,
221 'innodb_buffer_pool_size_min' => 268435456,
222 'wait_timeout_min' => 600,
223 'interactive_timeout_min' => 600,
224 'thread_stack_min' => 196608,
225 'open_files_limit_min' => 1024,
226 'innodb_online_alter_log_max_size_min' => 134217728,
227 );
228
229 /**
230 * Transient gate key for the c384 on-page-load fallback advance.
231 * One inline advance per gate window; bursts of admin sub-requests
232 * (prefetch, browser refresh, multiple tabs) inside the window
233 * short-circuit so the page-load cost cannot compound. Defined
234 * here (rather than on the consuming trait) because PHP < 8.2
235 * forbids constants in trait bodies.
236 */
237 const PAGE_LOAD_FALLBACK_GATE_KEY = 'abj404_page_load_fallback_advance';
238
239 /**
240 * Seconds the page-load fallback gate transient survives. 60s is
241 * short enough that an attentive admin sees real per-load progress
242 * (one stage per minute of navigation) and long enough that a
243 * burst of clicks within a single working moment does not stack
244 * inline build work.
245 */
246 const PAGE_LOAD_FALLBACK_GATE_SECONDS = 60;
247
248 /**
249 * Per-stage budget seconds during a page-load fallback advance.
250 * The admin is blocking on the response, so a single tick must
251 * not exceed roughly the human-perceived "loading" tolerance.
252 * Picked at 2.0s because that matches the c377 page-load contract
253 * and stays well under the WordPress admin heartbeat default. The
254 * fallback registers this as a ceiling via add_filter on
255 * abj404_view_build_per_stage_budget_seconds (clamping with min(),
256 * not overwriting, so any operator-set smaller budget wins).
257 */
258 const PAGE_LOAD_FALLBACK_BUDGET_SECONDS = 2.0;
259
260 private function __construct() {}
261 }
262