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

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

168 lines 7.8 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 * The concurrent-control browser receipt: how it is journaled, and what makes
9 * one complete enough to be believed.
10 *
11 * The concurrent control is a canary step launched BESIDE the first real table
12 * attempt, under the same host conditions, so a table failure can be separated
13 * from a host that was struggling for everyone at that instant. That only works
14 * if the browser's account of the control comes back, and the browser can only
15 * send it on a LATER request -- so the record arrives carried by one request,
16 * describes a second, and is filed under a third.
17 *
18 * Those three joins, plus the browser's own observation, are what
19 * `isCompleteJournalRecord()` checks. It is a contract with two consumers
20 * outside this file (ABJ_404_Solution_DiagnosticCollectionManifest decides from
21 * it whether required evidence is available;
22 * ABJ_404_Solution_CanaryReceiptEvidence decides from it whether a ladder run
23 * can be interpreted), which is why the record owns its own class rather than
24 * living inside ABJ_404_Solution_ClientTransportReport -- that class reads and
25 * bounds whatever the browser sent, and this one knows what this particular
26 * record means.
27 */
28 final class ABJ_404_Solution_ConcurrentControlReceipt {
29
30 /** The browser's own name for this report kind, on the wire. */
31 const KIND = 'concurrent_control_browser_receipt';
32
33 /** The event name it is journaled under. */
34 const JOURNAL_EVENT = 'concurrent_control_client_receipt';
35
36 /**
37 * The overlap-state vocabulary, as the browser spells it.
38 *
39 * A wire vocabulary shared with view_updater_concurrent_control_evidence.js,
40 * so it gets one definition here and ConcurrentControlReceiptTest pins these
41 * values against that file. Read as bare strings in two separate PHP files,
42 * the set was drift-prone in the worst direction: a renamed state does not
43 * error, it silently makes every receipt incomplete, and the support payload
44 * then reports "no concurrent control" for a run that had one.
45 */
46 const OVERLAP_COMPUTED = 'computed';
47 const OVERLAP_UNAVAILABLE = 'unavailable';
48 const OVERLAP_PENDING = 'pending';
49
50 /**
51 * Every state the browser may report. PENDING is deliberately absent from
52 * the accepted set in hasBrowserEvidence(): the table had not settled when
53 * the receipt was built, so there is no overlap to reason about yet.
54 *
55 * @return array<int, string>
56 */
57 public static function everyOverlapState(): array {
58 return array(self::OVERLAP_COMPUTED, self::OVERLAP_UNAVAILABLE, self::OVERLAP_PENDING);
59 }
60
61 /**
62 * File the receipt beside that control's own server trace while retaining
63 * which later request delivered it.
64 *
65 * The browser session is stamped on the record for the same reason the
66 * canary step receipts carry it (see
67 * Ajax_CanaryLadder::journalPriorStepReceipts): the reconstruction that
68 * consumes this record must not have to resolve the session through a
69 * second, separately-armed journal that can be empty. Hashed into the
70 * `session_key` this journal already uses for `detach_ab_mode`, because
71 * equality is the whole requirement.
72 *
73 * @param array{carrierRequestId: string, sessionId: string, report: array<string, mixed>} $receipt
74 * One keyed bag rather than two adjacent strings. Both are opaque
75 * identifiers of the same type, and swapping them type-checks: the record
76 * would then be filed under the session, `carried_by` would name the
77 * session, and `session_key` would be a perfectly valid hash of a request
78 * ID. Every join in the record points somewhere plausible and wrong, and
79 * because the joins are what the reconstruction reads, nothing downstream
80 * can tell. `sessionId` is already bounded by
81 * AjaxRequestLedger::readFields().
82 */
83 public static function journal(array $receipt): void {
84 $carrierRequestId = $receipt['carrierRequestId'];
85 $report = $receipt['report'];
86 $controlRequestId = self::ledgerIdOrEmpty($report['controlRequestId'] ?? '');
87 $controlForRequestId = self::ledgerIdOrEmpty($report['controlForRequestId'] ?? '');
88 ABJ_404_Solution_AjaxCheckpointLogger::record(
89 $controlRequestId !== '' ? $controlRequestId : $carrierRequestId,
90 self::JOURNAL_EVENT,
91 array(
92 'carried_by' => $carrierRequestId,
93 'control_for_request_id' => $controlForRequestId,
94 'control_request_id' => $controlRequestId,
95 'session_key' => ABJ_404_Solution_DetachAbExperiment::sessionKey($receipt['sessionId']),
96 'report' => $report,
97 )
98 );
99 }
100
101 /**
102 * Is this raw browser report a concurrent-control receipt at all?
103 *
104 * @param array<mixed, mixed> $report
105 */
106 public static function isBrowserReceipt(array $report): bool {
107 return ($report['kind'] ?? '') === self::KIND;
108 }
109
110 /**
111 * Whether a journal record is complete enough to serve as the required
112 * concurrent-control evidence in a support payload.
113 *
114 * Both halves must hold: the JOURNAL joins (this record can be tied to the
115 * control request, its carrier, and the request it was a control FOR) and
116 * the BROWSER evidence (the browser actually observed an outcome and an
117 * overlap). A record with joins and no observation describes a probe that
118 * ran and told us nothing, and counting it as evidence would let the
119 * support payload claim a control it does not have.
120 *
121 * @param array<mixed, mixed> $record
122 */
123 public static function isCompleteJournalRecord(array $record): bool {
124 $report = is_array($record['report'] ?? null) ? $record['report'] : array();
125 return self::hasJournalJoins($record) && self::hasBrowserEvidence($report);
126 }
127
128 /** @param array<mixed, mixed> $record */
129 private static function hasJournalJoins(array $record): bool {
130 return ($record['envelope'] ?? '') === ABJ_404_Solution_CheckpointRecordFactory::ENVELOPE_FULL
131 && ($record['event'] ?? '') === self::JOURNAL_EVENT
132 && is_string($record['carried_by'] ?? null)
133 && $record['carried_by'] !== ''
134 && is_string($record['control_for_request_id'] ?? null)
135 && $record['control_for_request_id'] !== ''
136 && is_string($record['control_request_id'] ?? null)
137 && $record['control_request_id'] !== ''
138 && ($record['request_id'] ?? '') === $record['control_request_id'];
139 }
140
141 /** @param array<mixed, mixed> $report */
142 private static function hasBrowserEvidence(array $report): bool {
143 $receipt = is_array($report['receipt'] ?? null) ? $report['receipt'] : array();
144 $overlap = is_array($report['overlap'] ?? null) ? $report['overlap'] : array();
145 $overlapState = $overlap['state'] ?? '';
146 $validOverlap = $overlapState === self::OVERLAP_UNAVAILABLE
147 || ($overlapState === self::OVERLAP_COMPUTED
148 && is_numeric($overlap['durationMs'] ?? null)
149 && (int)$overlap['durationMs'] >= 0);
150 return self::isBrowserReceipt($report)
151 && $validOverlap
152 && is_string($receipt['resourceTimingState'] ?? null)
153 && $receipt['resourceTimingState'] !== '';
154 }
155
156 /**
157 * A client-supplied request id, or '' when it is not a ledger id. The
158 * journal is joined on these, so a value that cannot be one is dropped
159 * rather than reflected into a record other readers will try to match.
160 *
161 * @param mixed $value
162 */
163 private static function ledgerIdOrEmpty($value): string {
164 $id = is_scalar($value) ? (string)$value : '';
165 return preg_match('/^[a-zA-Z0-9]{8,64}$/', $id) === 1 ? $id : '';
166 }
167 }
168