PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
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 / ViewCacheInvalidator.php

ViewCacheInvalidator.php in 404 Solution 4.3.0, at includes/view-build/ViewCacheInvalidator.php

148 lines 5.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 * Cache invalidation orchestration for admin view queries. Coordinates
9 * status-count cache invalidation, view snapshot invalidation, and regex
10 * cache clearing when redirects mutate.
11 */
12 class ABJ_404_Solution_ViewCacheInvalidator {
13
14 /** @var ABJ_404_Solution_DatabaseCore */
15 private $dbCore;
16
17 /** @var ABJ_404_Solution_RedirectsRepository */
18 private $redirectsRepo;
19
20 /** @var string */
21 private $viewDoneFreshnessOptionName;
22
23 /**
24 * @param ABJ_404_Solution_DatabaseCore $dbCore
25 * @param ABJ_404_Solution_RedirectsRepository $redirectsRepo
26 * @param string $viewDoneFreshnessOptionName
27 */
28 public function __construct(
29 ABJ_404_Solution_DatabaseCore $dbCore,
30 ABJ_404_Solution_RedirectsRepository $redirectsRepo,
31 string $viewDoneFreshnessOptionName
32 ) {
33 $this->dbCore = $dbCore;
34 $this->redirectsRepo = $redirectsRepo;
35 $this->viewDoneFreshnessOptionName = $viewDoneFreshnessOptionName;
36 }
37
38 /** @return void */
39 public function setSqlBigSelects(): void {
40 $ignoreErrorsOptions = array('log_errors' => false);
41 $this->dbCore->queryAndGetResults("set session max_join_size = 18446744073709551615",
42 $ignoreErrorsOptions);
43 $this->dbCore->queryAndGetResults("set session sql_big_selects = 1", $ignoreErrorsOptions);
44 }
45
46 /**
47 * Open/close the bulk-mutation window.
48 *
49 * @template T
50 * @param callable():T $work
51 * @return T
52 */
53 public function runWithDeferredInvalidation(callable $work) {
54 $prior = ABJ_404_Solution_ViewReadRuntimeState::$bulkMutationInProgress;
55 ABJ_404_Solution_ViewReadRuntimeState::$bulkMutationInProgress = true;
56 try {
57 return $work();
58 } finally {
59 ABJ_404_Solution_ViewReadRuntimeState::$bulkMutationInProgress = $prior;
60 }
61 }
62
63 /**
64 * Invalidate cached status counts.
65 * Call this when redirects are created, updated, or deleted.
66 *
67 * @return void
68 */
69 public function invalidateStatusCountsCache(): void {
70 if (ABJ_404_Solution_ViewReadRuntimeState::$bulkMutationInProgress) {
71 return;
72 }
73 delete_transient(ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_REDIRECT_STATUS);
74 delete_transient(ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_CAPTURED_STATUS);
75 delete_transient(ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_HIGH_IMPACT_CAPTURED);
76 $this->invalidateViewSnapshotCache();
77 }
78
79 /**
80 * Debounced, captured-scoped status-count invalidation for the
81 * high-frequency captured-insert path (report.md Finding 4).
82 *
83 * A captured 404 insert only changes the captured + high-impact counts; it
84 * never changes the redirect (manual/auto/regex) counts, so this leaves the
85 * REDIRECT status-count cache intact (the redirects tab stays warm through a
86 * capture burst). It also collapses a burst into at most one invalidation per
87 * cooldown window, so the SUM(CASE) captured-count aggregate is recomputed at
88 * most once per window instead of cold on every admin load.
89 *
90 * It does NOT clear the view snapshot: the admin table read is live off
91 * wp_abj404_redirects (Denorm Step 3b -- no snapshot result cache), so a new
92 * captured row appears on the next read regardless. The detect-only poll
93 * (Finding 3) reads the same live source, so staleness is still detected.
94 *
95 * Static + pure transient ops: holds no instance state, so the
96 * frontend-capture hot path can call it without a fully-wired invalidator.
97 *
98 * @return void
99 */
100 public static function invalidateCapturedStatusCountsCacheDebounced(): void {
101 if (ABJ_404_Solution_ViewReadRuntimeState::$bulkMutationInProgress) {
102 return;
103 }
104 $cooldownKey = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_CAPTURED_COUNT_INVALIDATE_COOLDOWN;
105 if (get_transient($cooldownKey)) {
106 return;
107 }
108 set_transient($cooldownKey, 1,
109 ABJ_404_Solution_ViewReadRuntimeState::CAPTURED_COUNT_INVALIDATE_COOLDOWN_SECONDS);
110 delete_transient(ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_CAPTURED_STATUS);
111 delete_transient(ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_HIGH_IMPACT_CAPTURED);
112 }
113
114 /**
115 * Clear the view snapshot cache.
116 *
117 * @return void
118 */
119 public function invalidateViewSnapshotCache(): void {
120 if (function_exists('delete_option')) {
121 delete_option($this->viewDoneFreshnessOptionName);
122 }
123
124 $query = "DELETE FROM {wp_abj404_view_cache} WHERE 1=1";
125 $this->dbCore->queryAndGetResults($query, array('log_errors' => false, 'skip_repair' => true));
126
127 global $wpdb;
128 if (isset($wpdb->options) && method_exists($wpdb, 'query')) {
129 /** @var string $optionsTable */
130 // @utf8-audit: opt-out — wpdb->options is a WordPress-controlled table identifier.
131 $optionsTable = esc_sql($wpdb->options);
132 // DAO-bypass-approved: View-cache clear targets wp_options -- outside the plugin's owned tables; runs during cache invalidation hot path; failure is best-effort
133 $wpdb->query(
134 "DELETE FROM `{$optionsTable}` WHERE option_name LIKE '_transient_abj404_view_%'"
135 . " OR option_name LIKE '_transient_timeout_abj404_view_%'"
136 );
137 }
138 }
139
140 /**
141 * @return void
142 */
143 public function clearRegexRedirectsCache(): void {
144 $this->redirectsRepo->clearRegexRedirectsCache();
145 }
146
147 }
148