PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 All 35 releases
← All changes | includes/class-host-page-caches.php +87 -59 1.3.4 → 1.4.1 View file →
@@ -47,21 +47,21 @@
47 47 * Detection is by constant and global only, never `is_plugin_active()` on a
48 48 * path string: a renamed plugin folder must not silently turn the integration
49 49 * off. Same rule as Render_Caches.
50 50 *
51 - * Deliberately purge-ALL only. Nginx Helper's per-URL entry point is a method
52 - * call on its purger object rather than an action, and `Cache::purge_url()`
53 - * has no action to hook yet; forwarding single URLs is a separate change once
54 - * that seam lands.
51 + * **This class is the mechanism, not the policy.** It knows how to detect the
52 + * server cache and how to ask Nginx Helper to clear it. WHEN to ask is decided
53 + * one level up, by `Server_Caches::forward()`, from the `intent` and `scope`
54 + * on the purge-event contract — which is also where the reasoning for standing
55 + * down on a content purge is written. Nothing here listens to a hook.
55 56 *
56 - * **Converges with `Server_Caches` later.** PR #348 introduces that class for
57 - * the same idea — forwarding a purge to a cache in front of PHP — with
58 - * LiteSpeed as its first adapter and `xspeed_purge_server_caches` as its
59 - * public seam. Nothing this class does overlaps with it today (different
60 - * server cache, different plugin, and the purge sets are disjoint), so the two
61 - * can land independently. Once #348 is merged, the right shape for this is an
62 - * adapter registered on that filter rather than its own listener; keeping it
63 - * separate now is what avoids editing a branch that is out for re-test.
57 + * Two calls: purge_nginx_helper() clears the whole zone through the action
58 + * above, and purge_nginx_helper_urls() clears named pages through
59 + * `$GLOBALS['nginx_purger']->purge_url()`, a method on its purger object
60 + * rather than an action. That method checks `is_page()`/`is_single()` before
61 + * purging an AMP copy; both only warn when the global `$wp_query` does not
62 + * exist, and `wp-settings.php` creates it for every request, so WP-CLI, cron
63 + * and REST purges are quiet.
64 64 *
65 65 * **Multisite:** nginx keys one cache zone per *install*, not per subsite, so
66 66 * a purge here clears every site on the network at the nginx layer. That is
67 67 * accepted rather than worked around — a cold cache costs one slow request
@@ -84,59 +84,33 @@
84 84 */
85 85 private const NH_FASTCGI = 'enable_fastcgi';
86 86
87 87 /**
88 - * Re-entrancy latch. See purge_nginx_helper().
88 + * Forward a full purge to the server-level cache, when there is one.
89 89 *
90 - * @var bool
91 - */
92 - private static bool $purging = false;
93 -
94 - /**
95 - * Register the listener.
90 + * No gate on the purge's reason here — `Server_Caches::forward()` has
91 + * already decided this purge should reach the server layer. Detection is
92 + * still checked, because it is a fact about the environment rather than
93 + * about the purge, and it is only stable at purge time: Nginx Helper
94 + * builds `$GLOBALS['nginx_purger']` from its own `plugins_loaded`
95 + * callback, so anything asked earlier races its load order.
96 96 *
97 - * Registration is unconditional and the gate lives in the callback: this
98 - * runs from `Plugin::init()` on `plugins_loaded`, and Nginx Helper builds
99 - * `$GLOBALS['nginx_purger']` from its own `plugins_loaded` callback, so
100 - * load order decides whether a check made here would see it. The callback
101 - * runs during a purge, long after both plugins are up, where the answer
102 - * is stable.
103 - */
104 - public static function boot(): void {
105 - add_action( 'xspeed_after_purge_all', array( __CLASS__, 'purge_nginx_helper' ), 10, 1 );
106 - }
107 -
108 - /**
109 - * Forward a full purge to the server-level cache, when there is one.
97 + * Re-entrancy is handled upstream. A third-party listener on
98 + * `rt_nginx_helper_purge_all` that calls back into `Cache::purge_all()`
99 + * used to recurse here until PHP ran out of stack, which is what the
100 + * latch this method once carried was for. The inner purge now re-enters
101 + * `Cache::dispatch_purge_event()`, whose `$purge_events_in_flight` guard
102 + * is still held by the outer one and returns before any adapter runs.
103 + * Pinned by a test that recurses through `Cache::purge_all()` itself
104 + * rather than through this method.
110 105 *
111 - * @param string $cause Who asked. Threaded through for symmetry with the
112 - * other `xspeed_after_purge_all` listeners; Nginx
113 - * Helper's action takes no arguments.
114 106 * @return bool Whether the purge was forwarded.
115 107 */
116 - public static function purge_nginx_helper( $cause = 'manual' ): bool {
117 - unset( $cause );
118 -
119 - /*
120 - * Nginx Helper's purge is a directory sweep, but it is not OUR code:
121 - * it runs third-party listeners on `rt_nginx_helper_purge_all`, and a
122 - * site can easily have one that calls back into a WordPress purge —
123 - * a "keep every cache in sync" mu-plugin is the common shape. Without
124 - * this latch that lands back in Cache::purge_all(), which fires
125 - * `xspeed_after_purge_all` again, and the two purges recurse until PHP
126 - * runs out of stack. One forward per request is all this integration
127 - * can usefully do anyway, since the second sweep would find an empty
128 - * directory.
129 - */
130 - if ( self::$purging ) {
131 - return false;
132 - }
133 -
108 + public static function purge_nginx_helper(): bool {
134 109 if ( ! self::nginx_helper_is_fastcgi() ) {
135 110 return false;
136 111 }
137 112
138 - self::$purging = true;
139 113 try {
140 114 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Nginx Helper's own public integration hook; an xspeed_-prefixed name would reach nothing.
141 115 do_action( 'rt_nginx_helper_purge_all' );
142 116 } catch ( \Throwable $e ) {
@@ -146,14 +120,14 @@
146 120 * own. Letting it out would take down `Cache::purge_all()` — the
147 121 * admin presses Purge All, another plugin's mu-plugin fatals, and
148 122 * the failure reads as xSpeed's. The local sweep has already
149 123 * happened by the time we run, so swallowing this costs the
150 - * server layer and nothing else. `Server_Caches::forward()` on
151 - * #348 catches at the same boundary for the same reason.
124 + * server layer and nothing else. `Server_Caches::forward()`
125 + * catches around this call for the same reason and logs it under
126 + * WP_DEBUG; this inner catch is what turns a throw into an honest
127 + * "not forwarded" return rather than a reported purge.
152 128 */
153 129 return false;
154 - } finally {
155 - self::$purging = false;
156 130 }
157 131
158 132 return true;
159 133 }
@@ -158,8 +132,46 @@
158 132 return true;
159 133 }
160 134
161 135 /**
136 + * Purge named pages from the nginx FastCGI cache through Nginx Helper.
137 + *
138 + * Its purger's `purge_url()` is the same call Nginx Helper makes for its
139 + * own post purges. Feeds are not added (`$feed = false`): xSpeed names the
140 + * feeds it means. If the purger cannot take a URL, the whole zone goes
141 + * instead, because a page left stale is worse than a cold cache. The
142 + * same happens when the calls run past `$seconds`: with the `get_request`
143 + * method each one is a blocking HTTP request, and a slow purge endpoint
144 + * would otherwise hold the request for the whole list.
145 + *
146 + * @param string[] $urls Absolute URLs on this site.
147 + * @param float $seconds Time allowed for the per-URL calls; 0 for no limit.
148 + */
149 + public static function purge_nginx_helper_urls( array $urls, float $seconds = 0.0 ): bool {
150 + if ( array() === $urls || ! self::nginx_helper_is_fastcgi() ) {
151 + return false;
152 + }
153 + $purger = $GLOBALS['nginx_purger'] ?? null;
154 + if ( ! is_object( $purger ) || ! method_exists( $purger, 'purge_url' ) ) {
155 + return self::purge_nginx_helper();
156 + }
157 + $started = microtime( true );
158 + $left = count( $urls );
159 + try {
160 + foreach ( $urls as $url ) {
161 + $purger->purge_url( (string) $url, false );
162 + --$left;
163 + if ( $left > 0 && $seconds > 0 && microtime( true ) - $started > $seconds ) {
164 + return self::purge_nginx_helper();
165 + }
166 + }
167 + } catch ( \Throwable $e ) {
168 + return self::purge_nginx_helper();
169 + }
170 + return true;
171 + }
172 +
173 + /**
162 174 * Whether Nginx Helper is present AND configured against an nginx FastCGI
163 175 * cache.
164 176 *
165 177 * Three separate facts, because each one alone is a false positive:
@@ -199,9 +211,10 @@
199 211 * has said "don't purge behind my back"; they have not said "ignore me
200 212 * when I press Purge All". Reading it as the latter would leave an
201 213 * explicit, operator-initiated purge silently short of the layer actually
202 214 * serving the page, which is the exact failure this integration exists to
203 - * fix.
215 + * fix. `nginx_helper_purges_changes()` does read it, only to decide
216 + * whether a content purge can be left to Nginx Helper.
204 217 */
205 218 public static function nginx_helper_is_fastcgi(): bool {
206 219 return null !== self::nginx_helper_cache_path();
207 220 }
@@ -228,8 +241,23 @@
228 241 if ( ! is_array( $options ) || self::NH_FASTCGI !== ( $options['cache_method'] ?? '' ) ) {
229 242 return null;
230 243 }
231 244 return $path;
245 + }
246 +
247 + /**
248 + * Whether Nginx Helper purges changed pages by itself.
249 + *
250 + * Its `enable_purge` option is the one switch in front of all of its
251 + * automatic purging: the post, comment and term hooks each return early
252 + * without it. It defaults to off, so an install where the host never
253 + * turned it on purges nothing when a post is published. Not part of the
254 + * detection gate above; `Server_Caches` asks it only to decide whether a
255 + * content purge can be left to Nginx Helper.
256 + */
257 + public static function nginx_helper_purges_changes(): bool {
258 + $options = get_site_option( self::NH_OPTION );
259 + return is_array( $options ) && ! empty( $options['enable_purge'] );
232 260 }
233 261
234 262 /**
235 263 * The configured purge method (`unlink_files`, `get_request`, …), or ''