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

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

213 lines 7.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 * Request-scoped boundary instrumentation for foreground status-count work.
9 *
10 * Activated only by AJAX handlers that install the three status-count
11 * authority seams. It temporarily instruments WordPress hook callbacks and
12 * the live object cache, while StatusCountOperationJournal owns the durable
13 * operation protocol and privacy-safe record content.
14 */
15 final class ABJ_404_Solution_StatusCountsForegroundTracer {
16
17 /** @var self|null */
18 private static $active = null;
19
20 /** @var bool */
21 private $scopeActive = false;
22 /** @var bool */
23 private $suspended = false;
24 /** @var object|null */
25 private $originalCache;
26 /** @var ABJ_404_Solution_InstrumentedObjectCache|null */
27 private $cacheProxy;
28 /** @var ABJ_404_Solution_HookInstrumentationLifecycleTracer */
29 private $lifecycleTracer;
30 /** @var ABJ_404_Solution_StatusCountOperationJournal */
31 private $operationJournal;
32 /**
33 * @var ABJ_404_Solution_HookCallbackInstrumenter<array{
34 * mode:string,
35 * identity:array<string,mixed>,
36 * started_at?:float
37 * }|null>
38 */
39 private $hookInstrumenter;
40
41 /**
42 * @template T
43 * @param array<string,mixed> $fields
44 * @param callable():T $work
45 * @return T
46 */
47 public static function trace(string $operation, array $fields, callable $work) {
48 if (self::$active !== null) {
49 return self::$active->operationJournal->trace($operation, $fields, $work);
50 }
51
52 $requestId = self::requestId();
53 if ($requestId === '') {
54 return $work();
55 }
56 $tracer = new self($requestId);
57 self::$active = $tracer;
58 $tracer->install();
59 try {
60 $result = $tracer->operationJournal->trace($operation, $fields, $work);
61 } catch (Throwable $error) {
62 $tracer->suspended = true;
63 $tracer->operationJournal->suspend();
64 $tracer->restore(false);
65 self::$active = null;
66 throw $error;
67 }
68 $tracer->restore(true);
69 self::$active = null;
70 return $result;
71 }
72
73 private function __construct(string $requestId) {
74 $this->operationJournal = new ABJ_404_Solution_StatusCountOperationJournal(
75 $requestId
76 );
77 $this->lifecycleTracer = new ABJ_404_Solution_HookInstrumentationLifecycleTracer(
78 $requestId,
79 'status_count'
80 );
81 $this->hookInstrumenter = new ABJ_404_Solution_HookCallbackInstrumenter(
82 function (
83 string $registeredHook,
84 string $actualHook,
85 int $priority,
86 array $identity
87 ) {
88 return $this->operationJournal->beginHookCallback(
89 $actualHook,
90 $priority,
91 $identity
92 );
93 },
94 function ($token): void {
95 $this->operationJournal->finishHookCallback($token);
96 },
97 $this->lifecycleTracer
98 );
99 }
100
101 private function install(): void {
102 $hookBoundary = 'unavailable';
103 $allCounts = array(
104 'callbacks_wrapped' => 0,
105 'callbacks_marked' => 0,
106 'callbacks_unavailable' => 0,
107 );
108 if (function_exists('add_filter')) {
109 try {
110 $allCounts = $this->hookInstrumenter->instrument('all');
111 if ($allCounts['registry_status'] !== 'unavailable') {
112 $this->lifecycleTracer->traceBoundary(
113 ABJ_404_Solution_HookInstrumentationLifecycleTracer::PHASE_REGISTRATION,
114 'all',
115 function (): void {
116 add_filter('all', array($this, 'prepareHookCallbacks'), PHP_INT_MIN, 1);
117 }
118 );
119 $hookBoundary = 'ready';
120 }
121 } catch (Throwable $error) {
122 self::reportFailure('hook boundary install failed: ' . $error->getMessage());
123 }
124 }
125
126 $cacheBoundary = 'unavailable';
127 $cache = $GLOBALS['wp_object_cache'] ?? null;
128 if (is_object($cache) && !$cache instanceof ABJ_404_Solution_InstrumentedObjectCache) {
129 $this->originalCache = $cache;
130 $this->cacheProxy = new ABJ_404_Solution_InstrumentedObjectCache(
131 $cache,
132 $this->operationJournal
133 );
134 $GLOBALS['wp_object_cache'] = $this->cacheProxy;
135 $cacheBoundary = 'ready';
136 }
137 $this->scopeActive = true;
138 $this->operationJournal->activate();
139 $this->operationJournal->recordInstrumentation(array(
140 'hook_boundary' => $hookBoundary,
141 'cache_boundary' => $cacheBoundary,
142 'all_callbacks_wrapped' => $allCounts['callbacks_wrapped'],
143 'all_callbacks_marked' => $allCounts['callbacks_marked'],
144 'all_callbacks_attributed' => $allCounts['callbacks_wrapped']
145 + $allCounts['callbacks_marked'],
146 'all_callbacks_unavailable' => $allCounts['callbacks_unavailable'],
147 ));
148 }
149
150 /**
151 * Instrument callbacks immediately before WordPress dispatches a named hook.
152 *
153 * @param mixed $hookName
154 * @return mixed
155 */
156 public function prepareHookCallbacks($hookName) {
157 if (!$this->scopeActive || $this->suspended
158 || $this->operationJournal->isRecording()
159 || $this->lifecycleTracer->isRecording()
160 || (class_exists('ABJ_404_Solution_AjaxCheckpointLogger')
161 && ABJ_404_Solution_AjaxCheckpointLogger::isRecording())
162 || !is_string($hookName) || $hookName === 'all') {
163 return $hookName;
164 }
165 $this->hookInstrumenter->instrument($hookName);
166 return $hookName;
167 }
168
169 private function restore(bool $scopeCompleted): void {
170 $this->scopeActive = false;
171 $this->operationJournal->deactivate();
172 if (function_exists('remove_filter')) {
173 try {
174 $this->lifecycleTracer->traceBoundary(
175 ABJ_404_Solution_HookInstrumentationLifecycleTracer::PHASE_REMOVAL,
176 'all',
177 function (): void {
178 remove_filter('all', array($this, 'prepareHookCallbacks'), PHP_INT_MIN);
179 }
180 );
181 } catch (Throwable $error) {
182 self::reportFailure('hook boundary removal failed: ' . $error->getMessage());
183 }
184 }
185 $this->hookInstrumenter->restore($scopeCompleted);
186 if ($this->cacheProxy !== null
187 && ($GLOBALS['wp_object_cache'] ?? null) === $this->cacheProxy) {
188 $GLOBALS['wp_object_cache'] = $this->originalCache;
189 }
190 }
191
192 private static function requestId(): string {
193 if (!class_exists('ABJ_404_Solution_AjaxDiagnosticRequestPolicy')) {
194 return '';
195 }
196 $context = $GLOBALS['abj404_ajax_context'] ?? null;
197 $action = is_array($context) && is_scalar($context['action'] ?? null)
198 ? (string)$context['action'] : '';
199 if (!in_array($action, array(
200 ABJ_404_Solution_AjaxDiagnosticRequestPolicy::INSTRUMENTED_ACTION,
201 'ajaxRunCanaryStep',
202 'ajaxRefreshHealthBar',
203 ), true)) {
204 return '';
205 }
206 return ABJ_404_Solution_AjaxRequestLedger::requestIdFromGlobalContext();
207 }
208
209 private static function reportFailure(string $message): void {
210 abj404_logPhpFallback('status-count-foreground', $message);
211 }
212 }
213