PluginProbe
404 Solution / 4.1.19
404 Solution v4.1.19
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 / DataAccessTrait_ViewBuildStartedWatermark.php

DataAccessTrait_ViewBuildStartedWatermark.php in 404 Solution 4.1.19, at includes/DataAccessTrait_ViewBuildStartedWatermark.php

257 lines 11.6 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 * Per-build watermark stamp machinery for the staged view-build pipeline.
9 *
10 * Three persistent stamps, three lifetimes (Phase 3a step 3 split of the
11 * legacy single `started_watermark` -- Codex #7):
12 *
13 * - active_build_started_watermark: written at S1 entry on a fresh build,
14 * cleared on S11 success and on every abort (mutation-watermark gate,
15 * prefix-changed gate, force-restart primitive). Read at every stage
16 * boundary by {@see mutationWatermarkAdvancedSinceBuildStart}: a strict
17 * "current() > stamp" comparison drives the abort decision. While this
18 * stamp exists, a build is in flight against the watermark it holds.
19 * - last_build_started_watermark: diagnostic sibling. Written at S1 entry
20 * alongside the active stamp, RETAINED across every settle path. Tells
21 * an operator "what was the most recent stamp we wrote, regardless of
22 * whether that build completed?" -- the question the original single
23 * `started_watermark` could not answer cleanly (a stamp surviving a
24 * completed build was the footgun Codex #7 flagged).
25 * - built_watermark: the cross-build pre-image. Written ONLY at S11
26 * success and ONLY from the active stamp's value, so it records
27 * "what mutation watermark did the LAST SUCCESSFUL view_done snapshot
28 * cover?" Read by future freshness checks (Phase 4) to decide whether
29 * to serve view_done or trigger a rebuild. Survives every fresh-start
30 * and abort cleanup.
31 *
32 * All three stamps live OUTSIDE `$viewBuildProgressOptionNames` because
33 * reads/writes route through {@see readWatermarkOption} / {@see writeWatermarkOption}
34 * to preserve the absent-vs-0 distinction: a fresh install whose mutation
35 * watermark is 0 produces a legitimate stamp of 0, which `readProgressOption`
36 * (clamping to >= 0) cannot distinguish from "never stamped".
37 *
38 * Migration. A pre-Phase-3a-step-3 install upgrading mid-flight may carry
39 * the legacy `started_watermark` option populated by the prior code path.
40 * {@see readActiveBuildStartedWatermark} falls back to the legacy key when
41 * the active key is absent, promotes the value into the active key, and
42 * deletes the legacy key so convergence happens after one tick. The stamp /
43 * clear methods also delete the legacy key whenever they run, so the next
44 * S1 entry, abort, or S11 success on a post-upgrade install converges to
45 * the new naming. Phase 5 cleanup deletes the fallback + the legacy helper
46 * once the rename has been in production for one release cycle.
47 *
48 * Composition. Mixed into `ABJ_404_Solution_DataAccess` alongside
49 * `ABJ_404_Solution_DataAccess_ViewBuildHelpersTrait` (the orchestrator's
50 * abort/fresh-start/gate methods call into this trait via `$this->`).
51 *
52 * @see ABJ_404_Solution_DataAccess_ViewBuildHelpersTrait
53 * @see ABJ_404_Solution_DataAccess_ViewBuildForceRestartTrait
54 * @see ABJ_404_Solution_DataAccess_ViewQueriesStagedTrait
55 */
56 trait ABJ_404_Solution_DataAccess_ViewBuildStartedWatermarkTrait {
57
58 /**
59 * Unprefixed option-name suffix for the S11-completion watermark stamp.
60 * Lives on its own (NOT in `$viewBuildProgressOptionNames`) so it
61 * survives across builds: this is the published "what watermark did the
62 * LAST SUCCESSFUL view_done snapshot cover?" pre-image.
63 * clearAllProgressOptions (called on abort and on fresh-start) must NOT
64 * touch it.
65 */
66 private function builtWatermarkOptionName(): string {
67 return $this->getLowercasePrefix() . 'abj404_view_build_built_watermark';
68 }
69
70 /**
71 * Option name for the in-flight build's S1-entry stamp. Cleared on S11
72 * success, on at-stage abort, and by `forceRestartViewBuild()`.
73 */
74 private function activeBuildStartedWatermarkOptionName(): string {
75 return $this->getLowercasePrefix() . 'abj404_view_build_active_started_watermark';
76 }
77
78 /**
79 * Option name for the diagnostic-only "most recent stamp" sibling.
80 * Written at S1 entry alongside the active stamp, RETAINED across S11
81 * success, abort, and force-restart so operators can see the value
82 * regardless of completion.
83 */
84 private function lastBuildStartedWatermarkOptionName(): string {
85 return $this->getLowercasePrefix() . 'abj404_view_build_last_started_watermark';
86 }
87
88 /**
89 * Pre-Phase-3a-step-3 option name. Read via the migration fallback in
90 * {@see readActiveBuildStartedWatermark} and deleted by every stamp /
91 * clear call so post-upgrade installs converge to the new naming after
92 * one build cycle. Phase 5 cleanup deletes this helper.
93 */
94 private function legacyStartedWatermarkOptionName(): string {
95 return $this->getLowercasePrefix() . 'abj404_view_build_started_watermark';
96 }
97
98 /**
99 * Raw watermark-option reader. Returns -1 when the option is absent,
100 * the stored integer otherwise. The absent-vs-0 distinction matters
101 * for the resume-preserves-stamp contract: a fresh install with no
102 * mutations has current()=0, so a stamp of 0 is a legitimate stamped
103 * value, NOT the "no stamp yet" sentinel.
104 *
105 * @param string $fullyPrefixedName Option name as built by
106 * activeBuildStartedWatermarkOptionName() / builtWatermarkOptionName().
107 * @return int -1 when absent; the stored integer otherwise.
108 */
109 private function readWatermarkOption(string $fullyPrefixedName): int {
110 if (!function_exists('get_option')) {
111 return -1;
112 }
113 $value = get_option($fullyPrefixedName, null);
114 if ($value === null || $value === false) {
115 return -1;
116 }
117 if (!is_scalar($value)) {
118 return -1;
119 }
120 return intval($value);
121 }
122
123 /**
124 * Raw watermark-option writer. Autoload=false so the per-build stamps
125 * do not bloat the alloptions cache loaded on every WP page.
126 */
127 private function writeWatermarkOption(string $fullyPrefixedName, int $value): void {
128 if (!function_exists('update_option')) {
129 return;
130 }
131 update_option($fullyPrefixedName, max(0, $value), false);
132 }
133
134 /**
135 * True when the live mutation watermark is strictly greater than the
136 * pre-image stamped at S1 entry of the in-flight build. Returns false
137 * when no stamp exists (no in-flight build) or when current() <= stamp.
138 *
139 * Called at every stage boundary by the orchestrator. A true result
140 * means an external caller bumped the watermark while this build was
141 * running, so the buffer it has assembled does not cover that mutation.
142 * The orchestrator must abort and let the next tick rebuild.
143 */
144 private function mutationWatermarkAdvancedSinceBuildStart(): bool {
145 $started = $this->readActiveBuildStartedWatermark();
146 if ($started < 0) {
147 return false;
148 }
149 if (!class_exists('ABJ_404_Solution_MutationWatermark')) {
150 return false;
151 }
152 $current = ABJ_404_Solution_MutationWatermark::current();
153 return $current > $started;
154 }
155
156 /**
157 * Read the active stamp, falling back to the pre-rename legacy option
158 * name when the new key is absent. The fallback covers the upgrade
159 * window: an install whose previous PHP request stamped
160 * `started_watermark` mid-flight and whose current request is the
161 * first to run with the renamed code must still see the stamp at the
162 * gate-check site, otherwise the stage-boundary abort would miss a
163 * real mutation that landed before the upgrade. On a hit, we promote
164 * the legacy value into the active key (so the next read is direct)
165 * and delete the legacy key so convergence happens after one tick.
166 *
167 * @return int -1 when neither key is set; the stamp value otherwise.
168 */
169 private function readActiveBuildStartedWatermark(): int {
170 $value = $this->readWatermarkOption($this->activeBuildStartedWatermarkOptionName());
171 if ($value >= 0) {
172 return $value;
173 }
174 $legacy = $this->readWatermarkOption($this->legacyStartedWatermarkOptionName());
175 if ($legacy < 0) {
176 return -1;
177 }
178 $this->writeWatermarkOption($this->activeBuildStartedWatermarkOptionName(), $legacy);
179 if (function_exists('delete_option')) {
180 delete_option($this->legacyStartedWatermarkOptionName());
181 }
182 return $legacy;
183 }
184
185 /**
186 * Stamp BOTH started-watermark fields at S1 entry of a fresh build.
187 * Callers route through this from the orchestrator's `if ($stage < 1)`
188 * block, which only fires on fresh starts -- a resuming tick has
189 * `current_stage >= 1` and never re-enters this branch, so the stamps
190 * written here remain the pre-image for every subsequent stage
191 * boundary in the build.
192 *
193 * Always overwrites both. The S11-completion path and the abort
194 * cleanup path clear `active_*` only; the diagnostic `last_*` is
195 * retained for operator visibility and overwritten on the next S1
196 * entry. Also deletes the legacy option name so a post-upgrade
197 * install converges to the new naming.
198 */
199 private function stampStartedWatermarksAtS1Entry(): void {
200 if (!class_exists('ABJ_404_Solution_MutationWatermark')) {
201 return;
202 }
203 $watermark = ABJ_404_Solution_MutationWatermark::current();
204 $this->writeWatermarkOption($this->activeBuildStartedWatermarkOptionName(), $watermark);
205 $this->writeWatermarkOption($this->lastBuildStartedWatermarkOptionName(), $watermark);
206 if (function_exists('delete_option')) {
207 delete_option($this->legacyStartedWatermarkOptionName());
208 }
209 }
210
211 /**
212 * Delete the active stamp. Called from the abort path so the next
213 * tick's fresh-start branch sees no leftover stamp from the aborted
214 * run, and from the S11 success path so the just-completed build's
215 * pre-image does not become the next build's pre-image. (S11 also
216 * publishes built_watermark, which is the cross-build pre-image that
217 * survives the clear.)
218 *
219 * Does NOT clear `last_build_started_watermark`: that stamp's whole
220 * purpose is to survive both abort and success so operators can
221 * observe "what watermark did the most recent build attempt stamp?"
222 * regardless of completion.
223 *
224 * Also deletes the legacy option name so post-upgrade installs
225 * converge to the new naming after the first build settles.
226 */
227 private function clearActiveBuildStartedWatermark(): void {
228 if (!function_exists('delete_option')) {
229 return;
230 }
231 delete_option($this->activeBuildStartedWatermarkOptionName());
232 delete_option($this->legacyStartedWatermarkOptionName());
233 }
234
235 /**
236 * Stamp built_watermark = active stamp at S11 success so future
237 * freshness checks can compare it against the live mutation watermark
238 * to decide if view_done covers the latest data.
239 *
240 * Must be called BEFORE clearActiveBuildStartedWatermark() since the
241 * read source disappears once the active stamp is cleared.
242 * built_watermark itself lives outside the progress registry, so it
243 * survives the post-S11 clears.
244 */
245 private function publishBuiltWatermarkFromActiveBuildStartedWatermark(): void {
246 $started = $this->readActiveBuildStartedWatermark();
247 if ($started < 0) {
248 // No active stamp (and no legacy fallback): pre-Phase-2 install
249 // upgrading mid-build, or a code path that reached S11 without
250 // entering S1. Either way, do not overwrite a prior good
251 // built_watermark with a bogus zero.
252 return;
253 }
254 $this->writeWatermarkOption($this->builtWatermarkOptionName(), $started);
255 }
256 }
257