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 / view-build / ViewReadService.php

ViewReadService.php in 404 Solution trunk, at includes/view-build/ViewReadService.php

497 lines 20.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 require_once __DIR__ . '/AdminViewReadCoordinator.php';
8
9 /**
10 * Compatibility facade for admin view-read collaborators.
11 *
12 * Preserves the public view-read interface while delegating each operation to
13 * focused collaborators:
14 *
15 * - ABJ_404_Solution_AdminViewReadCoordinator -- row/count reads + snapshots
16 * - ABJ_404_Solution_StatusCountsRepository -- cached aggregate status tallies
17 * - ABJ_404_Solution_RedirectHitCountHistogramRepository -- telemetry buckets
18 * - ABJ_404_Solution_RedirectRowCountRepository -- uncached row totals
19 * - ABJ_404_Solution_RedirectsBulkReader -- non-paginated redirect reads
20 * - ABJ_404_Solution_LogsMetricsReader -- logs row count + disk usage
21 * - ABJ_404_Solution_DatabaseMetadataReader -- engine + post-type metadata
22 * - ABJ_404_Solution_ViewQueryBuilder -- single-table SQL construction
23 * - ABJ_404_Solution_ViewCacheInvalidator -- invalidation primitives
24 * - ABJ_404_Solution_ViewDiagnostics -- failure diagnostics
25 *
26 * @see docs/dataaccess-refactor-plan.md Phase 6.
27 */
28 class ABJ_404_Solution_ViewReadService implements ABJ_404_Solution_ViewReadServiceInterface {
29
30 const CACHE_KEY_REDIRECT_STATUS = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_REDIRECT_STATUS;
31 const CACHE_KEY_CAPTURED_STATUS = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_CAPTURED_STATUS;
32 const CACHE_KEY_HIGH_IMPACT_CAPTURED = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_HIGH_IMPACT_CAPTURED;
33 const STATUS_CACHE_TTL = ABJ_404_Solution_ViewReadRuntimeState::STATUS_CACHE_TTL;
34 const LOGS_COUNT_CACHE_TTL_SECONDS = ABJ_404_Solution_LogsMetricsReader::LOGS_COUNT_CACHE_TTL_SECONDS;
35
36 /** @var bool Per-request "bulk mutation in progress" flag. */
37 public static $bulkMutationInProgress = false;
38
39 /** @var ABJ_404_Solution_DatabaseCore */
40 private $dbCore;
41
42 // --- Collaborators ---
43
44 /** @var ABJ_404_Solution_ViewQueryBuilder */
45 private $queryBuilder;
46
47 /** @var ABJ_404_Solution_ViewDiagnostics */
48 private $diagnostics;
49
50 /** @var ABJ_404_Solution_ViewCacheInvalidator */
51 private $cacheInvalidator;
52
53 /** @var ABJ_404_Solution_RedirectHitCountHistogramRepository */
54 private $redirectHitCountHistogram;
55
56 /** @var ABJ_404_Solution_RedirectRowCountRepository */
57 private $redirectRowCounts;
58
59 /** @var ABJ_404_Solution_StatusCountsRefreshCoordinator */
60 private $statusCountsRefreshCoordinator;
61
62 /** @var ABJ_404_Solution_RedirectsBulkReader */
63 private $redirectsBulkReader;
64
65 /** @var ABJ_404_Solution_LogsMetricsReader */
66 private $logsMetricsReader;
67
68 /** @var ABJ_404_Solution_DatabaseMetadataReader */
69 private $dbMetadataReader;
70
71 /** @var ABJ_404_Solution_HitsTableRebuildPolicy */
72 private $hitsTableRebuildPolicy;
73
74 /** @var ABJ_404_Solution_AdminViewReadCoordinator */
75 private $adminViewReadCoordinator;
76
77 /** @var ABJ_404_Solution_RedirectsViewLiveResolver */
78 private $liveResolver;
79
80 /**
81 * @param ABJ_404_Solution_DatabaseCore $dbCore
82 * @param ABJ_404_Solution_LogsRepository $logsRepo
83 * @param ABJ_404_Solution_RedirectsRepository $redirectsRepo
84 * @param ABJ_404_Solution_Functions|null $f Falls back to abj_service('functions')
85 * @param ABJ_404_Solution_Logging|null $logger Falls back to abj_service('logging')
86 */
87 public function __construct(
88 ABJ_404_Solution_DatabaseCore $dbCore,
89 ABJ_404_Solution_LogsRepository $logsRepo,
90 ABJ_404_Solution_RedirectsRepository $redirectsRepo,
91 $f = null,
92 $logger = null
93 ) {
94 $this->dbCore = $dbCore;
95 $functions = $f !== null ? $f : abj_service('functions');
96 $resolvedLogger = $logger !== null ? $logger : abj_service('logging');
97
98 $this->diagnostics = new ABJ_404_Solution_ViewDiagnostics($dbCore);
99 $this->cacheInvalidator = new ABJ_404_Solution_ViewCacheInvalidator(
100 $dbCore, $redirectsRepo, $this->viewDoneFreshnessOptionName()
101 );
102 $this->queryBuilder = new ABJ_404_Solution_ViewQueryBuilder($dbCore);
103 $this->liveResolver = new ABJ_404_Solution_RedirectsViewLiveResolver($dbCore, $functions);
104
105 $readiness = new ABJ_404_Solution_TableReadinessGate(
106 $dbCore,
107 $dbCore->tableNameResolver()
108 );
109 $statusCounts = new ABJ_404_Solution_StatusCountsRepository(
110 $dbCore,
111 $logsRepo,
112 $this->queryBuilder,
113 $readiness
114 );
115 $this->redirectHitCountHistogram = new ABJ_404_Solution_RedirectHitCountHistogramRepository(
116 $dbCore,
117 $readiness
118 );
119 $this->redirectRowCounts = new ABJ_404_Solution_RedirectRowCountRepository(
120 $dbCore,
121 $readiness
122 );
123 $this->statusCountsRefreshCoordinator = new ABJ_404_Solution_StatusCountsRefreshCoordinator(
124 $statusCounts,
125 new ABJ_404_Solution_StatsRefreshLock($dbCore),
126 static function(string $message) use ($resolvedLogger): void {
127 $resolvedLogger->warn($message);
128 }
129 );
130 $this->redirectsBulkReader = new ABJ_404_Solution_RedirectsBulkReader($dbCore, $this->queryBuilder, $functions);
131 $this->logsMetricsReader = new ABJ_404_Solution_LogsMetricsReader($dbCore, $logsRepo, $functions, $resolvedLogger);
132 $this->dbMetadataReader = new ABJ_404_Solution_DatabaseMetadataReader($dbCore);
133 $this->hitsTableRebuildPolicy = new ABJ_404_Solution_HitsTableRebuildPolicy($dbCore, $logsRepo, $resolvedLogger);
134 $this->adminViewReadCoordinator = new ABJ_404_Solution_AdminViewReadCoordinator(
135 $dbCore,
136 $this->queryBuilder,
137 $this->diagnostics,
138 $this->cacheInvalidator,
139 $this->liveResolver,
140 $resolvedLogger
141 );
142 }
143
144 /** @return string */
145 private function viewDoneFreshnessOptionName(): string {
146 return $this->dbCore->tableNameResolver()->getLowercasePrefix() . 'abj404_view_done_built_at';
147 }
148
149 // =========================================================================
150 // Delegated: AdminViewReadCoordinator
151 // =========================================================================
152
153 /**
154 * @param string $sub
155 * @param array<string, mixed> $tableOptions
156 * @return array<int|string, mixed>
157 */
158 function getRedirectsForView($sub, $tableOptions) {
159 return $this->adminViewReadCoordinator->getRedirectsForView($sub, $tableOptions);
160 }
161
162 /**
163 * Map a UI orderby alias to the narrow sort-key column that backs it, or ''
164 * for a sort that is not sort-key-backed (it orders by a real always-present
165 * column and is therefore always available).
166 *
167 * @var array<string, string>
168 */
169 const ORDERBY_TO_SORT_KEY = array(
170 'url' => 'url_sort_key',
171 'dest' => 'dest_sort_key',
172 'final_dest' => 'dest_sort_key',
173 );
174
175 /** @var int|null Per-request memo of MAX(id) for the progress denominator. */
176 private $sortKeyMaxIdMemo = null;
177
178 /**
179 * Whether ordering the admin list by $orderby can be served index-ordered
180 * right now. For a narrow-sort-key-backed column (URL, Destination) that
181 * means the column exists AND its one-time legacy-row drain has converged
182 * (the latch is set) -- the SAME condition the read path uses before ordering
183 * by the key (see ViewQueryBuilder::wideColumnSortPendingBackfill /
184 * AdminViewReadCoordinator). Sorts on real always-populated columns
185 * (logshits, last_used, score, timestamp, ...) are always ready.
186 *
187 * The admin header uses this to disable the URL / Destination sort links on
188 * the captured tab during the post-upgrade window, where ordering by those
189 * columns would otherwise filesort the captured majority over the wide
190 * varchar(2048) source column and risk the host's max_statement_time.
191 *
192 * @param string $orderby UI orderby alias (url, final_dest, logshits, ...).
193 * @return bool
194 */
195 public function isSortReadyForOrderby(string $orderby): bool {
196 return $this->sortReadinessStatusForOrderby($orderby)
197 === ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_READY;
198 }
199
200 /**
201 * @param string $orderby UI orderby alias (url, final_dest, logshits, ...).
202 * @return string One of ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_*.
203 */
204 public function sortReadinessStatusForOrderby(string $orderby): string {
205 $column = self::ORDERBY_TO_SORT_KEY[strtolower($orderby)] ?? '';
206 if ($column === '') {
207 return ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_READY;
208 }
209
210 $readiness = $this->liveResolver->schemaReadiness();
211 if (!$readiness->sortKeySchemaAvailableForColumn($column)) {
212 return ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_SCHEMA_UNAVAILABLE;
213 }
214 if ($readiness->sortKeyReadyForColumn($column)) {
215 return ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_READY;
216 }
217 return ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_BACKFILL_PENDING;
218 }
219
220 /**
221 * Backfill progress for $orderby's narrow sort key as a 0..100 integer, for
222 * the admin "building the index" tooltip. 100 once ready (or when the sort is
223 * not sort-key-backed). Otherwise derived from the drain cursor (highest
224 * redirect id drained) over MAX(id): a wp_options read plus an O(1)
225 * primary-key probe, NEVER a COUNT over the captured rows. This is a
226 * high-water estimate over the id space, not an exact row-count fraction, so
227 * sparse ids are acceptable. Capped at 99 until the latch flips so the
228 * tooltip never claims 100% before the sort is actually available. A wrapped
229 * cursor of 0 is shown as 0% until the latch flips or the next drain advances
230 * it again.
231 *
232 * @param string $orderby UI orderby alias.
233 * @return int
234 */
235 public function sortBackfillPercentForOrderby(string $orderby): int {
236 if ($this->isSortReadyForOrderby($orderby)) {
237 return 100;
238 }
239 $column = self::ORDERBY_TO_SORT_KEY[strtolower($orderby)] ?? '';
240 if ($column === '' || !function_exists('get_option')) {
241 return 0;
242 }
243 $cursorOption = ABJ_404_Solution_RedirectsDenormColumnSql::sortKeyBackfillCursorOption($column);
244 $cursorRaw = get_option($cursorOption);
245 $cursor = is_numeric($cursorRaw) ? (int) $cursorRaw : 0;
246 $maxId = $this->sortKeyMaxId();
247 if ($maxId <= 0 || $cursor <= 0) {
248 return 0;
249 }
250 return max(1, min(99, (int) floor(100 * $cursor / $maxId)));
251 }
252
253 /** @return int MAX(id) on the redirects table (O(1) PK probe), memoized per request. */
254 private function sortKeyMaxId(): int {
255 if ($this->sortKeyMaxIdMemo !== null) {
256 return $this->sortKeyMaxIdMemo;
257 }
258 $table = $this->dbCore->doTableNameReplacements('{wp_abj404_redirects}');
259 $result = $this->dbCore->queryAndGetResults('SELECT MAX(id) AS max_id FROM ' . $table);
260 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
261 $firstRow = is_array($rows[0] ?? null) ? $rows[0] : array();
262 $raw = $firstRow['max_id'] ?? 0;
263 $this->sortKeyMaxIdMemo = is_numeric($raw) ? max(0, (int) $raw) : 0;
264 return $this->sortKeyMaxIdMemo;
265 }
266
267 /**
268 * Whether the most recent getRedirectsForView() result is NOT a trustworthy
269 * "genuinely empty" listing (pending build, errored read, or an empty
270 * snapshot contradicting the live source count). Drives the renderer's
271 * "still preparing" state and the AJAX view-build poller re-engagement.
272 *
273 * @return bool
274 */
275 function lastRedirectsViewReadWasIncomplete(): bool {
276 return $this->adminViewReadCoordinator->lastRedirectsViewReadWasIncomplete();
277 }
278
279 /**
280 * @param string $sub
281 * @param array<string, mixed> $tableOptions
282 * @return int Negative when the count query was incomplete or unavailable.
283 */
284 function getRedirectsForViewCount(string $sub, array $tableOptions): int {
285 return $this->adminViewReadCoordinator->getRedirectsForViewCount($sub, $tableOptions);
286 }
287
288 // =========================================================================
289 // Delegated: HitsTableRebuildPolicy
290 // =========================================================================
291
292 /** @return void */
293 function maybeUpdateRedirectsForViewHitsTable(): void {
294 $this->hitsTableRebuildPolicy->maybeUpdateRedirectsForViewHitsTable();
295 }
296
297 // =========================================================================
298 // Delegated: status and row-count repositories
299 // =========================================================================
300
301 /**
302 * @param bool $bypassCache Retained for compatibility; foreground reads are always cache-only.
303 * @param array<string, mixed> $tableOptions Retained for compatibility; never enables a foreground query.
304 * @return array<string, int>
305 */
306 function getRedirectStatusCounts($bypassCache = false, array $tableOptions = array()): array {
307 unset($bypassCache, $tableOptions);
308 return $this->statusCountsRefreshCoordinator->refreshingRedirectStatusCounts();
309 }
310
311 /** @inheritDoc Enqueues a recomputation when the cache is stale. */
312 function getRedirectStatusCountsResult(): array {
313 return $this->statusCountsRefreshCoordinator->refreshingRedirectStatusCountsResult();
314 }
315
316 /** @return array<string, int> */
317 function getRedirectHitCountHistogram(): array {
318 return $this->redirectHitCountHistogram->getRedirectHitCountHistogram();
319 }
320
321 /**
322 * @param bool $bypassCache Retained for compatibility; foreground reads are always cache-only.
323 * @param array<string, mixed> $tableOptions Retained for compatibility; never enables a foreground query.
324 * @return array<string, int>
325 */
326 function getCapturedStatusCounts($bypassCache = false, array $tableOptions = array()): array {
327 unset($bypassCache, $tableOptions);
328 return $this->statusCountsRefreshCoordinator->refreshingCapturedStatusCounts();
329 }
330
331 /** @inheritDoc Enqueues a recomputation when the cache is stale. */
332 function getCapturedStatusCountsResult(): array {
333 return $this->statusCountsRefreshCoordinator->refreshingCapturedStatusCountsResult();
334 }
335
336 /**
337 * Cron-only status-count recomputation. Foreground callers can only enqueue
338 * the work, which prevents a cache miss from entering an aggregate query.
339 */
340 public function refreshStatusCounts(string $scope): void {
341 $this->statusCountsRefreshCoordinator->refresh($scope);
342 }
343
344 /** @return int|null Enqueues a recomputation when the cache is stale. */
345 function getHighImpactCapturedCount(): ?int {
346 return $this->statusCountsRefreshCoordinator->refreshingHighImpactCapturedCount();
347 }
348
349 /** @return int */
350 function getCapturedCount() {
351 return $this->redirectRowCounts->getCapturedCount();
352 }
353
354 /**
355 * @param array<int, int> $types
356 * @param int $trashed
357 * @return int
358 */
359 function getRecordCount($types = array(), $trashed = 0) {
360 return $this->redirectRowCounts->getRecordCount(is_array($types) ? $types : array(), $trashed);
361 }
362
363 // =========================================================================
364 // Delegated: RedirectsBulkReader
365 // =========================================================================
366
367 /** @param string $tempFile @return void */
368 function doRedirectsExport(string $tempFile): void {
369 $this->redirectsBulkReader->doRedirectsExport($tempFile);
370 }
371
372 /** @return iterable<int, array<string, mixed>> */
373 function getRedirectsWithRegEx() {
374 return $this->redirectsBulkReader->getRedirectsWithRegEx();
375 }
376
377 /** @return array<int, array<string, mixed>> */
378 function getManualRedirectsWithRegexMetachars() {
379 return $this->redirectsBulkReader->getManualRedirectsWithRegexMetachars();
380 }
381
382 /** @param array<int, string> $postIDs @return array<int, mixed> */
383 function getExtraDataToPermalinkSuggestions(array $postIDs): array {
384 return $this->redirectsBulkReader->getExtraDataToPermalinkSuggestions($postIDs);
385 }
386
387 // =========================================================================
388 // Delegated: LogsMetricsReader
389 // =========================================================================
390
391 /** @param int $logID @return int */
392 function getLogsCount($logID) {
393 return $this->logsMetricsReader->getLogsCount($logID);
394 }
395
396 /** @return int */
397 function getLogDiskUsage() {
398 return $this->logsMetricsReader->getLogDiskUsage();
399 }
400
401 // =========================================================================
402 // Delegated: DatabaseMetadataReader
403 // =========================================================================
404
405 /** @return array<string, mixed> */
406 function getTableEngines() {
407 return $this->dbMetadataReader->getTableEngines();
408 }
409
410 /** @return bool */
411 function isMyISAMSupported(): bool {
412 return $this->dbMetadataReader->isMyISAMSupported();
413 }
414
415 /** @return array<int, string> */
416 function getAllPostTypes() {
417 return $this->dbMetadataReader->getAllPostTypes();
418 }
419
420 // =========================================================================
421 // Delegated: ViewQueryBuilder
422 // =========================================================================
423
424 /** @return string */
425 function buildHighImpactCapturedCountQuery(): string {
426 return $this->queryBuilder->buildHighImpactCapturedCountQuery();
427 }
428
429 /**
430 * Single-table redirects read for one page (Denorm Step 3b): fetch the
431 * ordered/filtered page off wp_abj404_redirects, then resolve the visible
432 * rows' derived/display values live and write the four denorm columns back.
433 * This is the live read path that replaces the staged view_done read.
434 *
435 * @param string $sub
436 * @param array<string, mixed> $tableOptions
437 * @return array<int, array<string, mixed>>
438 */
439 public function readRedirectsSingleTable(string $sub, array $tableOptions): array {
440 return $this->adminViewReadCoordinator->readRedirectsSingleTable($sub, $tableOptions);
441 }
442
443 /**
444 * Single-table filtered count against wp_abj404_redirects (Denorm Step 3b).
445 *
446 * @param string $sub
447 * @param array<string, mixed> $tableOptions
448 * @return int
449 */
450 public function countRedirectsSingleTable(string $sub, array $tableOptions): int {
451 return $this->adminViewReadCoordinator->countRedirectsSingleTable($sub, $tableOptions);
452 }
453
454 // =========================================================================
455 // Delegated: ViewCacheInvalidator
456 // =========================================================================
457
458 /**
459 * @template T
460 * @param callable():T $work
461 * @return T
462 */
463 public function runWithDeferredInvalidation(callable $work) {
464 return $this->cacheInvalidator->runWithDeferredInvalidation($work);
465 }
466
467 /** @return void */
468 function invalidateStatusCountsCache(): void {
469 $this->cacheInvalidator->invalidateStatusCountsCache();
470 }
471
472 /** @return void */
473 function invalidateViewSnapshotCache(): void {
474 $this->cacheInvalidator->invalidateViewSnapshotCache();
475 }
476
477 /** @return void */
478 function clearRegexRedirectsCache(): void {
479 $this->cacheInvalidator->clearRegexRedirectsCache();
480 }
481
482 // =========================================================================
483 // Delegated: ViewDiagnostics
484 // =========================================================================
485
486 /**
487 * @param string $sub
488 * @param string $failedQuery
489 * @param array<string, mixed> $tableOptions
490 * @param array<string, mixed> $queryResult
491 * @return array<string, mixed>
492 */
493 public function captureViewQueryFailureDiagnostics(string $sub, string $failedQuery, array $tableOptions, array $queryResult): array {
494 return $this->diagnostics->captureViewQueryFailureDiagnostics($sub, $failedQuery, $tableOptions, $queryResult);
495 }
496 }
497