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

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

467 lines 24.3 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 * Admin-mutation visibility gate (Phase 4 of the staged view-build watermark
9 * refactor; see docs/refactor-staged-view-build-watermark.md and queue task
10 * t_260516_140130_872).
11 *
12 * The single observable contract: after an admin clicks Save on the redirects
13 * UI, the next AJAX fetch must NOT return a snapshot built before the
14 * mutation, OR it must fall back to stale-serving once the sanity timeout
15 * elapses (so a stuck cron does not block the admin redirects screen
16 * forever). Before Phase 4 the gate was driven by a unix timestamp
17 * comparison (`viewDoneMutationInvalidatedAt > viewDoneDataBuiltAt`). Phase
18 * 4 swaps the inputs to watermark comparison: `built_watermark` (published
19 * at S11 by the runner) vs `mutation_watermark_observed_by_admin_action`
20 * (the post-increment value the admin's bump returned at click-Save time).
21 *
22 * Three options:
23 *
24 * - `wp_abj404_view_done_mutation_watermark_observed_by_admin_action`:
25 * the post-increment watermark value the admin's bump returned. Set by
26 * {@see markViewDoneInvalidatedByAdminMutation()}; cleared by
27 * {@see markViewDoneBuildCompleted()} on success.
28 * - `wp_abj404_view_done_mutation_watermark_observed_at`: wall-clock
29 * timestamp paired with the observed value for the sanity-window
30 * fallback. Same lifetime as the observed-watermark option.
31 * - `wp_abj404_view_done_mutation_invalidated_at`: pre-Phase-4 timestamp
32 * option. Read as a cold-bootstrap fallback when the watermark class is
33 * not yet loaded (defensive guard); written by the same fallback path
34 * so an upgraded install that hits a cold path still gates reads.
35 * Cleaned up by {@see markViewDoneBuildCompleted()} for convergence.
36 *
37 * Comparison semantics (pinned by ViewDoneServeabilityWatermarkGateTest):
38 *
39 * - `built_watermark >= observed` releases the gate (the snapshot covers
40 * the admin mutation). `>=`, not `>`, so a build that exactly covers
41 * the observed watermark unblocks reads.
42 * - The comparison uses the OBSERVED value, NOT the live counter
43 * ({@see ABJ_404_Solution_MutationWatermark::current()}). Unrelated
44 * later mutations on a busy site advance the live counter past the
45 * admin's observed value; gating against the live counter would block
46 * reads forever even after a covering build completed.
47 * - The gate respects the same `VIEW_DONE_MUTATION_INVALIDATED_SANITY_
48 * SECONDS` upper bound the legacy timestamp gate did. After the window
49 * elapses the gate falls back to fbc270d8 stale-serving + the
50 * hard-stale admin notice; a stuck cron / broken build cannot block
51 * the admin redirects screen indefinitely.
52 *
53 * Composition. Mixed into `ABJ_404_Solution_DataAccess` alongside
54 * `ABJ_404_Solution_DataAccess_ViewQueriesStagedTrait` (which holds
55 * `viewDoneIsServeable()` and consults this trait's reader helpers),
56 * `ABJ_404_Solution_DataAccess_MutationWatermarkSeamTrait` (the
57 * `bumpMutationWatermark()` source-mutation seam), and
58 * `ABJ_404_Solution_DataAccess_ViewBuildStartedWatermarkTrait` (which
59 * supplies `builtWatermarkOptionName()` + `readWatermarkOption()`).
60 *
61 * @see ABJ_404_Solution_DataAccess_ViewQueriesStagedTrait
62 * @see ABJ_404_Solution_DataAccess_MutationWatermarkSeamTrait
63 * @see ABJ_404_Solution_DataAccess_ViewBuildStartedWatermarkTrait
64 *
65 * @property ABJ_404_Solution_DatabaseCore $dbCore
66 * @property ABJ_404_Solution_Functions $f
67 * @property ABJ_404_Solution_Logging $logger
68 * @property ABJ_404_Solution_ViewReadService|null $viewReadService
69 * @property ABJ_404_Solution_LogsRepository|null $logsRepo
70 * @property int $stagedQueryTimeoutSeconds
71 * @property string $lastBatchProgressDetail
72 * @property bool $viewBuildStageOpenForShutdown
73 * @property int $viewBuildShutdownStageNumber
74 * @property string $viewBuildShutdownStageKey
75 * @property bool|null $namedLockSupportedThisRequest
76 * @property bool $fallbackLockLoggedThisRequest
77 * @property bool $usingTransientFallbackLock
78 * @property string $lastNamedLockUnsupportedReason
79 * @property string $lastNamedLockUnsupportedError
80 * @method void abortStagedBuildForMutationWatermarkAdvance(...$arguments)
81 * @method bool acquireTransientFallbackLock(...$arguments)
82 * @method bool acquireViewBuildLock(...$arguments)
83 * @method string activeBuildStartedWatermarkOptionName(...$arguments)
84 * @method bool adminMutationGateBlocks(...$arguments)
85 * @method array<mixed> advanceViewBuildOnce(...$arguments)
86 * @method void assertBuildBufferExistsOrHalt(...$arguments)
87 * @method ?bool attemptRelaxSqlModeForBuildConnection(...$arguments)
88 * @method bool bufferIntegrityPassesForPromote(...$arguments)
89 * @method string buildHaltTransientKey(...$arguments)
90 * @method string buildViewDoneCountQuery(...$arguments)
91 * @method string builtWatermarkOptionName(...$arguments)
92 * @method int bumpMutationWatermark(...$arguments)
93 * @method int bumpStageNoProgressStreak(...$arguments)
94 * @method string capturedPrefixForLog(...$arguments)
95 * @method void capturePrefixAtBuildStart(...$arguments)
96 * @method void claimForegroundViewBuildLease(...$arguments)
97 * @method string classifyAndHandleStageFailure(...$arguments)
98 * @method array<mixed> classifySessionVariableWarnings(...$arguments)
99 * @method string classifyStageFailure(...$arguments)
100 * @method void clearActiveBuildStartedWatermark(...$arguments)
101 * @method void clearAdminMutationGateOptions(...$arguments)
102 * @method void clearAllProgressOptions(...$arguments)
103 * @method void clearPhpEnvironmentProbeCache(...$arguments)
104 * @method void clearPrefixAtStageOne(...$arguments)
105 * @method void clearSessionVariablesProbeCache(...$arguments)
106 * @method void clearSqlModeProbeCache(...$arguments)
107 * @method void clearStagedBuildDegradedState(...$arguments)
108 * @method void clearViewBuildOpenStageForShutdown(...$arguments)
109 * @method void clearViewDoneHardStaleNotice(...$arguments)
110 * @method ABJ_404_Solution_Clock clock(...$arguments)
111 * @method int countLiveRedirects(...$arguments)
112 * @method int countViewBuildRows(...$arguments)
113 * @method string describeBuildProgressForNotice(...$arguments)
114 * @method string describeDegradedNotice(...$arguments)
115 * @method string describeStagedSqlFailure(...$arguments)
116 * @method array<mixed> detectAndAdjustSqlMode(...$arguments)
117 * @method float detectHostStagedQueryLimitSeconds(...$arguments)
118 * @method string doTableNameReplacements(...$arguments)
119 * @method void dropDeletemeTable(...$arguments)
120 * @method void dropTransientBuffersIfPresent(...$arguments)
121 * @method void dropTransientStagedTables(...$arguments)
122 * @method void ensureConnection(...$arguments)
123 * @method void ensureFallbackLockNoticeAndLog(...$arguments)
124 * @method int extendedTimeoutForKilledNonBatchedStage(...$arguments)
125 * @method array<mixed> fetchSessionVariablesRowOrEmpty(...$arguments)
126 * @method string filesystemEnvironmentProbeOptionName(...$arguments)
127 * @method bool forceRestartViewBuild(...$arguments)
128 * @method bool foregroundViewBuildLeaseActive(...$arguments)
129 * @method string formatPhpMemoryBytesHuman(...$arguments)
130 * @method bool gateAbortIfMutationWatermarkAdvanced(...$arguments)
131 * @method string getColumnCollationString(...$arguments)
132 * @method int getCronStuckHours(...$arguments)
133 * @method string getLowercasePrefix(...$arguments)
134 * @method array<string, mixed> getViewBuildProgress(...$arguments)
135 * @method array<mixed> getViewBuildProgressFingerprint(...$arguments)
136 * @method int getViewDoneBuiltAtTimestamp(...$arguments)
137 * @method bool haltIfPrefixChangedSinceStageOne(...$arguments)
138 * @method string humanBatchProgress(...$arguments)
139 * @method float intelligentStagedQueryTimeoutSeconds(...$arguments)
140 * @method void invalidateViewDoneServeableCache(...$arguments)
141 * @method bool isBuildHaltedForHostFailure(...$arguments)
142 * @method bool isCurrentStageOptionName(...$arguments)
143 * @method bool isNamedLockUnsupportedError(...$arguments)
144 * @method bool isResumableStagedKill(...$arguments)
145 * @method bool isStageMarkedSkipped(...$arguments)
146 * @method bool isTransientConnectionError(...$arguments)
147 * @method string lastBuildStartedWatermarkOptionName(...$arguments)
148 * @method string legacyStartedWatermarkOptionName(...$arguments)
149 * @method string localizeOrDefaultViewBuildNotice(...$arguments)
150 * @method bool logsHitsTableExists(...$arguments)
151 * @method void logTimedViewBuildStage(...$arguments)
152 * @method void logViewBuildProgressOptionWrite(...$arguments)
153 * @method void logViewBuildShutdownDiagnostics(...$arguments)
154 * @method void markBuildHaltedForHostFailure(...$arguments)
155 * @method void markBuildStage(...$arguments)
156 * @method void markStageSkippedForHostFailure(...$arguments)
157 * @method void markViewBuildStageCompleted(...$arguments)
158 * @method void markViewBuildStageStarted(...$arguments)
159 * @method void markViewDoneBuildCompleted(...$arguments)
160 * @method void markViewDoneInvalidatedByAdminMutation(...$arguments)
161 * @method int maxBuildBufferId(...$arguments)
162 * @method void maybeRaiseViewDoneHardStaleNotice(...$arguments)
163 * @method bool mutationWatermarkAdvancedSinceBuildStart(...$arguments)
164 * @method int mutationWatermarkObservedByAdminAction(...$arguments)
165 * @method int mutationWatermarkObservedByAdminActionAt(...$arguments)
166 * @method string mutationWatermarkObservedByAdminActionAtOptionName(...$arguments)
167 * @method string mutationWatermarkObservedByAdminActionOptionName(...$arguments)
168 * @method string normalizePathPrefix(...$arguments)
169 * @method bool optionReadBackMatches(...$arguments)
170 * @method int parsePhpMemoryLimitToBytes(...$arguments)
171 * @method bool pathFallsWithinAny(...$arguments)
172 * @method void performFreshStartCleanup(...$arguments)
173 * @method array<mixed> phpDisabledFunctionsList(...$arguments)
174 * @method string phpEnvironmentProbeOptionName(...$arguments)
175 * @method float phpTimeRemainingSeconds(...$arguments)
176 * @method string prefixAtStageOneOptionName(...$arguments)
177 * @method array<mixed> probeFilesystemEnvironmentForBuild(...$arguments)
178 * @method float probeFloatFromValues(...$arguments)
179 * @method int probeIntFromValues(...$arguments)
180 * @method int probeMemoryLimitForS9(...$arguments)
181 * @method array<mixed> probePhpEnvironmentForBuild(...$arguments)
182 * @method array<mixed> probeSessionVariablesAtS1Entry(...$arguments)
183 * @method bool probeSetTimeLimitAvailability(...$arguments)
184 * @method array<mixed> probeSqlModeForBuild(...$arguments)
185 * @method string probeStringFromValues(...$arguments)
186 * @method string progressOptionName(...$arguments)
187 * @method void publishBuiltWatermarkFromActiveBuildStartedWatermark(...$arguments)
188 * @method array<mixed> queryAndGetResults(...$arguments)
189 * @method int readActiveBuildStartedWatermark(...$arguments)
190 * @method array<int, array<string, mixed>> readFromViewDone(...$arguments)
191 * @method int readProgressOption(...$arguments)
192 * @method int readWatermarkOption(...$arguments)
193 * @method void rebuildViewDoneInBackground(...$arguments)
194 * @method bool reconcilePostStageElevenState(...$arguments)
195 * @method string reconcileStagedTablesAtRunnerStartup(...$arguments)
196 * @method int recordStageBatchKilled(...$arguments)
197 * @method void registerViewBuildShutdownDiagnostics(...$arguments)
198 * @method bool releaseAndReacquireBetweenStages(...$arguments)
199 * @method void releaseViewBuildLock(...$arguments)
200 * @method void resetStageNoProgressStreak(...$arguments)
201 * @method string resolveColumnCollationForStagedBuild(...$arguments)
202 * @method void runForceRestartCleanupInsideLock(...$arguments)
203 * @method bool runIdRangeBatchedUpdate(...$arguments)
204 * @method int runInsertBatch(...$arguments)
205 * @method mixed runNonBatchedStageWithKillStreakEscape(...$arguments)
206 * @method array{ran: bool, reason: string, progress: array<string, mixed>} runPageLoadFallbackAdvance(...$arguments)
207 * @method int runRedirectsForViewCountStaged(...$arguments)
208 * @method array<int, array<string, mixed>> runRedirectsForViewStaged(...$arguments)
209 * @method bool runS11SwapWithPreRenameWatermarkRecheck(...$arguments)
210 * @method bool runStagedBuildOnce(...$arguments)
211 * @method bool runStagedBuildStages6Through11(...$arguments)
212 * @method void runStagedSqlFile(...$arguments)
213 * @method void runStagedSqlFileTolerantOfDuplicateKey(...$arguments)
214 * @method mixed runTimedViewBuildStage(...$arguments)
215 * @method int safeCurrentMutationWatermark(...$arguments)
216 * @method string sanitizeUrlBeforeInsert(...$arguments)
217 * @method void scheduleViewDoneRebuild(...$arguments)
218 * @method string sessionVariablesProbeOptionName(...$arguments)
219 * @method void setFilesystemEnvAdminNotice(...$arguments)
220 * @method void setLowMemoryLimitAdminNotice(...$arguments)
221 * @method void setSessionEnvAdminNotice(...$arguments)
222 * @method void setStagedBuildDegradedNotice(...$arguments)
223 * @method void setStagedBuildHaltNotice(...$arguments)
224 * @method void setViewBuildCronStuckNotice(...$arguments)
225 * @method void setViewBuildScheduleFailedNotice(...$arguments)
226 * @method void setViewDoneHardStaleNotice(...$arguments)
227 * @method array<mixed> splitOpenBasedirPaths(...$arguments)
228 * @method string sqlModeProbeOptionName(...$arguments)
229 * @method void stageAddPreJoinIndexes(...$arguments)
230 * @method void stageAddSortIndexes(...$arguments)
231 * @method void stageCreateBuildTable(...$arguments)
232 * @method array<string, mixed> stagedQueryOptions(...$arguments)
233 * @method bool stagedTableExists(...$arguments)
234 * @method bool stageInsertRedirectsBatched(...$arguments)
235 * @method string stageNoProgressStreakOptionName(...$arguments)
236 * @method void stageRenameSwap(...$arguments)
237 * @method string stageSkipOptionName(...$arguments)
238 * @method void stageUpdateExternal(...$arguments)
239 * @method void stageUpdateHits(...$arguments)
240 * @method void stageUpdateHome(...$arguments)
241 * @method bool stageUpdatePostsBatched(...$arguments)
242 * @method void stageUpdateSpecial(...$arguments)
243 * @method bool stageUpdateTermsBatched(...$arguments)
244 * @method void stampStartedWatermarksAtS1Entry(...$arguments)
245 * @method void sweepStaleRebuildTransients(...$arguments)
246 * @method string transientFallbackLockOptionName(...$arguments)
247 * @method bool verifyBuildLockSerializesWriter(...$arguments)
248 * @method bool verifyOptionWriteCoherent(...$arguments)
249 * @method bool verifyPrefixUnchangedSinceStageOne(...$arguments)
250 * @method int viewBuildBatchSize(...$arguments)
251 * @method int viewBuildBatchSizeForStage(...$arguments)
252 * @method array<mixed> viewBuildOnlyTranslations(...$arguments)
253 * @method float viewBuildPerStageBudgetSeconds(...$arguments)
254 * @method string viewBuildTableName(...$arguments)
255 * @method string viewDeletemeTableName(...$arguments)
256 * @method int viewDoneBuiltAt(...$arguments)
257 * @method int viewDoneBuiltWatermark(...$arguments)
258 * @method int viewDoneDataBuiltAt(...$arguments)
259 * @method string viewDoneDataBuiltAtOptionName(...$arguments)
260 * @method string viewDoneFreshnessOptionName(...$arguments)
261 * @method bool viewDoneHasRows(...$arguments)
262 * @method bool viewDoneIsFresh(...$arguments)
263 * @method bool viewDoneIsServeable(...$arguments)
264 * @method int viewDoneMutationInvalidatedAt(...$arguments)
265 * @method string viewDoneMutationInvalidatedAtOptionName(...$arguments)
266 * @method bool viewDoneTableExists(...$arguments)
267 * @method string viewDoneTableName(...$arguments)
268 * @method void writeProgressOption(...$arguments)
269 * @method void writeWatermarkOption(...$arguments)
270 */
271 class ABJ_404_Solution_AdminMutationGate extends ABJ_404_Solution_ViewBuildCollaborator {
272
273 /**
274 * Phase 4 replacement for the timestamp-based admin-mutation gate.
275 * Records the {@see ABJ_404_Solution_MutationWatermark::current()}
276 * value observed at the moment {@see markViewDoneInvalidatedByAdmin
277 * Mutation()} fires, so {@see viewDoneIsServeable()} can compare it
278 * against `built_watermark` (the watermark covered by the last
279 * successful build, published at S11 by {@see publishBuiltWatermark
280 * FromActiveBuildStartedWatermark()}). The gate blocks reads while
281 * `built_watermark < observed`, i.e. while the snapshot on disk
282 * does not yet cover the admin's mutation.
283 */
284 public function mutationWatermarkObservedByAdminActionOptionName(): string {
285 return $this->getLowercasePrefix() . 'abj404_view_done_mutation_watermark_observed_by_admin_action';
286 }
287
288 /**
289 * Sanity-window timestamp for the observed-watermark gate. Set
290 * alongside the observed-watermark value at click-Save time so {@see
291 * viewDoneIsServeable()} can apply the same `VIEW_DONE_MUTATION_
292 * INVALIDATED_SANITY_SECONDS` bound as the legacy timestamp-based
293 * gate: a stuck cron / broken build cannot keep view_done unserveable
294 * forever; after the sanity window the gate falls back to fbc270d8
295 * stale-serving.
296 */
297 public function mutationWatermarkObservedByAdminActionAtOptionName(): string {
298 return $this->getLowercasePrefix() . 'abj404_view_done_mutation_watermark_observed_at';
299 }
300
301 /**
302 * Pre-Phase-4 timestamp option. Retained for two reasons: (1) cold-
303 * bootstrap fallback when the watermark class is not yet loaded;
304 * (2) cleanup target on `markViewDoneBuildCompleted()` so upgraded
305 * installs converge to "no admin gate" after their first successful
306 * build.
307 */
308 public function viewDoneMutationInvalidatedAtOptionName(): string {
309 return $this->getLowercasePrefix() . 'abj404_view_done_mutation_invalidated_at';
310 }
311
312 /**
313 * Read the watermark value the admin observed at click-Save time.
314 * Returns 0 when no admin mutation has been recorded since the last
315 * build completion (option absent or cleared by
316 * {@see markViewDoneBuildCompleted()}).
317 */
318 public function mutationWatermarkObservedByAdminAction(): int {
319 if (!function_exists('get_option')) {
320 return 0;
321 }
322 $val = get_option($this->mutationWatermarkObservedByAdminActionOptionName(), 0);
323 return is_scalar($val) ? max(0, intval($val)) : 0;
324 }
325
326 /** @return int Unix timestamp the admin-mutation watermark was observed, or 0. */
327 public function mutationWatermarkObservedByAdminActionAt(): int {
328 if (!function_exists('get_option')) {
329 return 0;
330 }
331 $val = get_option($this->mutationWatermarkObservedByAdminActionAtOptionName(), 0);
332 return is_scalar($val) ? max(0, intval($val)) : 0;
333 }
334
335 /** @return int Unix timestamp of the last admin-initiated mutation, or 0. */
336 public function viewDoneMutationInvalidatedAt(): int {
337 if (!function_exists('get_option')) {
338 return 0;
339 }
340 $val = get_option($this->viewDoneMutationInvalidatedAtOptionName(), 0);
341 return is_scalar($val) ? max(0, intval($val)) : 0;
342 }
343
344 /**
345 * Read the `built_watermark` published at the last successful S11
346 * completion. Returns 0 when no successful build has run on this
347 * install yet, so the gate naturally treats a fresh install as "no
348 * admin mutation is covered yet" -- harmless because on a fresh
349 * install the observed-admin-mutation-watermark option is also
350 * absent.
351 */
352 public function viewDoneBuiltWatermark(): int {
353 $value = $this->readWatermarkOption($this->builtWatermarkOptionName());
354 return $value < 0 ? 0 : $value;
355 }
356
357 /**
358 * True when the admin-mutation gate is currently blocking reads. The
359 * gate fires when an observed-watermark is recorded, the observation
360 * is within the sanity window, AND `built_watermark` has not yet
361 * caught up to the observed value. Otherwise false (no gate state,
362 * gate expired, or build already covers the mutation).
363 *
364 * Called from {@see viewDoneIsServeable()} as the sole admin-gate
365 * check; the staged-queries trait does not consult any of the
366 * underlying options directly.
367 */
368 public function adminMutationGateBlocks(): bool {
369 $observedWatermark = $this->mutationWatermarkObservedByAdminAction();
370 if ($observedWatermark <= 0) {
371 return false;
372 }
373 $observedAt = $this->mutationWatermarkObservedByAdminActionAt();
374 if ($observedAt <= 0) {
375 return false;
376 }
377 $sanity = ABJ_404_Solution_ViewBuildConfig::VIEW_DONE_MUTATION_INVALIDATED_SANITY_SECONDS;
378 if ($observedAt <= time() - $sanity) {
379 return false;
380 }
381 $builtWatermark = $this->viewDoneBuiltWatermark();
382 return $builtWatermark < $observedWatermark;
383 }
384
385 /**
386 * Mark view_done as needing a fresh build because the admin just
387 * mutated a redirect through the UI (add/edit/trash/delete). Phase 4
388 * mechanism: bump the mutation watermark and record the
389 * post-increment value in
390 * `mutation_watermark_observed_by_admin_action`;
391 * {@see viewDoneIsServeable()} then blocks reads until
392 * `built_watermark >= the recorded value` (or the sanity timeout
393 * elapses). The runner observes the bump at the next stage boundary
394 * and aborts/restarts the in-flight build so the next snapshot
395 * covers the admin's change.
396 *
397 * Differs from a plain `bumpMutationWatermark()` call (Cluster A-D
398 * callers): admin actions need IMMEDIATE feedback, so the recorded
399 * observed value drives the stricter gate that pends the AJAX fetch
400 * until a covering build completes. Non-admin mutations only need
401 * the runner to abort/restart at the next stage boundary; they don't
402 * need to block reads in the meantime (fbc270d8 stale-serving).
403 */
404 public function markViewDoneInvalidatedByAdminMutation(): void {
405 // Bump the watermark so the build runner sees the admin mutation at
406 // the next stage boundary. This is the sole watermark bump for admin
407 // actions; invalidateViewSnapshotCache() no longer bumps (that
408 // caused an infinite abort cycle on high-traffic sites where every
409 // captured 404 was resetting the build).
410 $observed = $this->bumpMutationWatermark();
411 if (!function_exists('update_option')) {
412 return;
413 }
414 if ($observed <= 0) {
415 // Watermark primitive still unavailable after the fallback
416 // bump attempt (cold-bootstrap path before the autoloader
417 // resolves MutationWatermark.php). Stamp the legacy timestamp
418 // option so a legacy installation of viewDoneIsServeable() can
419 // still gate reads if it ever sees this state.
420 update_option($this->viewDoneMutationInvalidatedAtOptionName(), time(), false);
421 return;
422 }
423 // Record the watermark and a wall-clock timestamp so
424 // viewDoneIsServeable() can apply the sanity timeout to the gate
425 // the same way the legacy timestamp gate did.
426 update_option($this->mutationWatermarkObservedByAdminActionOptionName(), $observed, false);
427 update_option($this->mutationWatermarkObservedByAdminActionAtOptionName(), time(), false);
428 $this->invalidateViewDoneServeableCache();
429 }
430
431 /**
432 * Read the current per-blog mutation watermark, returning 0 when the
433 * primitive is unavailable for any reason (class not autoloaded,
434 * degraded wpdb that lacks get_var / prepare, transient DB error).
435 * Same fallback contract as readMutationWatermarkForCacheKey in
436 * DataAccessTrait_ViewSnapshotCache: 0 means "treat as unversioned"
437 * and the caller falls through to its degraded path.
438 */
439 public function safeCurrentMutationWatermark(): int {
440 if (!class_exists('ABJ_404_Solution_MutationWatermark')) {
441 return 0;
442 }
443 try {
444 return ABJ_404_Solution_MutationWatermark::current();
445 // allow-silent-catch: degraded wpdb (test mocks lacking get_var, transient connection errors) collapses to fallback bump in markView; we never want the gate setter to throw and abort the admin response
446 } catch (\Throwable $e) {
447 return 0;
448 }
449 }
450
451 /**
452 * Clear the admin-mutation gate options after a build covers the
453 * recorded watermark. Called from {@see markViewDoneBuildCompleted()}
454 * on the S11-success and reconcile-promote paths. Cleans up the
455 * legacy timestamp option as well, so installs upgrading from a
456 * pre-Phase-4 build converge to "no admin gate".
457 */
458 public function clearAdminMutationGateOptions(): void {
459 if (!function_exists('delete_option')) {
460 return;
461 }
462 delete_option($this->mutationWatermarkObservedByAdminActionOptionName());
463 delete_option($this->mutationWatermarkObservedByAdminActionAtOptionName());
464 delete_option($this->viewDoneMutationInvalidatedAtOptionName());
465 }
466 }
467