PluginProbe
404 Solution / trunk
404 Solution vtrunk
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 / diagnostics / RequiredCheckpointEvidence.php

RequiredCheckpointEvidence.php in 404 Solution trunk, at includes/diagnostics/RequiredCheckpointEvidence.php

360 lines 14.5 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 * Selects checkpoint records that must survive bounded support ranking.
9 *
10 * Ordinary request-group ranking may omit an early census, a row-progress
11 * sample, or a completed browser report even though each is the only record
12 * carrying its discriminator fields. This policy reserves one complete record
13 * for every such evidence class before the remaining byte budget is ranked.
14 */
15 final class ABJ_404_Solution_RequiredCheckpointEvidence {
16
17 /**
18 * Latest complete record for every required evidence identity.
19 *
20 * @param array<int, string> $lines JSONL lines, oldest first.
21 * @return array<int, string>
22 */
23 public static function select(array $lines): array {
24 if (class_exists('ABJ_404_Solution_ActiveOperationBreadcrumbs')) {
25 $lines = ABJ_404_Solution_ActiveOperationBreadcrumbs::compactSupportLines($lines);
26 }
27 $operationState = array();
28 foreach (array_reverse($lines) as $line) {
29 $record = json_decode($line, true);
30 if (!is_array($record)) {
31 continue;
32 }
33 $operationKey = self::activeOperationKey($record);
34 if ($operationKey !== '' && !isset($operationState[$operationKey])) {
35 $operationState[$operationKey] = array('line' => $line, 'record' => $record);
36 }
37 }
38 $required = array_merge(
39 ABJ_404_Solution_MalformedCheckpointEvidence::select($lines),
40 ABJ_404_Solution_RequiredCheckpointIdentityEvidence::select($lines)
41 );
42 foreach ($operationState as $latest) {
43 $record = $latest['record'];
44 $line = $latest['line'];
45 if (self::isReservedActiveOperation($record)) {
46 $required[] = $line;
47 }
48 }
49 foreach (self::reservedDurableOperationLines($lines) as $line) {
50 $required[] = $line;
51 }
52 foreach (self::unmatchedOperationLines($lines) as $line) {
53 $required[] = $line;
54 }
55 foreach (ABJ_404_Solution_CheckpointIntentCorrelation::unmatchedIntentLines($lines)
56 as $line) {
57 $required[] = $line;
58 }
59 foreach (self::lastFullBoundaryLinesForIncompleteRequests($lines) as $line) {
60 $required[] = $line;
61 }
62 return array_values(array_unique($required));
63 }
64
65 /**
66 * The final full-envelope boundary reached by each unterminated request.
67 *
68 * High-frequency query and row records can follow the last lifecycle
69 * boundary before a worker stalls. Raw head/tail byte trimming therefore
70 * treats that boundary as middle content and can discard the exact
71 * pre-stall location when host-pressure counters widen the full envelope.
72 * Reserve one boundary per incomplete request so variable environment
73 * values cannot change whether the failure remains attributable.
74 *
75 * A request with only frequent records has no boundary to invent. A
76 * request with any terminal event is complete and stays in ordinary
77 * request-group ranking.
78 *
79 * @param array<int, string> $lines
80 * @return array<int, string>
81 */
82 private static function lastFullBoundaryLinesForIncompleteRequests(array $lines): array {
83 $requests = array();
84 foreach ($lines as $line) {
85 $record = json_decode($line, true);
86 if (!is_array($record) || !is_scalar($record['request_id'] ?? null)
87 || !is_scalar($record['event'] ?? null)) {
88 continue;
89 }
90 $requestId = (string)$record['request_id'];
91 $event = (string)$record['event'];
92 if ($requestId === '' || $event === '') {
93 continue;
94 }
95 if (!isset($requests[$requestId])) {
96 $requests[$requestId] = array(
97 'terminal' => false,
98 'boundary' => '',
99 'boundary_count' => 0,
100 );
101 }
102 if (in_array($event, ABJ_404_Solution_DiagnosticEvidencePriority::TERMINAL_EVENTS, true)) {
103 $requests[$requestId]['terminal'] = true;
104 }
105 if (($record['envelope'] ?? '')
106 === ABJ_404_Solution_CheckpointRecordFactory::ENVELOPE_FULL) {
107 $requests[$requestId]['boundary'] = $line;
108 $requests[$requestId]['boundary_count']++;
109 }
110 }
111
112 $selected = array();
113 foreach ($requests as $request) {
114 // A lone full record is already both the head and tail of its
115 // request group, so raw head/tail ranking cannot hide it. Requiring
116 // a preceding boundary also keeps a standalone partial evidence
117 // sample from being promoted merely because it lacks a terminal.
118 if ($request['terminal'] === false && $request['boundary_count'] > 1) {
119 $selected[] = $request['boundary'];
120 }
121 }
122 return $selected;
123 }
124
125 /**
126 * Select the latest unresolved fixed-sink state for each operation.
127 *
128 * @param array<int, string> $lines
129 * @return array<int, string>
130 */
131 private static function reservedDurableOperationLines(array $lines): array {
132 $latestByOperation = array();
133 foreach ($lines as $line) {
134 $record = json_decode($line, true);
135 $operationKey = is_array($record) ? self::durableOperationKey($record) : '';
136 if ($operationKey !== '') {
137 $latestByOperation[$operationKey] = array('line' => $line, 'record' => $record);
138 }
139 }
140 $selected = array();
141 foreach ($latestByOperation as $latest) {
142 $record = $latest['record'] ?? null;
143 $line = $latest['line'] ?? null;
144 if (is_array($record) && is_string($line)
145 && self::isReservedDurableOperation($record)) {
146 $selected[] = $line;
147 }
148 }
149 return $selected;
150 }
151
152 /**
153 * Starts whose matching completion never reached disk.
154 *
155 * The start/end pairs to reserve are DERIVED from the decisive-record
156 * manifest, not hardcoded here: every operation family the manifest marks
157 * `reserve` (row-render, rate-limit backend/cache, option persistence,
158 * option-hook callback) is preserved by the same request_id + operation_id
159 * matching. Enrolling a new decisive record in the manifest extends this
160 * reservation automatically, which is the structural fix for the recurring
161 * "emitted but un-reserved" gap. The query timeline keeps its own start /
162 * clear semantics (a summary with open_query === null cancels the reserve).
163 *
164 * @param array<int, string> $lines
165 * @return array<int, string>
166 */
167 private static function unmatchedOperationLines(array $lines): array {
168 return array_merge(
169 self::unmatchedReservedStartLines($lines),
170 self::unmatchedQueryLines($lines)
171 );
172 }
173
174 /**
175 * Reserved operation starts (from the manifest) whose matching end never
176 * reached disk. Each family's start/end pair is matched by request_id +
177 * operation_id; an end removes its start, so what remains is the hung set.
178 *
179 * @param array<int, string> $lines
180 * @return array<int, string>
181 */
182 private static function unmatchedReservedStartLines(array $lines): array {
183 $startEvents = array();
184 $endToStart = array();
185 foreach (ABJ_404_Solution_DecisiveRecordManifest::reservedOperationPairs() as $pair) {
186 $startEvents[$pair['start']] = true;
187 $endToStart[$pair['end']] = $pair['start'];
188 }
189
190 $openStarts = array();
191 foreach ($lines as $line) {
192 $record = json_decode($line, true);
193 if (!is_array($record)) {
194 continue;
195 }
196 $event = is_scalar($record['event'] ?? null) ? (string)$record['event'] : '';
197 $isStart = isset($startEvents[$event]);
198 if (!$isStart && !isset($endToStart[$event])) {
199 continue;
200 }
201 $key = self::operationKey($record, $isStart ? $event : $endToStart[$event]);
202 if ($key === '') {
203 continue;
204 }
205 if ($isStart) {
206 $openStarts[$key] = $line;
207 } else {
208 unset($openStarts[$key]);
209 }
210 }
211 return array_values($openStarts);
212 }
213
214 /**
215 * The reservation key for a start/end record: its start-event namespace
216 * plus request_id and operation_id. Empty when either identifier is absent,
217 * so an unattributable record is never reserved.
218 *
219 * @param array<mixed, mixed> $record
220 */
221 private static function operationKey(array $record, string $startEvent): string {
222 $requestId = is_scalar($record['request_id'] ?? null) ? (string)$record['request_id'] : '';
223 $operationId = is_scalar($record['operation_id'] ?? null) ? (string)$record['operation_id'] : '';
224 if ($requestId === '' || $operationId === '') {
225 return '';
226 }
227 return $startEvent . '|' . $requestId . '|' . $operationId;
228 }
229
230 /**
231 * The last query probe per request whose timeline never closed (no summary
232 * with open_query === null). A killed worker mid-query leaves exactly this.
233 *
234 * @param array<int, string> $lines
235 * @return array<int, string>
236 */
237 private static function unmatchedQueryLines(array $lines): array {
238 $lastQueries = array();
239 foreach ($lines as $line) {
240 $record = json_decode($line, true);
241 if (!is_array($record)) {
242 continue;
243 }
244 $requestId = is_scalar($record['request_id'] ?? null)
245 ? (string)$record['request_id'] : '';
246 $event = is_scalar($record['event'] ?? null) ? (string)$record['event'] : '';
247 if ($event === 'query_probe' && $requestId !== '') {
248 $lastQueries[$requestId] = $line;
249 } elseif ($event === 'query_timeline_summary' && $requestId !== ''
250 && ($record['open_query'] ?? null) === null) {
251 unset($lastQueries[$requestId]);
252 }
253 }
254 return array_values($lastQueries);
255 }
256
257 /** @param array<mixed, mixed> $record */
258 private static function activeOperationKey(array $record): string {
259 if (($record['event'] ?? '') !== 'active_operation_breadcrumb') {
260 return '';
261 }
262 $requestId = is_scalar($record['request_id'] ?? null)
263 ? (string)$record['request_id'] : '';
264 $boundary = is_scalar($record['boundary'] ?? null)
265 ? (string)$record['boundary'] : '';
266 $state = is_scalar($record['state'] ?? null) ? (string)$record['state'] : '';
267 $manifest = self::activeBoundaryManifest();
268 if ($requestId === ''
269 || !array_key_exists($boundary, $manifest)
270 || !in_array($state, array('active', 'complete'), true)) {
271 return '';
272 }
273 return $requestId . '|' . $boundary;
274 }
275
276 /** @param array<mixed, mixed> $record */
277 private static function durableOperationKey(array $record): string {
278 if (($record['event'] ?? '') !== 'durable_operation_state') {
279 return '';
280 }
281 $requestId = is_scalar($record['request_id'] ?? null)
282 ? (string)$record['request_id'] : '';
283 $checkpointId = is_scalar($record['operation_checkpoint_id'] ?? null)
284 ? (string)$record['operation_checkpoint_id'] : '';
285 return $requestId === '' || $checkpointId === ''
286 ? ''
287 : $requestId . '|' . $checkpointId;
288 }
289
290 /** @param array<mixed, mixed> $record */
291 private static function isReservedActiveOperation(array $record): bool {
292 if (($record['event'] ?? '') !== 'active_operation_breadcrumb'
293 || ($record['state'] ?? '') !== 'active') {
294 return false;
295 }
296 $boundary = is_scalar($record['boundary'] ?? null)
297 ? (string)$record['boundary'] : '';
298 $manifest = self::activeBoundaryManifest();
299 $requiredFields = $manifest[$boundary]['required_evidence_fields'] ?? array();
300 return $requiredFields !== array()
301 && self::hasNonEmptyScalarKeys($record, $requiredFields);
302 }
303
304 /** @param array<mixed, mixed> $record */
305 private static function isReservedDurableOperation(array $record): bool {
306 if (($record['event'] ?? '') !== 'durable_operation_state'
307 || !in_array($record['operation_state'] ?? '', array('intent', 'armed'), true)) {
308 return false;
309 }
310 $operationEvent = is_scalar($record['operation_event'] ?? null)
311 ? (string)$record['operation_event'] : '';
312 if ($operationEvent === 'cache_metrics_probe_start') {
313 return self::hasNonEmptyScalarKeys(
314 $record,
315 array('operation_id', 'source', 'phase', 'operation_checkpoint_id')
316 );
317 }
318 if ($operationEvent !== 'active_operation_breadcrumb'
319 || ($record['state'] ?? '') !== 'active') {
320 return false;
321 }
322 $boundary = is_scalar($record['boundary'] ?? null)
323 ? (string)$record['boundary'] : '';
324 $requiredFields = self::activeBoundaryManifest()[$boundary]['required_evidence_fields']
325 ?? array();
326 return $requiredFields !== array()
327 && self::hasNonEmptyScalarKeys($record, $requiredFields)
328 && self::hasNonEmptyScalarKeys($record, array('operation_checkpoint_id'));
329 }
330
331 /**
332 * Missing diagnostics files must degrade to no reserved active evidence
333 * instead of breaking the support-request path on a corrupt install.
334 *
335 * @return array<string, array{
336 * fields: array<int, string>,
337 * required_evidence_fields: array<int, string>
338 * }>
339 */
340 private static function activeBoundaryManifest(): array {
341 return class_exists('ABJ_404_Solution_ActiveOperationBoundaryManifest')
342 ? ABJ_404_Solution_ActiveOperationBoundaryManifest::boundaries()
343 : array();
344 }
345
346 /**
347 * @param array<mixed, mixed> $record
348 * @param array<int, string> $fields
349 */
350 private static function hasNonEmptyScalarKeys(array $record, array $fields): bool {
351 foreach ($fields as $field) {
352 $value = $record[$field] ?? null;
353 if (!is_scalar($value) || (string)$value === '') {
354 return false;
355 }
356 }
357 return true;
358 }
359 }
360