| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Freshness signals for the redirects-hits rollup, for the feedback payload's |
| 9 |
* `environment_extras.view_build_state` field. |
| 10 |
* |
| 11 |
* The rollup (ABJ_404_Solution_LogsHitsRollupService, reached through the |
| 12 |
* `logs_repository` service) is the subsystem most implicated in "the admin |
| 13 |
* redirects page never loads" reports: the redirects tab reads hit counts from |
| 14 |
* the rollup table, and a rollup that does not exist, is mid-rebuild, or has |
| 15 |
* fallen far behind logsv2 is the difference between a page that renders and |
| 16 |
* one that times out. This probe answers, in one payload field, whether that |
| 17 |
* has happened on the reporting site. |
| 18 |
* |
| 19 |
* WHY THIS READS A SERVICE AND NOT OPTIONS. This is the live successor to the |
| 20 |
* staged view-build pipeline that commit 73a55a70 (2026-06-13) deleted. The |
| 21 |
* previous implementation hand-assembled the same field from five |
| 22 |
* `get_option('abj404_view_build_*')` literals; that commit removed every |
| 23 |
* writer of those names without touching the reads, and because get_option() |
| 24 |
* returns null for a missing key and is_scalar(null) is false, every key was |
| 25 |
* silently dropped and this field shipped `[]` on every payload for seven |
| 26 |
* weeks -- through four consecutive support reports from a user whose reported |
| 27 |
* symptom was exactly the one this field exists to diagnose. Reading the |
| 28 |
* rollup service's own public accessors instead means there is no storage-key |
| 29 |
* literal here to be orphaned: the service owns its keys, so a rename moves |
| 30 |
* the read with the write. tests/FeedbackProbeOrphanedOptionReadsTest.php |
| 31 |
* fails the build if any feedback probe reintroduces that shape. |
| 32 |
* |
| 33 |
* Owned by ABJ_404_Solution_FeedbackEnvironmentExtras via composition. |
| 34 |
*/ |
| 35 |
class ABJ_404_Solution_RollupFreshnessProbe { |
| 36 |
|
| 37 |
/** |
| 38 |
* Live rollup state: does the rollup table exist, does it need a rebuild |
| 39 |
* right now, when did it last actually refresh or get scheduled, and how |
| 40 |
* far behind is its stored watermark from logsv2's true max id? |
| 41 |
* |
| 42 |
* `rollup_stored_max_log_id` vs `logsv2_max_log_id` is the staleness |
| 43 |
* measure the field exists for: equal means current, a large gap means the |
| 44 |
* rollup has fallen behind and the redirects tab is showing stale hit |
| 45 |
* counts (or timing out trying not to). |
| 46 |
* |
| 47 |
* Throws when the rollup service is unavailable, so the caller's |
| 48 |
* recordProbe() wrapper writes a `view_build_state_error` marker. An empty |
| 49 |
* array must never again be this probe's way of saying "I could not look." |
| 50 |
* |
| 51 |
* COST. Every accessor here is a single-row read (information_schema |
| 52 |
* lookups, an indexed MAX(id), a runtime flag); none of them starts a |
| 53 |
* rebuild, which matters because a probe that provoked the stall it exists |
| 54 |
* to diagnose would be worse than no probe. hitsTableNeedsRebuild() |
| 55 |
* re-reads three of the values below internally, so a support send issues |
| 56 |
* those reads twice. That is deliberate: re-deriving the staleness rule |
| 57 |
* here from the raw ids would copy LogsHitsRollupService's policy into the |
| 58 |
* payload builder, and a duplicated rule that drifts is a far worse trade |
| 59 |
* than three extra single-row queries on a user-initiated send. |
| 60 |
* |
| 61 |
* @return array<string, int|bool> |
| 62 |
*/ |
| 63 |
public function collectRollupFreshness(): array { |
| 64 |
$logsRepo = function_exists('abj_service_optional') ? abj_service_optional('logs_repository') : null; |
| 65 |
if (!$logsRepo instanceof ABJ_404_Solution_LogHitsLifecycleInterface |
| 66 |
|| !$logsRepo instanceof ABJ_404_Solution_LogHitsRebuildInterface) { |
| 67 |
throw new \RuntimeException('logs_repository service unavailable for view_build_state probe'); |
| 68 |
} |
| 69 |
$lastUpdated = $logsRepo->getLogsHitsTableLastUpdated(); |
| 70 |
$lastScheduled = $logsRepo->getLogsHitsTableLastScheduledAt(); |
| 71 |
return array( |
| 72 |
'rollup_table_exists' => (bool)$logsRepo->logsHitsTableExists(), |
| 73 |
'rollup_needs_rebuild' => (bool)$logsRepo->hitsTableNeedsRebuild(), |
| 74 |
'rollup_last_updated_at' => is_numeric($lastUpdated) ? (int)$lastUpdated : 0, |
| 75 |
'rollup_last_scheduled_at' => is_numeric($lastScheduled) ? (int)$lastScheduled : 0, |
| 76 |
'rollup_stored_max_log_id' => (int)$logsRepo->getStoredMaxLogId(), |
| 77 |
'logsv2_max_log_id' => (int)$logsRepo->getMaxLogId(), |
| 78 |
); |
| 79 |
} |
| 80 |
} |
| 81 |
|