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

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

632 lines 29.7 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 * MySQL-session environment probe for the staged view-build pipeline.
9 *
10 * Read-and-warn-only check of operational + DDL-safety MySQL session
11 * variables that can silently degrade or break a staged view-build but
12 * are not severe enough to halt it. Each out-of-range variable produces
13 * one warning-level log line and contributes to a single consolidated
14 * deduplicated admin notice (per the task spec: "One concise warning per
15 * S1 entry -- no per-stage spam").
16 *
17 * Sibling to ABJ_404_Solution_DataAccess_ViewBuildHelpersTrait. Extracted
18 * from that trait to keep both files under the project's 1500-line cap.
19 * The public probe method probeSessionVariablesAtS1Entry() is the entry
20 * point called from runStagedBuildOnce() right after the existing
21 * probeSqlModeForBuild() call.
22 *
23 * Variables covered (P3 + P4 ride-along, see task brief):
24 * - innodb_lock_wait_timeout (< 30s)
25 * - tmp_table_size + max_heap_table_size (< 16M)
26 * - slow_query_log + long_query_time (log on AND threshold very low)
27 * - innodb_buffer_pool_size (< 256M)
28 * - wait_timeout + interactive_timeout (< 600s; orphan-table cause)
29 * - innodb_flush_method (== O_DSYNC)
30 * - character_set_server / collation_server (not utf8mb4)
31 * - sql_require_primary_key (ON; future CREATE TABLE breaks)
32 * - innodb_file_per_table (OFF; tablespace bloat / un-reclaimable DROP)
33 * - thread_stack + open_files_limit (very low)
34 * - innodb_online_alter_log_max_size (small; S3/S10 ALTER risk)
35 *
36 * @property ABJ_404_Solution_DatabaseCore $dbCore
37 * @property ABJ_404_Solution_Functions $f
38 * @property ABJ_404_Solution_Logging $logger
39 * @property ABJ_404_Solution_ViewReadService|null $viewReadService
40 * @property ABJ_404_Solution_LogsRepository|null $logsRepo
41 * @property int $stagedQueryTimeoutSeconds
42 * @property string $lastBatchProgressDetail
43 * @property bool $viewBuildStageOpenForShutdown
44 * @property int $viewBuildShutdownStageNumber
45 * @property string $viewBuildShutdownStageKey
46 * @property bool|null $namedLockSupportedThisRequest
47 * @property bool $fallbackLockLoggedThisRequest
48 * @property bool $usingTransientFallbackLock
49 * @property string $lastNamedLockUnsupportedReason
50 * @property string $lastNamedLockUnsupportedError
51 * @method void abortStagedBuildForMutationWatermarkAdvance(...$arguments)
52 * @method bool acquireTransientFallbackLock(...$arguments)
53 * @method bool acquireViewBuildLock(...$arguments)
54 * @method string activeBuildStartedWatermarkOptionName(...$arguments)
55 * @method bool adminMutationGateBlocks(...$arguments)
56 * @method array<mixed> advanceViewBuildOnce(...$arguments)
57 * @method void assertBuildBufferExistsOrHalt(...$arguments)
58 * @method ?bool attemptRelaxSqlModeForBuildConnection(...$arguments)
59 * @method bool bufferIntegrityPassesForPromote(...$arguments)
60 * @method string buildHaltTransientKey(...$arguments)
61 * @method string buildViewDoneCountQuery(...$arguments)
62 * @method string builtWatermarkOptionName(...$arguments)
63 * @method int bumpMutationWatermark(...$arguments)
64 * @method int bumpStageNoProgressStreak(...$arguments)
65 * @method string capturedPrefixForLog(...$arguments)
66 * @method void capturePrefixAtBuildStart(...$arguments)
67 * @method void claimForegroundViewBuildLease(...$arguments)
68 * @method string classifyAndHandleStageFailure(...$arguments)
69 * @method array<mixed> classifySessionVariableWarnings(...$arguments)
70 * @method string classifyStageFailure(...$arguments)
71 * @method void clearActiveBuildStartedWatermark(...$arguments)
72 * @method void clearAdminMutationGateOptions(...$arguments)
73 * @method void clearAllProgressOptions(...$arguments)
74 * @method void clearPhpEnvironmentProbeCache(...$arguments)
75 * @method void clearPrefixAtStageOne(...$arguments)
76 * @method void clearSessionVariablesProbeCache(...$arguments)
77 * @method void clearSqlModeProbeCache(...$arguments)
78 * @method void clearStagedBuildDegradedState(...$arguments)
79 * @method void clearViewBuildOpenStageForShutdown(...$arguments)
80 * @method void clearViewDoneHardStaleNotice(...$arguments)
81 * @method ABJ_404_Solution_Clock clock(...$arguments)
82 * @method int countLiveRedirects(...$arguments)
83 * @method int countViewBuildRows(...$arguments)
84 * @method string describeBuildProgressForNotice(...$arguments)
85 * @method string describeDegradedNotice(...$arguments)
86 * @method string describeStagedSqlFailure(...$arguments)
87 * @method array<mixed> detectAndAdjustSqlMode(...$arguments)
88 * @method float detectHostStagedQueryLimitSeconds(...$arguments)
89 * @method string doTableNameReplacements(...$arguments)
90 * @method void dropDeletemeTable(...$arguments)
91 * @method void dropTransientBuffersIfPresent(...$arguments)
92 * @method void dropTransientStagedTables(...$arguments)
93 * @method void ensureConnection(...$arguments)
94 * @method void ensureFallbackLockNoticeAndLog(...$arguments)
95 * @method int extendedTimeoutForKilledNonBatchedStage(...$arguments)
96 * @method array<mixed> fetchSessionVariablesRowOrEmpty(...$arguments)
97 * @method string filesystemEnvironmentProbeOptionName(...$arguments)
98 * @method bool forceRestartViewBuild(...$arguments)
99 * @method bool foregroundViewBuildLeaseActive(...$arguments)
100 * @method string formatPhpMemoryBytesHuman(...$arguments)
101 * @method bool gateAbortIfMutationWatermarkAdvanced(...$arguments)
102 * @method string getColumnCollationString(...$arguments)
103 * @method int getCronStuckHours(...$arguments)
104 * @method string getLowercasePrefix(...$arguments)
105 * @method array<string, mixed> getViewBuildProgress(...$arguments)
106 * @method array<mixed> getViewBuildProgressFingerprint(...$arguments)
107 * @method int getViewDoneBuiltAtTimestamp(...$arguments)
108 * @method bool haltIfPrefixChangedSinceStageOne(...$arguments)
109 * @method string humanBatchProgress(...$arguments)
110 * @method float intelligentStagedQueryTimeoutSeconds(...$arguments)
111 * @method void invalidateViewDoneServeableCache(...$arguments)
112 * @method bool isBuildHaltedForHostFailure(...$arguments)
113 * @method bool isCurrentStageOptionName(...$arguments)
114 * @method bool isNamedLockUnsupportedError(...$arguments)
115 * @method bool isResumableStagedKill(...$arguments)
116 * @method bool isStageMarkedSkipped(...$arguments)
117 * @method bool isTransientConnectionError(...$arguments)
118 * @method string lastBuildStartedWatermarkOptionName(...$arguments)
119 * @method string legacyStartedWatermarkOptionName(...$arguments)
120 * @method string localizeOrDefaultViewBuildNotice(...$arguments)
121 * @method bool logsHitsTableExists(...$arguments)
122 * @method void logTimedViewBuildStage(...$arguments)
123 * @method void logViewBuildProgressOptionWrite(...$arguments)
124 * @method void logViewBuildShutdownDiagnostics(...$arguments)
125 * @method void markBuildHaltedForHostFailure(...$arguments)
126 * @method void markBuildStage(...$arguments)
127 * @method void markStageSkippedForHostFailure(...$arguments)
128 * @method void markViewBuildStageCompleted(...$arguments)
129 * @method void markViewBuildStageStarted(...$arguments)
130 * @method void markViewDoneBuildCompleted(...$arguments)
131 * @method void markViewDoneInvalidatedByAdminMutation(...$arguments)
132 * @method int maxBuildBufferId(...$arguments)
133 * @method void maybeRaiseViewDoneHardStaleNotice(...$arguments)
134 * @method bool mutationWatermarkAdvancedSinceBuildStart(...$arguments)
135 * @method int mutationWatermarkObservedByAdminAction(...$arguments)
136 * @method int mutationWatermarkObservedByAdminActionAt(...$arguments)
137 * @method string mutationWatermarkObservedByAdminActionAtOptionName(...$arguments)
138 * @method string mutationWatermarkObservedByAdminActionOptionName(...$arguments)
139 * @method string normalizePathPrefix(...$arguments)
140 * @method bool optionReadBackMatches(...$arguments)
141 * @method int parsePhpMemoryLimitToBytes(...$arguments)
142 * @method bool pathFallsWithinAny(...$arguments)
143 * @method void performFreshStartCleanup(...$arguments)
144 * @method array<mixed> phpDisabledFunctionsList(...$arguments)
145 * @method string phpEnvironmentProbeOptionName(...$arguments)
146 * @method float phpTimeRemainingSeconds(...$arguments)
147 * @method string prefixAtStageOneOptionName(...$arguments)
148 * @method array<mixed> probeFilesystemEnvironmentForBuild(...$arguments)
149 * @method float probeFloatFromValues(...$arguments)
150 * @method int probeIntFromValues(...$arguments)
151 * @method int probeMemoryLimitForS9(...$arguments)
152 * @method array<mixed> probePhpEnvironmentForBuild(...$arguments)
153 * @method array<mixed> probeSessionVariablesAtS1Entry(...$arguments)
154 * @method bool probeSetTimeLimitAvailability(...$arguments)
155 * @method array<mixed> probeSqlModeForBuild(...$arguments)
156 * @method string probeStringFromValues(...$arguments)
157 * @method string progressOptionName(...$arguments)
158 * @method void publishBuiltWatermarkFromActiveBuildStartedWatermark(...$arguments)
159 * @method array<mixed> queryAndGetResults(...$arguments)
160 * @method int readActiveBuildStartedWatermark(...$arguments)
161 * @method array<int, array<string, mixed>> readFromViewDone(...$arguments)
162 * @method int readProgressOption(...$arguments)
163 * @method int readWatermarkOption(...$arguments)
164 * @method void rebuildViewDoneInBackground(...$arguments)
165 * @method bool reconcilePostStageElevenState(...$arguments)
166 * @method string reconcileStagedTablesAtRunnerStartup(...$arguments)
167 * @method int recordStageBatchKilled(...$arguments)
168 * @method void registerViewBuildShutdownDiagnostics(...$arguments)
169 * @method bool releaseAndReacquireBetweenStages(...$arguments)
170 * @method void releaseViewBuildLock(...$arguments)
171 * @method void resetStageNoProgressStreak(...$arguments)
172 * @method string resolveColumnCollationForStagedBuild(...$arguments)
173 * @method void runForceRestartCleanupInsideLock(...$arguments)
174 * @method bool runIdRangeBatchedUpdate(...$arguments)
175 * @method int runInsertBatch(...$arguments)
176 * @method mixed runNonBatchedStageWithKillStreakEscape(...$arguments)
177 * @method array{ran: bool, reason: string, progress: array<string, mixed>} runPageLoadFallbackAdvance(...$arguments)
178 * @method int runRedirectsForViewCountStaged(...$arguments)
179 * @method array<int, array<string, mixed>> runRedirectsForViewStaged(...$arguments)
180 * @method bool runS11SwapWithPreRenameWatermarkRecheck(...$arguments)
181 * @method bool runStagedBuildOnce(...$arguments)
182 * @method bool runStagedBuildStages6Through11(...$arguments)
183 * @method void runStagedSqlFile(...$arguments)
184 * @method void runStagedSqlFileTolerantOfDuplicateKey(...$arguments)
185 * @method mixed runTimedViewBuildStage(...$arguments)
186 * @method int safeCurrentMutationWatermark(...$arguments)
187 * @method string sanitizeUrlBeforeInsert(...$arguments)
188 * @method void scheduleViewDoneRebuild(...$arguments)
189 * @method string sessionVariablesProbeOptionName(...$arguments)
190 * @method void setFilesystemEnvAdminNotice(...$arguments)
191 * @method void setLowMemoryLimitAdminNotice(...$arguments)
192 * @method void setSessionEnvAdminNotice(...$arguments)
193 * @method void setStagedBuildDegradedNotice(...$arguments)
194 * @method void setStagedBuildHaltNotice(...$arguments)
195 * @method void setViewBuildCronStuckNotice(...$arguments)
196 * @method void setViewBuildScheduleFailedNotice(...$arguments)
197 * @method void setViewDoneHardStaleNotice(...$arguments)
198 * @method array<mixed> splitOpenBasedirPaths(...$arguments)
199 * @method string sqlModeProbeOptionName(...$arguments)
200 * @method void stageAddPreJoinIndexes(...$arguments)
201 * @method void stageAddSortIndexes(...$arguments)
202 * @method void stageCreateBuildTable(...$arguments)
203 * @method array<string, mixed> stagedQueryOptions(...$arguments)
204 * @method bool stagedTableExists(...$arguments)
205 * @method bool stageInsertRedirectsBatched(...$arguments)
206 * @method string stageNoProgressStreakOptionName(...$arguments)
207 * @method void stageRenameSwap(...$arguments)
208 * @method string stageSkipOptionName(...$arguments)
209 * @method void stageUpdateExternal(...$arguments)
210 * @method void stageUpdateHits(...$arguments)
211 * @method void stageUpdateHome(...$arguments)
212 * @method bool stageUpdatePostsBatched(...$arguments)
213 * @method void stageUpdateSpecial(...$arguments)
214 * @method bool stageUpdateTermsBatched(...$arguments)
215 * @method void stampStartedWatermarksAtS1Entry(...$arguments)
216 * @method void sweepStaleRebuildTransients(...$arguments)
217 * @method string transientFallbackLockOptionName(...$arguments)
218 * @method bool verifyBuildLockSerializesWriter(...$arguments)
219 * @method bool verifyOptionWriteCoherent(...$arguments)
220 * @method bool verifyPrefixUnchangedSinceStageOne(...$arguments)
221 * @method int viewBuildBatchSize(...$arguments)
222 * @method int viewBuildBatchSizeForStage(...$arguments)
223 * @method array<mixed> viewBuildOnlyTranslations(...$arguments)
224 * @method float viewBuildPerStageBudgetSeconds(...$arguments)
225 * @method string viewBuildTableName(...$arguments)
226 * @method string viewDeletemeTableName(...$arguments)
227 * @method int viewDoneBuiltAt(...$arguments)
228 * @method int viewDoneBuiltWatermark(...$arguments)
229 * @method int viewDoneDataBuiltAt(...$arguments)
230 * @method string viewDoneDataBuiltAtOptionName(...$arguments)
231 * @method string viewDoneFreshnessOptionName(...$arguments)
232 * @method bool viewDoneHasRows(...$arguments)
233 * @method bool viewDoneIsFresh(...$arguments)
234 * @method bool viewDoneIsServeable(...$arguments)
235 * @method int viewDoneMutationInvalidatedAt(...$arguments)
236 * @method string viewDoneMutationInvalidatedAtOptionName(...$arguments)
237 * @method bool viewDoneTableExists(...$arguments)
238 * @method string viewDoneTableName(...$arguments)
239 * @method void writeProgressOption(...$arguments)
240 * @method void writeWatermarkOption(...$arguments)
241 */
242 class ABJ_404_Solution_ViewBuildSessionEnvProbe extends ABJ_404_Solution_ViewBuildCollaborator {
243
244 /**
245 * Cached session-variables probe result for the current request. Holds
246 * the raw values pulled via SHOW VARIABLES at S1 entry plus the per-key
247 * out-of-range flags so callers can render a single consolidated warning
248 * without re-querying.
249 *
250 * @var array<string,mixed>|null
251 */
252 private $sessionVariablesProbeCache = null;
253
254 /** @return string Option name for the persisted session-variables probe. */
255 public function sessionVariablesProbeOptionName(): string {
256 return 'abj404_view_build_session_env_probe';
257 }
258
259 /**
260 * Read an int from a probe values map, returning 0 when the value is
261 * non-numeric. Avoids the (int) cast on `mixed` which PHPStan level 9
262 * flags as `cast.int`.
263 *
264 * @param array<string,mixed> $values
265 */
266 private static function probeIntFromValues(array $values, string $key): int {
267 $v = $values[$key] ?? 0;
268 return is_numeric($v) ? (int)$v : 0;
269 }
270
271 /**
272 * Read a float from a probe values map, returning 0.0 when non-numeric.
273 *
274 * @param array<string,mixed> $values
275 */
276 private static function probeFloatFromValues(array $values, string $key): float {
277 $v = $values[$key] ?? 0;
278 return is_numeric($v) ? (float)$v : 0.0;
279 }
280
281 /**
282 * Read a string from a probe values map, returning '' when not scalar.
283 *
284 * @param array<string,mixed> $values
285 */
286 private static function probeStringFromValues(array $values, string $key): string {
287 $v = $values[$key] ?? '';
288 return is_scalar($v) ? (string)$v : '';
289 }
290
291 /**
292 * Probe the live MySQL session for operational + DDL-safety variables
293 * that can silently degrade or break the staged view-build pipeline.
294 * Read-and-warn-only: never throws, never blocks the build. Logs each
295 * out-of-range variable at warning level (per defensive philosophy §8 --
296 * infrastructure issues the plugin can degrade past) and surfaces ONE
297 * consolidated admin notice per 24h on the plugin's own admin screen.
298 *
299 * Idempotent within a request: repeat calls return the cached result.
300 * Cleared by clearSessionVariablesProbeCache() (chained from
301 * clearSqlModeProbeCache) on a fresh build.
302 *
303 * Filterable via `apply_filters('abj404_session_env_probe', $defaults)`
304 * so tests and operators can simulate a constrained host without
305 * mutating the live MySQL session.
306 *
307 * @return array<string,mixed>
308 */
309 public function probeSessionVariablesAtS1Entry(): array {
310 if (is_array($this->sessionVariablesProbeCache)) {
311 return $this->sessionVariablesProbeCache;
312 }
313
314 $defaults = array(
315 'innodb_lock_wait_timeout' => 0,
316 'tmp_table_size' => 0,
317 'max_heap_table_size' => 0,
318 'slow_query_log' => 0,
319 'long_query_time' => 0.0,
320 'innodb_buffer_pool_size' => 0,
321 'wait_timeout' => 0,
322 'interactive_timeout' => 0,
323 'innodb_flush_method' => '',
324 'character_set_server' => '',
325 'collation_server' => '',
326 'sql_require_primary_key' => '',
327 'innodb_file_per_table' => '',
328 'thread_stack' => 0,
329 'open_files_limit' => 0,
330 'innodb_online_alter_log_max_size' => 0,
331 'probe_succeeded' => false,
332 );
333
334 $row = $this->fetchSessionVariablesRowOrEmpty();
335 $values = $defaults;
336 if (!empty($row)) {
337 $values['probe_succeeded'] = true;
338 foreach ($row as $k => $v) {
339 $klow = strtolower((string)$k);
340 if (!array_key_exists($klow, $defaults)) { continue; }
341 if ($klow === 'long_query_time') {
342 $values[$klow] = is_scalar($v) ? (float)$v : 0.0;
343 } elseif (is_int($defaults[$klow])) {
344 $values[$klow] = is_scalar($v) ? (int)$v : 0;
345 } else {
346 $values[$klow] = is_scalar($v) ? (string)$v : '';
347 }
348 }
349 }
350
351 if (function_exists('apply_filters')) {
352 $filtered = apply_filters('abj404_session_env_probe', $values);
353 if (is_array($filtered)) {
354 $values = array_merge($values, $filtered);
355 }
356 }
357
358 $warnings = $this->classifySessionVariableWarnings($values);
359 $values['warnings'] = $warnings;
360
361 foreach ($warnings as $w) {
362 if (is_scalar($w)) {
363 $this->logger->warn('[staged] ' . (string)$w);
364 }
365 }
366 if (!empty($warnings)) {
367 $this->setSessionEnvAdminNotice($warnings);
368 }
369
370 if (function_exists('update_option')) {
371 update_option($this->sessionVariablesProbeOptionName(), $values, false);
372 }
373
374 $this->sessionVariablesProbeCache = $values;
375 return $values;
376 }
377
378 /**
379 * Single-query SHOW VARIABLES read for every name we care about. Returns
380 * a key=>value map keyed by lowercase Variable_name (case-insensitive
381 * driver tolerance per defensive philosophy §5). Empty on probe failure.
382 *
383 * @return array<string,string>
384 */
385 public function fetchSessionVariablesRowOrEmpty(): array {
386 global $wpdb;
387 if (!isset($wpdb) || !is_object($wpdb) || !method_exists($wpdb, 'get_results')) {
388 return array();
389 }
390 /** @var \wpdb $wpdb */
391 $names = array(
392 'innodb_lock_wait_timeout',
393 'tmp_table_size',
394 'max_heap_table_size',
395 'slow_query_log',
396 'long_query_time',
397 'innodb_buffer_pool_size',
398 'wait_timeout',
399 'interactive_timeout',
400 'innodb_flush_method',
401 'character_set_server',
402 'collation_server',
403 'sql_require_primary_key',
404 'innodb_file_per_table',
405 'thread_stack',
406 'open_files_limit',
407 'innodb_online_alter_log_max_size',
408 );
409 $placeholders = implode(',', array_fill(0, count($names), '%s'));
410 $sql = "SHOW SESSION VARIABLES WHERE Variable_name IN ($placeholders)";
411 $prevSuppress = method_exists($wpdb, 'suppress_errors') ? $wpdb->suppress_errors(true) : false;
412 try {
413 $prepared = method_exists($wpdb, 'prepare') ? $wpdb->prepare($sql, $names) : $sql;
414 // DAO-bypass-approved: read-only probe of @@SESSION on this connection.
415 $rows = $wpdb->get_results($prepared, ARRAY_A);
416 } catch (\Throwable $e) { // allow-silent-catch: probe is best-effort; suppress restored below
417 $rows = null;
418 }
419 if (method_exists($wpdb, 'suppress_errors')) {
420 $wpdb->suppress_errors($prevSuppress);
421 }
422
423 $out = array();
424 if (!is_array($rows)) { return $out; }
425 foreach ($rows as $row) {
426 if (!is_array($row)) { continue; }
427 $name = '';
428 $value = '';
429 foreach ($row as $k => $v) {
430 $klow = strtolower((string)$k);
431 if ($klow === 'variable_name' && is_scalar($v)) { $name = strtolower((string)$v); }
432 if ($klow === 'value' && is_scalar($v)) { $value = (string)$v; }
433 }
434 if ($name !== '') { $out[$name] = $value; }
435 }
436 return $out;
437 }
438
439 /**
440 * Apply the SESSION_PROBE_THRESHOLDS to a value map and return a flat
441 * list of human-readable warning strings (one per out-of-range variable).
442 * Pure / side-effect free so tests can assert classification independently
443 * from logging and admin-notice surfacing.
444 *
445 * @param array<string,mixed> $values
446 * @return array<int,string>
447 */
448 public function classifySessionVariableWarnings(array $values): array {
449 $warnings = array();
450 $t = ABJ_404_Solution_ViewBuildConfig::SESSION_PROBE_THRESHOLDS;
451
452 $iLockWait = self::probeIntFromValues($values, 'innodb_lock_wait_timeout');
453 if ($iLockWait > 0 && $iLockWait < $t['innodb_lock_wait_timeout_min']) {
454 $warnings[] = sprintf(
455 'innodb_lock_wait_timeout=%ds (< %ds); the staged build may abort '
456 . 'with "Lock wait timeout exceeded" on busy hosts.',
457 $iLockWait, $t['innodb_lock_wait_timeout_min']
458 );
459 }
460
461 $tmpTable = self::probeIntFromValues($values, 'tmp_table_size');
462 $maxHeap = self::probeIntFromValues($values, 'max_heap_table_size');
463 if ($tmpTable > 0 && $tmpTable < $t['tmp_table_size_min']) {
464 $warnings[] = sprintf(
465 'tmp_table_size=%d (< %d MB); MySQL will spill GROUP BY work to '
466 . 'disk earlier and the S9 hits aggregate may slow significantly.',
467 $tmpTable, (int)($t['tmp_table_size_min'] / 1048576)
468 );
469 }
470 if ($maxHeap > 0 && $maxHeap < $t['max_heap_table_size_min']) {
471 $warnings[] = sprintf(
472 'max_heap_table_size=%d (< %d MB); MEMORY-engine temp tables will '
473 . 'truncate or spill earlier than expected.',
474 $maxHeap, (int)($t['max_heap_table_size_min'] / 1048576)
475 );
476 }
477
478 $rawSlow = $values['slow_query_log'] ?? '';
479 $rawSlowStr = is_scalar($rawSlow) ? (string)$rawSlow : '';
480 $slowOn = (is_numeric($rawSlow) && (int)$rawSlow > 0)
481 || strtoupper($rawSlowStr) === 'ON';
482 $longTime = self::probeFloatFromValues($values, 'long_query_time');
483 if ($slowOn && $longTime > 0 && $longTime < $t['long_query_time_min']) {
484 $warnings[] = sprintf(
485 'slow_query_log=ON with long_query_time=%.3fs (< %.1fs); the staged '
486 . 'build will flood the slow log with stage-batch queries.',
487 $longTime, (float)$t['long_query_time_min']
488 );
489 }
490
491 $bufferPool = self::probeIntFromValues($values, 'innodb_buffer_pool_size');
492 if ($bufferPool > 0 && $bufferPool < $t['innodb_buffer_pool_size_min']) {
493 $warnings[] = sprintf(
494 'innodb_buffer_pool_size=%d (< %d MB); large logsv2 reads at S2/S9 '
495 . 'will thrash the buffer pool.',
496 $bufferPool, (int)($t['innodb_buffer_pool_size_min'] / 1048576)
497 );
498 }
499
500 $waitTimeout = self::probeIntFromValues($values, 'wait_timeout');
501 $interactiveTimeout = self::probeIntFromValues($values, 'interactive_timeout');
502 if ($waitTimeout > 0 && $waitTimeout < $t['wait_timeout_min']) {
503 $warnings[] = sprintf(
504 'wait_timeout=%ds (< %ds); the build connection can drop mid-stage '
505 . 'leaving the buffer table orphaned (cf. orphan-table cleanup at runner startup).',
506 $waitTimeout, $t['wait_timeout_min']
507 );
508 }
509 if ($interactiveTimeout > 0 && $interactiveTimeout < $t['interactive_timeout_min']) {
510 $warnings[] = sprintf(
511 'interactive_timeout=%ds (< %ds); same orphan-table risk as wait_timeout.',
512 $interactiveTimeout, $t['interactive_timeout_min']
513 );
514 }
515
516 $flush = strtoupper(self::probeStringFromValues($values, 'innodb_flush_method'));
517 if ($flush === 'O_DSYNC') {
518 $warnings[] = 'innodb_flush_method=O_DSYNC; this is the slowest flush '
519 . 'mode and large stage writes will be much slower than O_DIRECT.';
520 }
521
522 $charset = strtolower(self::probeStringFromValues($values, 'character_set_server'));
523 $collation = strtolower(self::probeStringFromValues($values, 'collation_server'));
524 if ($charset !== '' && strpos($charset, 'utf8mb4') !== 0) {
525 $warnings[] = sprintf(
526 'character_set_server=%s (not utf8mb4); 4-byte characters in URLs '
527 . 'will be truncated or rejected by the server default.',
528 $charset
529 );
530 }
531 if ($collation !== '' && strpos($collation, 'utf8mb4') !== 0) {
532 $warnings[] = sprintf(
533 'collation_server=%s (not utf8mb4); 4-byte characters in URLs '
534 . 'may sort or compare unexpectedly.',
535 $collation
536 );
537 }
538
539 $requirePk = strtoupper(self::probeStringFromValues($values, 'sql_require_primary_key'));
540 if ($requirePk === 'ON' || $requirePk === '1') {
541 $warnings[] = 'sql_require_primary_key=ON; future CREATE TABLE without '
542 . 'a primary key will be rejected by the server.';
543 }
544
545 $filePerTable = strtoupper(self::probeStringFromValues($values, 'innodb_file_per_table'));
546 if ($filePerTable === 'OFF' || $filePerTable === '0') {
547 $warnings[] = 'innodb_file_per_table=OFF; new InnoDB tables share the '
548 . 'system tablespace and cannot be reclaimed by DROP.';
549 }
550
551 $threadStack = self::probeIntFromValues($values, 'thread_stack');
552 if ($threadStack > 0 && $threadStack < $t['thread_stack_min']) {
553 $warnings[] = sprintf(
554 'thread_stack=%d bytes (< %dK); deeply nested SQL may exhaust '
555 . 'the connection thread stack.',
556 $threadStack, (int)($t['thread_stack_min'] / 1024)
557 );
558 }
559 $openFiles = self::probeIntFromValues($values, 'open_files_limit');
560 if ($openFiles > 0 && $openFiles < $t['open_files_limit_min']) {
561 $warnings[] = sprintf(
562 'open_files_limit=%d (< %d); high-concurrency table opens may '
563 . 'fail with "Too many open files".',
564 $openFiles, $t['open_files_limit_min']
565 );
566 }
567
568 $alterLog = self::probeIntFromValues($values, 'innodb_online_alter_log_max_size');
569 if ($alterLog > 0 && $alterLog < $t['innodb_online_alter_log_max_size_min']) {
570 $warnings[] = sprintf(
571 'innodb_online_alter_log_max_size=%d (< %d MB); the S3 / S10 '
572 . 'ALTER TABLE ADD INDEX may fail with "Online DDL log overflow" '
573 . 'on busy hosts.',
574 $alterLog, (int)($t['innodb_online_alter_log_max_size_min'] / 1048576)
575 );
576 }
577
578 return $warnings;
579 }
580
581 /**
582 * Surface a single deduplicated admin notice consolidating the session
583 * variable warnings (per the task spec: one concise warning per S1 entry,
584 * no per-stage spam). Dedup window matches the existing self-healing
585 * notice TTL (24h).
586 *
587 * @param array<int,string> $warnings
588 * @return void
589 */
590 public function setSessionEnvAdminNotice(array $warnings): void {
591 $key = 'abj404_view_build_session_env_notice';
592 $payload = array(
593 'kind' => 'session_env',
594 'warnings' => $warnings,
595 'message' => 'The 404 Solution view-build pipeline detected MySQL '
596 . 'session-variable settings that may degrade the next rebuild: '
597 . implode(' | ', $warnings),
598 'when' => $this->clock()->now(),
599 );
600 $dedupKey = 'abj404_view_build_session_env_dedup';
601 if (function_exists('get_transient') && get_transient($dedupKey) !== false) {
602 return;
603 }
604 if (function_exists('set_transient')) {
605 // allow-cache-empty: session-environment warning marker intentionally stores diagnostics, not query data.
606 set_transient(
607 $key,
608 $payload,
609 ABJ_404_Solution_ViewBuildConfig::VIEW_BUILD_DEGRADED_NOTICE_TTL_SECONDS
610 );
611 set_transient(
612 $dedupKey,
613 1,
614 ABJ_404_Solution_ViewBuildConfig::VIEW_BUILD_DEGRADED_NOTICE_TTL_SECONDS
615 );
616 } elseif (function_exists('update_option')) {
617 update_option($key, $payload, false);
618 }
619 }
620
621 /** @return void */
622 public function clearSessionVariablesProbeCache(): void {
623 $this->sessionVariablesProbeCache = null;
624 if (function_exists('delete_option')) {
625 delete_option($this->sessionVariablesProbeOptionName());
626 }
627 if (function_exists('delete_transient')) {
628 delete_transient('abj404_view_build_session_env_dedup');
629 }
630 }
631 }
632