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

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

97 lines 4.1 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 persisted attempt counter for one detach A/B workload scope.
9 *
10 * Split out of ABJ_404_Solution_DetachAbExperiment, which decides WHICH mode an
11 * attempt slot receives and had no business also owning where the slot number
12 * is stored. The experiment is policy; this is the data access behind it.
13 *
14 * The store is a transient and the read-modify-write is deliberately not
15 * atomic. This is a bounded diagnostic sequence, not a security limit: two
16 * tabs racing can land on the same slot, which costs one unusable pair in the
17 * evidence, and that is cheaper than coupling diagnostics policy to the
18 * database layer. The race is recorded here as a known property rather than
19 * left for a reader to infer from the absence of a lock.
20 */
21 final class ABJ_404_Solution_DetachAbAttemptCounter {
22
23 /** No slot could be reserved. Callers answer inert rather than guessing one. */
24 const NO_SLOT = -1;
25
26 /** How long an idle scope keeps its place in the sequence. */
27 const DEFAULT_TTL_SECONDS = 3600;
28
29 /**
30 * Reserve and return this scope's next attempt slot.
31 *
32 * Returns NO_SLOT when the scope cannot carry a counter, when the transient
33 * API is unavailable, or when the reservation could not be persisted.
34 *
35 * That last case used to return the slot anyway. A failed write leaves the
36 * stored counter where it was, so every later request in the session read
37 * the same value and was handed attempt zero again: the sequence could
38 * never reach MAX_ATTEMPTS, never revert to `default`, and the experiment's
39 * promise that a probe never permanently degrades an admin session was
40 * broken by precisely the infrastructure failure the plugin is built to
41 * degrade past. An unreserved slot is not a slot.
42 */
43 public static function reserveNextSlot(ABJ_404_Solution_DetachAbScope $scope): int {
44 if ($scope->isSessionless()
45 || !function_exists('get_transient')
46 || !function_exists('set_transient')) {
47 return self::NO_SLOT;
48 }
49
50 $key = $scope->transientKey();
51 $stored = ABJ_404_Solution_DetachAbResolutionTracer::traceTransientOperation(
52 'get_transient',
53 $key,
54 static function () use ($key) {
55 return get_transient($key);
56 }
57 );
58 $slot = self::storedSlot($stored);
59
60 // allow-cache-empty: locally computed attempt counter (always a valid
61 // non-negative int), not a fetched query result.
62 $persisted = ABJ_404_Solution_DetachAbResolutionTracer::traceTransientOperation(
63 'set_transient',
64 $key,
65 static function () use ($key, $slot) {
66 // allow-cache-empty: the locally computed attempt counter is a
67 // valid non-negative integer, never a fetched result.
68 return set_transient($key, $slot + 1, self::DEFAULT_TTL_SECONDS);
69 }
70 );
71
72 return $persisted ? $slot : self::NO_SLOT;
73 }
74
75 /**
76 * Interpret a stored counter, accepting only a canonical non-negative integer.
77 *
78 * is_numeric() plus an int cast accepted '2.9' and '1e3' and turned them
79 * into 2 and 1000, so a corrupted or foreign value under this key was
80 * promoted into a real-looking slot and journalled as A/B coordinates --
81 * fabricated evidence, which is worse for a diagnostic than no evidence.
82 * Anything that is not a counter starts the sequence over; the write that
83 * follows immediately replaces it with a canonical value, so a garbled
84 * store self-heals on the next request instead of resetting forever.
85 *
86 * ABJ_404_Solution_ExactInteger is that rule, already generalised: its own
87 * docblock names this experiment's attempt ordinal as one of the two places
88 * that discovered the defect independently. This counter is the third, and
89 * hand-rolling a fourth narrow pattern here is how there would be a fifth.
90 *
91 * @param mixed $stored
92 */
93 private static function storedSlot($stored): int {
94 return ABJ_404_Solution_ExactInteger::readOr($stored, 0, 0);
95 }
96 }
97