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

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

279 lines 12.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
11 * ABJ_404_Solution_DataAccess_ViewQueriesStagedTrait, 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 * Upper-bound staleness threshold. When viewDoneIsServeable() serves
106 * data older than this, an admin notice fires telling the admin the
107 * redirects table data is out of date. The fix that lets view_done
108 * serve stale post-invalidate data without blocking would otherwise
109 * let arbitrarily old data render silently if the rebuild never
110 * completes (cron stuck, repeated invalidation racing the build,
111 * floor-kill streak halt). This notice is the honest upper bound:
112 * stale-but-present is fine; stale-and-unsignalled is not.
113 *
114 * 24h matches the dedup TTL in VIEW_BUILD_DEGRADED_NOTICE_TTL_SECONDS
115 * and the cron-stuck threshold in scheduleViewDoneRebuild() so the
116 * three notice families speak in the same time units.
117 */
118 const VIEW_DONE_HARD_STALE_NOTICE_AGE_SECONDS = 86400;
119
120 /**
121 * Sanity cap on how long an admin-initiated mutation flag (set by
122 * markViewDoneInvalidatedByAdminMutation()) can keep view_done
123 * unserveable. While the flag is active, viewDoneIsServeable() returns
124 * false so the AJAX gate returns viewBuildPending and the JS poller
125 * waits for a build that covers the mutation. If the build never
126 * completes (cron broken, DB locked) the gate falls back to fbc270d8
127 * stale-serving at this timeout so the admin redirects page is not
128 * blocked indefinitely.
129 *
130 * Five minutes is a comfortable upper bound for staged rebuilds on
131 * Bruno/Troy-grade installs (~11 stages, multi-second per stage).
132 * Above this, the admin sees stale data plus the hard-stale notice.
133 */
134 const VIEW_DONE_MUTATION_INVALIDATED_SANITY_SECONDS = 300;
135
136 /**
137 * Cap on the per-query SET STATEMENT max_statement_time hint applied
138 * to a non-batched stage (S3 / S9 / S10) on retry after a kill. The
139 * hint is also bounded by the request's remaining PHP execution time
140 * minus a 2s safety margin -- this constant is the absolute ceiling
141 * regardless of how much PHP time is left.
142 *
143 * 240s matches the JS poller's no-progress deadline: if a single
144 * non-batched query needs longer than that, the user-facing UI gives
145 * up anyway, so giving the query more time would only delay the
146 * eventual failure.
147 */
148 const VIEW_BUILD_NON_BATCHED_KILL_RETRY_CAP_SECONDS = 240;
149
150 /**
151 * Floor-kill streak threshold. After this many consecutive kills at
152 * VIEW_BUILD_MIN_BATCH_SIZE on the same stage, the build halts: the
153 * host cannot finish the plugin's smallest unit of work, retrying
154 * will only loop forever and trip the JS poller's no-progress
155 * deadline. Surfaces as a "host_unfit" admin notice.
156 */
157 const VIEW_BUILD_FLOOR_KILL_STREAK_HALT_THRESHOLD = 5;
158
159 /**
160 * TTL for the deduplicated admin notice transients raised when a
161 * stage is permanently skipped or the build halts. One notice per
162 * 24h per failure type per the self-healing reliability rules in
163 * CLAUDE.md (notices on the plugin's own admin screen, never email,
164 * never wp-admin-wide banner).
165 */
166 const VIEW_BUILD_DEGRADED_NOTICE_TTL_SECONDS = 86400;
167
168 /**
169 * Per-stage failure policy used by the host-failure classifier. When
170 * a stage's callback raises an error that the classifier identifies
171 * as a permanent host-side constraint (access denied, read-only,
172 * disk-full, quota), this map decides whether the build can degrade
173 * gracefully past the stage ('optional': skip + advance) or must
174 * stop retrying and surface a critical notice ('critical': halt).
175 *
176 * Optional stages (skippable on permanent failure):
177 * - S3 (ALTER ADD INDEX on view_build): build runs slower without
178 * the index, but completes correctly.
179 * - S9 (CREATE TEMPORARY hits aggregate): hit-count column is
180 * null/0 but the rest of the redirect listing is intact.
181 * - S10 (ALTER ADD sort indexes): sorted reads slower, correct.
182 *
183 * Critical stages (halt on permanent failure):
184 * - S1 (create build buffer): nothing can run without the buffer.
185 * - S2 (insert redirects): empty buffer means empty published view.
186 * - S4-S8 (UPDATE-JOIN against wp_posts/wp_terms/external/special):
187 * resolved fields are mandatory for the rendered view; partial
188 * resolution would produce a broken admin screen.
189 * - S11 (RENAME swap): without the swap, view_done is never
190 * published and the build is wasted work.
191 *
192 * @var array<int, string>
193 */
194 private const STAGE_FAILURE_POLICY = array(
195 1 => 'critical',
196 2 => 'critical',
197 3 => 'optional',
198 4 => 'critical',
199 5 => 'critical',
200 6 => 'critical',
201 7 => 'critical',
202 8 => 'critical',
203 9 => 'optional',
204 10 => 'optional',
205 11 => 'critical',
206 );
207
208 /**
209 * Look up the per-stage failure policy. Stages not registered in
210 * STAGE_FAILURE_POLICY default to 'critical' (fail safely: any
211 * unknown stage that fails permanently halts rather than silently
212 * skipping data the user expects).
213 *
214 * @param int $stageNumber 1-based staged build number.
215 * @return string 'optional' or 'critical'.
216 */
217 public static function stageFailurePolicy(int $stageNumber): string {
218 return self::STAGE_FAILURE_POLICY[$stageNumber] ?? 'critical';
219 }
220
221 /** Recommended floor for memory_limit (128M) in the PHP env probe. */
222 const PHP_MEMORY_LIMIT_RECOMMENDED_BYTES = 134217728;
223
224 /** Floor for free space on @@tmpdir's volume before warning (100MB). */
225 const PHP_TMPDIR_FREE_FLOOR_BYTES = 104857600;
226
227 /**
228 * Out-of-range thresholds for the operational + DDL-safety MySQL session
229 * variables probed at S1 entry. Centralized here (rather than on the
230 * trait) because PHP < 8.2 forbids constants in trait bodies and the
231 * plugin supports 7.4+.
232 */
233 const SESSION_PROBE_THRESHOLDS = array(
234 'innodb_lock_wait_timeout_min' => 30,
235 'tmp_table_size_min' => 16777216,
236 'max_heap_table_size_min' => 16777216,
237 'long_query_time_min' => 1.0,
238 'innodb_buffer_pool_size_min' => 268435456,
239 'wait_timeout_min' => 600,
240 'interactive_timeout_min' => 600,
241 'thread_stack_min' => 196608,
242 'open_files_limit_min' => 1024,
243 'innodb_online_alter_log_max_size_min' => 134217728,
244 );
245
246 /**
247 * Transient gate key for the c384 on-page-load fallback advance.
248 * One inline advance per gate window; bursts of admin sub-requests
249 * (prefetch, browser refresh, multiple tabs) inside the window
250 * short-circuit so the page-load cost cannot compound. Defined
251 * here (rather than on the consuming trait) because PHP < 8.2
252 * forbids constants in trait bodies.
253 */
254 const PAGE_LOAD_FALLBACK_GATE_KEY = 'abj404_page_load_fallback_advance';
255
256 /**
257 * Seconds the page-load fallback gate transient survives. 60s is
258 * short enough that an attentive admin sees real per-load progress
259 * (one stage per minute of navigation) and long enough that a
260 * burst of clicks within a single working moment does not stack
261 * inline build work.
262 */
263 const PAGE_LOAD_FALLBACK_GATE_SECONDS = 60;
264
265 /**
266 * Per-stage budget seconds during a page-load fallback advance.
267 * The admin is blocking on the response, so a single tick must
268 * not exceed roughly the human-perceived "loading" tolerance.
269 * Picked at 2.0s because that matches the c377 page-load contract
270 * and stays well under the WordPress admin heartbeat default. The
271 * fallback registers this as a ceiling via add_filter on
272 * abj404_view_build_per_stage_budget_seconds (clamping with min(),
273 * not overwriting, so any operator-set smaller budget wins).
274 */
275 const PAGE_LOAD_FALLBACK_BUDGET_SECONDS = 2.0;
276
277 private function __construct() {}
278 }
279