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

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

412 lines 21.9 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 * Runner-owned `forceRestartViewBuild()` primitive (Phase 3a step 2 of the
9 * staged view-build watermark refactor; see
10 * docs/refactor-staged-view-build-watermark.md and queue task c554).
11 *
12 * Purpose. Replaces every external "discard whatever the runner has on disk
13 * and restart the build from scratch" caller (the diagnostic AJAX
14 * `?abj404_force_view_rebuild=1` path, the admin "rebuild now" button if
15 * any, the WP-CLI rebuild commands -- migrated by Phase 3a step 4 and the
16 * Cluster A-D tasks). Before Phase 4, those callers reached
17 * `invalidateViewDone()` directly, which violated the runner-ownership
18 * invariant the refactor exists to restore; Phase 4 (commit 2994e21c)
19 * deleted that symbol and routed every external caller through either
20 * `bumpMutationWatermark()` (source-mutation signal) or this primitive
21 * (explicit restart-from-scratch).
22 *
23 * The contract is exactly seven bullets (and the absence of an eighth):
24 *
25 * 1. Acquire the runner lock.
26 * 2. Drop the runner-owned buffer table (`view_build`).
27 * 3. Clear runner progress options (registry + prefix-at-S1 capture +
28 * probe caches -- the same set `clearAllProgressOptions()` owns).
29 * 4. Clear `active_build_started_watermark` (the in-flight build's
30 * S1-entry stamp). The sibling `last_build_started_watermark`
31 * stays put -- it is diagnostic-only and survives both abort and
32 * force-rebuild so an operator can see the most recent stamp.
33 * 5. PRESERVE `built_watermark` (the prior successful build's published
34 * coverage). The rebuild is in flight, the old view_done snapshot is
35 * still serveable until the new S11 RENAME completes; deleting
36 * built_watermark would orphan the freshness signal until then.
37 * 6. Do NOT bump the mutation watermark (force-rebuild is a runner
38 * command, not a data-change signal). Bumping would propagate as a
39 * phantom mutation to every concurrent reader that bracketed this
40 * moment -- their stage-boundary checks would abort their own builds.
41 * 7. Schedule S0/S1 immediately (via the existing cron primitive
42 * `scheduleViewDoneRebuild()`). The lock is released before the
43 * schedule call so the cron tick can acquire cleanly.
44 *
45 * The primitive does NOT:
46 *
47 * - clear `clearStagedBuildDegradedState()`. A host-failed-degraded
48 * site stays degraded across the force-restart unless the caller
49 * explicitly clears the gate; that decision is a caller policy, not
50 * a runner primitive.
51 * - run S0/S1 inline. Callers in request contexts that want immediate
52 * progress (the AJAX force-rebuild path) call `advanceViewBuildOnce()`
53 * after this primitive returns; callers without a request context
54 * (CLI, admin "rebuild now") rely on the scheduled cron tick.
55 * - write to `built_watermark` for any reason.
56 *
57 * Failure modes.
58 *
59 * - Lock contention. If `acquireViewBuildLock(N)` returns false (a
60 * sibling cron tick or admin worker is mid-stage), the primitive
61 * returns false without touching any runner state. Callers retry
62 * on the next request; the in-flight build either completes
63 * normally (best case) or aborts at its next stage boundary and
64 * the next force-restart attempt will find the lock free.
65 *
66 * - No `scheduleViewDoneRebuild()` on lock contention. Without the
67 * cleanup, scheduling a tick would just race the already-running
68 * tick. The existing holder is already advancing the build.
69 *
70 * Allowlist note. The host file
71 * (`includes/DataAccessTrait_ViewBuildForceRestart.php`) matches the
72 * runner-owned `DataAccessTrait_ViewBuild*` glob convention that the
73 * Phase 4 semantic forbidden-operation lint (Codex #6 resolution; see
74 * StagedBuildOwnershipLintTest) will use to define its allowlist of
75 * runner-owned files. The primitive itself issues no DROP TABLE
76 * (delegated to `dropTransientBuffersIfPresent()` in the
77 * StageCallbacks trait, which IS the allowed owner today) and no direct
78 * progress-option writes (delegated to `clearAllProgressOptions()` in
79 * the Helpers trait, which IS the allowed owner today), so no allowlist
80 * updates are required for the lints that are active on current HEAD.
81 *
82 * Sibling traits. `ABJ_404_Solution_DataAccess_ViewBuildHelpersTrait`,
83 * `ABJ_404_Solution_DataAccess_ViewBuildLockAndCronTrait`,
84 * `ABJ_404_Solution_DataAccess_ViewBuildStageCallbacksTrait`; all three
85 * provide the helpers this primitive composes (lock acquire/release,
86 * buffer drop, progress clear, watermark stamp clear, rebuild
87 * scheduling). All four traits are mixed into
88 * `ABJ_404_Solution_DataAccess`.
89 *
90 * @property ABJ_404_Solution_DatabaseCore $dbCore
91 * @property ABJ_404_Solution_Functions $f
92 * @property ABJ_404_Solution_Logging $logger
93 * @property ABJ_404_Solution_ViewReadService|null $viewReadService
94 * @property ABJ_404_Solution_LogsRepository|null $logsRepo
95 * @property int $stagedQueryTimeoutSeconds
96 * @property string $lastBatchProgressDetail
97 * @property bool $viewBuildStageOpenForShutdown
98 * @property int $viewBuildShutdownStageNumber
99 * @property string $viewBuildShutdownStageKey
100 * @property bool|null $namedLockSupportedThisRequest
101 * @property bool $fallbackLockLoggedThisRequest
102 * @property bool $usingTransientFallbackLock
103 * @property string $lastNamedLockUnsupportedReason
104 * @property string $lastNamedLockUnsupportedError
105 * @method void abortStagedBuildForMutationWatermarkAdvance(...$arguments)
106 * @method bool acquireTransientFallbackLock(...$arguments)
107 * @method bool acquireViewBuildLock(...$arguments)
108 * @method string activeBuildStartedWatermarkOptionName(...$arguments)
109 * @method bool adminMutationGateBlocks(...$arguments)
110 * @method array<mixed> advanceViewBuildOnce(...$arguments)
111 * @method void assertBuildBufferExistsOrHalt(...$arguments)
112 * @method ?bool attemptRelaxSqlModeForBuildConnection(...$arguments)
113 * @method bool bufferIntegrityPassesForPromote(...$arguments)
114 * @method string buildHaltTransientKey(...$arguments)
115 * @method string buildViewDoneCountQuery(...$arguments)
116 * @method string builtWatermarkOptionName(...$arguments)
117 * @method int bumpMutationWatermark(...$arguments)
118 * @method int bumpStageNoProgressStreak(...$arguments)
119 * @method string capturedPrefixForLog(...$arguments)
120 * @method void capturePrefixAtBuildStart(...$arguments)
121 * @method void claimForegroundViewBuildLease(...$arguments)
122 * @method string classifyAndHandleStageFailure(...$arguments)
123 * @method array<mixed> classifySessionVariableWarnings(...$arguments)
124 * @method string classifyStageFailure(...$arguments)
125 * @method void clearActiveBuildStartedWatermark(...$arguments)
126 * @method void clearAdminMutationGateOptions(...$arguments)
127 * @method void clearAllProgressOptions(...$arguments)
128 * @method void clearPhpEnvironmentProbeCache(...$arguments)
129 * @method void clearPrefixAtStageOne(...$arguments)
130 * @method void clearSessionVariablesProbeCache(...$arguments)
131 * @method void clearSqlModeProbeCache(...$arguments)
132 * @method void clearStagedBuildDegradedState(...$arguments)
133 * @method void clearViewBuildOpenStageForShutdown(...$arguments)
134 * @method void clearViewDoneHardStaleNotice(...$arguments)
135 * @method ABJ_404_Solution_Clock clock(...$arguments)
136 * @method int countLiveRedirects(...$arguments)
137 * @method int countViewBuildRows(...$arguments)
138 * @method string describeBuildProgressForNotice(...$arguments)
139 * @method string describeDegradedNotice(...$arguments)
140 * @method string describeStagedSqlFailure(...$arguments)
141 * @method array<mixed> detectAndAdjustSqlMode(...$arguments)
142 * @method float detectHostStagedQueryLimitSeconds(...$arguments)
143 * @method string doTableNameReplacements(...$arguments)
144 * @method void dropDeletemeTable(...$arguments)
145 * @method void dropTransientBuffersIfPresent(...$arguments)
146 * @method void dropTransientStagedTables(...$arguments)
147 * @method void ensureConnection(...$arguments)
148 * @method void ensureFallbackLockNoticeAndLog(...$arguments)
149 * @method int extendedTimeoutForKilledNonBatchedStage(...$arguments)
150 * @method array<mixed> fetchSessionVariablesRowOrEmpty(...$arguments)
151 * @method string filesystemEnvironmentProbeOptionName(...$arguments)
152 * @method bool forceRestartViewBuild(...$arguments)
153 * @method bool foregroundViewBuildLeaseActive(...$arguments)
154 * @method string formatPhpMemoryBytesHuman(...$arguments)
155 * @method bool gateAbortIfMutationWatermarkAdvanced(...$arguments)
156 * @method string getColumnCollationString(...$arguments)
157 * @method int getCronStuckHours(...$arguments)
158 * @method string getLowercasePrefix(...$arguments)
159 * @method array<string, mixed> getViewBuildProgress(...$arguments)
160 * @method array<mixed> getViewBuildProgressFingerprint(...$arguments)
161 * @method int getViewDoneBuiltAtTimestamp(...$arguments)
162 * @method bool haltIfPrefixChangedSinceStageOne(...$arguments)
163 * @method string humanBatchProgress(...$arguments)
164 * @method float intelligentStagedQueryTimeoutSeconds(...$arguments)
165 * @method void invalidateViewDoneServeableCache(...$arguments)
166 * @method bool isBuildHaltedForHostFailure(...$arguments)
167 * @method bool isCurrentStageOptionName(...$arguments)
168 * @method bool isNamedLockUnsupportedError(...$arguments)
169 * @method bool isResumableStagedKill(...$arguments)
170 * @method bool isStageMarkedSkipped(...$arguments)
171 * @method bool isTransientConnectionError(...$arguments)
172 * @method string lastBuildStartedWatermarkOptionName(...$arguments)
173 * @method string legacyStartedWatermarkOptionName(...$arguments)
174 * @method string localizeOrDefaultViewBuildNotice(...$arguments)
175 * @method bool logsHitsTableExists(...$arguments)
176 * @method void logTimedViewBuildStage(...$arguments)
177 * @method void logViewBuildProgressOptionWrite(...$arguments)
178 * @method void logViewBuildShutdownDiagnostics(...$arguments)
179 * @method void markBuildHaltedForHostFailure(...$arguments)
180 * @method void markBuildStage(...$arguments)
181 * @method void markStageSkippedForHostFailure(...$arguments)
182 * @method void markViewBuildStageCompleted(...$arguments)
183 * @method void markViewBuildStageStarted(...$arguments)
184 * @method void markViewDoneBuildCompleted(...$arguments)
185 * @method void markViewDoneInvalidatedByAdminMutation(...$arguments)
186 * @method int maxBuildBufferId(...$arguments)
187 * @method void maybeRaiseViewDoneHardStaleNotice(...$arguments)
188 * @method bool mutationWatermarkAdvancedSinceBuildStart(...$arguments)
189 * @method int mutationWatermarkObservedByAdminAction(...$arguments)
190 * @method int mutationWatermarkObservedByAdminActionAt(...$arguments)
191 * @method string mutationWatermarkObservedByAdminActionAtOptionName(...$arguments)
192 * @method string mutationWatermarkObservedByAdminActionOptionName(...$arguments)
193 * @method string normalizePathPrefix(...$arguments)
194 * @method bool optionReadBackMatches(...$arguments)
195 * @method int parsePhpMemoryLimitToBytes(...$arguments)
196 * @method bool pathFallsWithinAny(...$arguments)
197 * @method void performFreshStartCleanup(...$arguments)
198 * @method array<mixed> phpDisabledFunctionsList(...$arguments)
199 * @method string phpEnvironmentProbeOptionName(...$arguments)
200 * @method float phpTimeRemainingSeconds(...$arguments)
201 * @method string prefixAtStageOneOptionName(...$arguments)
202 * @method array<mixed> probeFilesystemEnvironmentForBuild(...$arguments)
203 * @method float probeFloatFromValues(...$arguments)
204 * @method int probeIntFromValues(...$arguments)
205 * @method int probeMemoryLimitForS9(...$arguments)
206 * @method array<mixed> probePhpEnvironmentForBuild(...$arguments)
207 * @method array<mixed> probeSessionVariablesAtS1Entry(...$arguments)
208 * @method bool probeSetTimeLimitAvailability(...$arguments)
209 * @method array<mixed> probeSqlModeForBuild(...$arguments)
210 * @method string probeStringFromValues(...$arguments)
211 * @method string progressOptionName(...$arguments)
212 * @method void publishBuiltWatermarkFromActiveBuildStartedWatermark(...$arguments)
213 * @method array<mixed> queryAndGetResults(...$arguments)
214 * @method int readActiveBuildStartedWatermark(...$arguments)
215 * @method array<int, array<string, mixed>> readFromViewDone(...$arguments)
216 * @method int readProgressOption(...$arguments)
217 * @method int readWatermarkOption(...$arguments)
218 * @method void rebuildViewDoneInBackground(...$arguments)
219 * @method bool reconcilePostStageElevenState(...$arguments)
220 * @method string reconcileStagedTablesAtRunnerStartup(...$arguments)
221 * @method int recordStageBatchKilled(...$arguments)
222 * @method void registerViewBuildShutdownDiagnostics(...$arguments)
223 * @method bool releaseAndReacquireBetweenStages(...$arguments)
224 * @method void releaseViewBuildLock(...$arguments)
225 * @method void resetStageNoProgressStreak(...$arguments)
226 * @method string resolveColumnCollationForStagedBuild(...$arguments)
227 * @method void runForceRestartCleanupInsideLock(...$arguments)
228 * @method bool runIdRangeBatchedUpdate(...$arguments)
229 * @method int runInsertBatch(...$arguments)
230 * @method mixed runNonBatchedStageWithKillStreakEscape(...$arguments)
231 * @method array{ran: bool, reason: string, progress: array<string, mixed>} runPageLoadFallbackAdvance(...$arguments)
232 * @method int runRedirectsForViewCountStaged(...$arguments)
233 * @method array<int, array<string, mixed>> runRedirectsForViewStaged(...$arguments)
234 * @method bool runS11SwapWithPreRenameWatermarkRecheck(...$arguments)
235 * @method bool runStagedBuildOnce(...$arguments)
236 * @method bool runStagedBuildStages6Through11(...$arguments)
237 * @method void runStagedSqlFile(...$arguments)
238 * @method void runStagedSqlFileTolerantOfDuplicateKey(...$arguments)
239 * @method mixed runTimedViewBuildStage(...$arguments)
240 * @method int safeCurrentMutationWatermark(...$arguments)
241 * @method string sanitizeUrlBeforeInsert(...$arguments)
242 * @method void scheduleViewDoneRebuild(...$arguments)
243 * @method string sessionVariablesProbeOptionName(...$arguments)
244 * @method void setFilesystemEnvAdminNotice(...$arguments)
245 * @method void setLowMemoryLimitAdminNotice(...$arguments)
246 * @method void setSessionEnvAdminNotice(...$arguments)
247 * @method void setStagedBuildDegradedNotice(...$arguments)
248 * @method void setStagedBuildHaltNotice(...$arguments)
249 * @method void setViewBuildCronStuckNotice(...$arguments)
250 * @method void setViewBuildScheduleFailedNotice(...$arguments)
251 * @method void setViewDoneHardStaleNotice(...$arguments)
252 * @method array<mixed> splitOpenBasedirPaths(...$arguments)
253 * @method string sqlModeProbeOptionName(...$arguments)
254 * @method void stageAddPreJoinIndexes(...$arguments)
255 * @method void stageAddSortIndexes(...$arguments)
256 * @method void stageCreateBuildTable(...$arguments)
257 * @method array<string, mixed> stagedQueryOptions(...$arguments)
258 * @method bool stagedTableExists(...$arguments)
259 * @method bool stageInsertRedirectsBatched(...$arguments)
260 * @method string stageNoProgressStreakOptionName(...$arguments)
261 * @method void stageRenameSwap(...$arguments)
262 * @method string stageSkipOptionName(...$arguments)
263 * @method void stageUpdateExternal(...$arguments)
264 * @method void stageUpdateHits(...$arguments)
265 * @method void stageUpdateHome(...$arguments)
266 * @method bool stageUpdatePostsBatched(...$arguments)
267 * @method void stageUpdateSpecial(...$arguments)
268 * @method bool stageUpdateTermsBatched(...$arguments)
269 * @method void stampStartedWatermarksAtS1Entry(...$arguments)
270 * @method void sweepStaleRebuildTransients(...$arguments)
271 * @method string transientFallbackLockOptionName(...$arguments)
272 * @method bool verifyBuildLockSerializesWriter(...$arguments)
273 * @method bool verifyOptionWriteCoherent(...$arguments)
274 * @method bool verifyPrefixUnchangedSinceStageOne(...$arguments)
275 * @method int viewBuildBatchSize(...$arguments)
276 * @method int viewBuildBatchSizeForStage(...$arguments)
277 * @method array<mixed> viewBuildOnlyTranslations(...$arguments)
278 * @method float viewBuildPerStageBudgetSeconds(...$arguments)
279 * @method string viewBuildTableName(...$arguments)
280 * @method string viewDeletemeTableName(...$arguments)
281 * @method int viewDoneBuiltAt(...$arguments)
282 * @method int viewDoneBuiltWatermark(...$arguments)
283 * @method int viewDoneDataBuiltAt(...$arguments)
284 * @method string viewDoneDataBuiltAtOptionName(...$arguments)
285 * @method string viewDoneFreshnessOptionName(...$arguments)
286 * @method bool viewDoneHasRows(...$arguments)
287 * @method bool viewDoneIsFresh(...$arguments)
288 * @method bool viewDoneIsServeable(...$arguments)
289 * @method int viewDoneMutationInvalidatedAt(...$arguments)
290 * @method string viewDoneMutationInvalidatedAtOptionName(...$arguments)
291 * @method bool viewDoneTableExists(...$arguments)
292 * @method string viewDoneTableName(...$arguments)
293 * @method void writeProgressOption(...$arguments)
294 * @method void writeWatermarkOption(...$arguments)
295 */
296 class ABJ_404_Solution_ViewBuildForceRestart extends ABJ_404_Solution_ViewBuildCollaborator {
297
298 /**
299 * Runner-owned force-restart primitive. Per Phase 3a step 2 (c554).
300 *
301 * Returns true when the restart completed cleanly: lock acquired,
302 * buffer dropped, progress cleared, active_build_started_watermark
303 * cleared, last_build_started_watermark preserved (diagnostic),
304 * built_watermark preserved (cross-build pre-image), watermark
305 * counter unchanged, rebuild scheduled. Returns false when the lock
306 * could not be acquired within `$lockTimeoutSeconds`; the caller
307 * may retry on the next request.
308 *
309 * Default lock wait of 10s matches the existing
310 * `?abj404_force_view_rebuild=1` AJAX handler in
311 * `advanceViewBuildOnce()` -- comfortable headroom inside a 30s PHP
312 * request budget so a typical cron build mid-flight has time to
313 * release before we return false to the caller. Inlined as the
314 * parameter default rather than a trait const because const-in-trait
315 * is PHP 8.2 and the plugin targets PHP 7.4.
316 *
317 * @param int $lockTimeoutSeconds GET_LOCK wait-time, default 10s.
318 * Pass 0 for a non-blocking acquire (callers that prefer to
319 * retry than to wait).
320 * @return bool true on success, false if lock contended.
321 */
322 public function forceRestartViewBuild(int $lockTimeoutSeconds = 10): bool {
323 // (1) Acquire runner lock. Returns false if a sibling worker (cron
324 // tick, admin form save, REST PUT) holds it -- caller retries.
325 if (!$this->acquireViewBuildLock(max(0, $lockTimeoutSeconds))) {
326 return false;
327 }
328
329 try {
330 $this->runForceRestartCleanupInsideLock();
331 if ($this->rebuildHealth instanceof ABJ_404_Solution_RebuildHealthState) {
332 $this->rebuildHealth->reset();
333 $this->rebuildHealth->acquireTrialToken();
334 }
335 } finally {
336 // Release the lock BEFORE scheduling the next tick so the
337 // cron callback can acquire cleanly. A leaked lock would
338 // stall every subsequent build attempt until the
339 // session-scoped GET_LOCK times out.
340 $this->releaseViewBuildLock();
341 }
342
343 // (7) Schedule S0/S1 immediately. scheduleViewDoneRebuild() is
344 // idempotent (wp_next_scheduled short-circuit) so callers
345 // can chain or replay safely. Cron tick will drive S0 fresh
346 // cleanup -> S1 prefix capture + started-watermark re-stamp
347 // -> S2..S11.
348 $this->scheduleViewDoneRebuild();
349
350 return true;
351 }
352
353 /**
354 * Inside-lock cleanup phase of force-restart, callable by code paths
355 * that already hold the view-build lock and intend to drive the
356 * subsequent S0/S1 run inline (e.g. advanceViewBuildOnce() with
357 * forceRebuild=true). Public callers should prefer
358 * {@see forceRestartViewBuild()} -- this helper does NOT acquire the
359 * lock and does NOT schedule the next cron tick.
360 *
361 * Performs steps 2-6 of the seven-bullet force-restart contract
362 * documented on the trait docblock above:
363 *
364 * - drop the runner-owned buffer table (and the deleteme leftover)
365 * - clear runner progress options + prefix-at-S1 capture
366 * - clear active_build_started_watermark
367 * - preserve built_watermark (no write, no delete)
368 * - DO NOT bump the mutation watermark
369 *
370 * Also resets the per-request serveability cache so a subsequent
371 * viewDoneIsServeable() inside the same request observes the new
372 * state, not the cached pre-cleanup value.
373 */
374 public function runForceRestartCleanupInsideLock(): void {
375 // (2) Drop the runner-owned buffer table (and the deleteme
376 // leftover from any prior crashed S11 RENAME swap).
377 // Gated by SHOW TABLES so a steady-state force-rebuild
378 // after a clean S11 (no buffer present) does not pile
379 // unconditional DDL on the hot path.
380 $this->dropTransientBuffersIfPresent();
381
382 // (3) Clear runner progress options. The helper owns the
383 // registry + prefix-at-S1 capture + sql_mode + php-env
384 // probe-cache clears as one atomic fresh-start step.
385 $this->clearAllProgressOptions();
386
387 // (4) Clear active_build_started_watermark. Lives outside
388 // the progress registry (so the S11 happy-path
389 // observability contract holds across the boundary), so
390 // it needs its own delete call. The sibling
391 // last_build_started_watermark is left alone --
392 // diagnostic-only, survives abort and force-restart.
393 $this->clearActiveBuildStartedWatermark();
394
395 // (5) PRESERVE built_watermark. No write, no delete. The
396 // prior successful build's published coverage stays as
397 // the cross-build pre-image for freshness checks until
398 // the new build's S11 swap publishes a fresh value.
399
400 // (6) DO NOT bump the mutation watermark. No call to
401 // ABJ_404_Solution_MutationWatermark::bump() exists in
402 // this method. Force-rebuild is a runner command, not a
403 // data-change signal; a bump would propagate to every
404 // concurrent reader as a phantom mutation.
405
406 // Reset per-request serveability cache so a subsequent
407 // viewDoneIsServeable() inside this request reflects the
408 // post-cleanup state rather than a stale-cached value.
409 $this->invalidateViewDoneServeableCache();
410 }
411 }
412