PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.0
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-server-caches.php +252 -11 1.3.6 → 1.4.0 View file →
@@ -209,8 +209,9 @@
209 209 if ( 'network' === $scope && ! defined( 'LSWCP_EMPTYCACHE' ) ) {
210 210 define( 'LSWCP_EMPTYCACHE', true );
211 211 }
212 212 \LiteSpeed\Purge::purge_all_lscache( 'xSpeed response invalidation' );
213 + self::note_forwarded( 'LiteSpeed Cache' );
213 214 }
214 215 return;
215 216 }
216 217
@@ -226,8 +227,11 @@
226 227 }
227 228 foreach ( array_values( array_unique( $targets ) ) as $target ) {
228 229 do_action( 'litespeed_purge_url', $target );
229 230 }
231 + if ( array() !== $targets ) {
232 + self::note_forwarded( 'LiteSpeed Cache' );
233 + }
230 234 }
231 235
232 236 /**
233 237 * nginx FastCGI full-page cache, through the Nginx Helper plugin.
@@ -263,9 +267,12 @@
263 267 * for itself. (QA #448)
264 268 *
265 269 * The trade that stays: Nginx Helper purges the post, the homepage and
266 270 * the post's archives. An ordinary page that lists recent posts is none of
267 - * those, and keeps its old list until the server TTL expires. The
271 + * those, and keeps its old list until the server TTL expires. When xSpeed
272 + * knows that happened, because a narrow purge fell back to the whole site
273 + * for a list the theme draws everywhere, the context says so in
274 + * `fallback` and the zone is cleared (fallback_needs_whole_zone()). The
268 275 * `xspeed_nginx_helper_defer_content_purge` filter returns to clearing
269 276 * the whole zone on every content purge outside an import, for a site that
270 277 * needs those pages current.
271 278 *
@@ -296,16 +303,32 @@
296 303 $scope = isset( $context['scope'] ) && is_string( $context['scope'] )
297 304 ? $context['scope']
298 305 : ( '' !== $url ? 'urls' : 'site' );
299 306
300 - // `urls` is a per-URL purge, which this integration does not do yet —
301 - // see Host_Page_Caches. Standing down is the honest answer: the
302 - // alternative, treating a one-page purge as a reason to clear the
303 - // whole install, is the bug this method exists to fix.
304 - if ( 'urls' === $scope || 'none' === $scope ) {
307 + if ( 'none' === $scope ) {
305 308 return;
306 309 }
307 310
311 + // Named pages go to Nginx Helper's own per-URL purge, whether or not
312 + // its automatic purging is on. Its own rules clear the post, the home
313 + // page and the post's archives, but no archive page past the first and
314 + // no neighbouring post, so leaving a narrow purge to it would leave
315 + // those stale. Only this site's URLs: with the `get_request` method
316 + // Nginx Helper keeps the path and swaps in its own host. Collected
317 + // and sent once, at shutdown (flush_nginx_helper()).
318 + if ( 'urls' === $scope ) {
319 + if ( self::is_importing() || ! Host_Page_Caches::nginx_helper_is_fastcgi() ) {
320 + return;
321 + }
322 + $urls = isset( $context['urls'] ) && is_array( $context['urls'] ) ? $context['urls'] : array( $url );
323 + foreach ( $urls as $target ) {
324 + if ( is_string( $target ) && '' !== $target && self::is_this_site( $target ) ) {
325 + self::queue_nginx_url( $target );
326 + }
327 + }
328 + return;
329 + }
330 +
308 331 $intent = isset( $context['intent'] ) && is_string( $context['intent'] ) && '' !== $context['intent']
309 332 ? $context['intent']
310 333 : 'complete';
311 334
@@ -322,11 +345,13 @@
322 345 * whole nginx cache.
323 346 *
324 347 * Defaults to true when Nginx Helper's automatic purging is on,
325 348 * since it has already purged the post, the homepage and the
326 - * post's archives. Return false to clear the whole zone instead,
327 - * for a site whose pages list posts somewhere Nginx Helper does
328 - * not purge.
349 + * post's archives. Defaults to false when `fallback` in the
350 + * context says pages outside those changed (`theme_list`,
351 + * `pending`, `filter`, `listing`). Return false to clear the
352 + * whole zone instead, for a site whose pages list posts
353 + * somewhere Nginx Helper does not purge.
329 354 *
330 355 * @param bool $defer Whether to leave it to Nginx Helper.
331 356 * @param array<string,mixed> $context Public purge context.
332 357 */
@@ -331,9 +356,9 @@
331 356 * @param array<string,mixed> $context Public purge context.
332 357 */
333 358 $defer = (bool) apply_filters(
334 359 'xspeed_nginx_helper_defer_content_purge',
335 - Host_Page_Caches::nginx_helper_purges_changes(),
360 + Host_Page_Caches::nginx_helper_purges_changes() && ! self::fallback_needs_whole_zone( $context ),
336 361 $context
337 362 );
338 363 if ( $defer ) {
339 364 return;
@@ -358,9 +383,225 @@
358 383 // the same directory as ours: skipping on a foreign host would leave
359 384 // the pages the purge was actually for still being served. Pro's
360 385 // Multisite::purge_site() runs inside switch_to_blog() and reaches
361 386 // here with that blog's host.
362 - Host_Page_Caches::purge_nginx_helper();
387 + self::purge_nginx_zone();
388 + }
389 +
390 + /**
391 + * Most URLs sent to Nginx Helper one by one in a request. Past this the
392 + * zone is cleared once instead. A typical save names about 35 pages.
393 + */
394 + private const NGINX_URL_LIMIT = 100;
395 +
396 + /**
397 + * Seconds of per-URL purging after which the rest of the batch becomes
398 + * one zone purge. Nginx Helper's `get_request` method sends a blocking
399 + * GET per URL with WordPress's 5-second default timeout.
400 + */
401 + private const NGINX_URL_SECONDS = 3.0;
402 +
403 + /**
404 + * This request's nginx work, sent once at shutdown.
405 + *
406 + * `urls` is keyed by URL, so a page that two saves in one request both
407 + * name (a bulk edit, where every post shares the home page and the
408 + * archives) is sent once. `overflow` means more than the limit arrived.
409 + * `zone_done` means the zone was cleared earlier in this request, and
410 + * `zone_again` that another clear was asked for after it.
411 + *
412 + * @var array{urls:array<string,bool>,overflow:bool,zone_done:bool,zone_again:bool,armed:bool}
413 + */
414 + private static $nginx = array(
415 + 'urls' => array(),
416 + 'overflow' => false,
417 + 'zone_done' => false,
418 + 'zone_again' => false,
419 + 'armed' => false,
420 + );
421 +
422 + /** Test seam: forget this request's nginx work. */
423 + public static function reset(): void {
424 + self::$nginx = array(
425 + 'urls' => array(),
426 + 'overflow' => false,
427 + 'zone_done' => false,
428 + 'zone_again' => false,
429 + 'armed' => false,
430 + );
431 + }
432 +
433 + /**
434 + * Clear the whole nginx zone, at most once now and once more at the end
435 + * of the request.
436 + *
437 + * The first clear runs at once, as it always has, so an operator's Purge
438 + * All lands before the response. A later one in the same request (a bulk
439 + * edit of ten posts that each fall back to the whole site) waits for
440 + * shutdown and runs once, after every change the request makes. Clearing
441 + * the zone covers every URL queued before it, so those are dropped.
442 + */
443 + private static function purge_nginx_zone(): void {
444 + self::note_forwarded( 'Nginx Helper' );
445 + self::$nginx['urls'] = array();
446 + self::$nginx['overflow'] = false;
447 + if ( self::$nginx['zone_done'] ) {
448 + self::$nginx['zone_again'] = true;
449 + self::arm_nginx_flush();
450 + return;
451 + }
452 + if ( Host_Page_Caches::purge_nginx_helper() ) {
453 + self::$nginx['zone_done'] = true;
454 + }
455 + }
456 +
457 + /**
458 + * Tell an operator's purge report which cache took the purge. The
459 + * nginx batch is sent at shutdown, after the caller has printed its
460 + * answer, so this records the hand-off rather than the send.
461 + *
462 + * @param string $layer Cache name.
463 + */
464 + private static function note_forwarded( string $layer ): void {
465 + if ( class_exists( __NAMESPACE__ . '\\Cache' ) ) {
466 + Cache::note_purge_forwarded( $layer );
467 + }
468 + }
469 +
470 + /** Add one URL to this request's nginx batch. */
471 + private static function queue_nginx_url( string $url ): void {
472 + self::note_forwarded( 'Nginx Helper' );
473 + if ( self::$nginx['zone_again'] ) {
474 + // The zone is cleared at shutdown anyway.
475 + return;
476 + }
477 + if ( ! self::$nginx['overflow'] ) {
478 + self::$nginx['urls'][ $url ] = true;
479 + if ( count( self::$nginx['urls'] ) > self::nginx_url_limit() ) {
480 + self::$nginx['overflow'] = true;
481 + self::$nginx['urls'] = array();
482 + }
483 + }
484 + self::arm_nginx_flush();
485 + }
486 +
487 + /**
488 + * Send the batch at shutdown, or now when shutdown is already running.
489 + *
490 + * Inside `shutdown` a callback added at a priority that has already run
491 + * would never fire, so a purge raised there (Cache::flush_pending_saves()
492 + * runs at priority 1) is sent straight away.
493 + */
494 + private static function arm_nginx_flush(): void {
495 + if ( function_exists( 'did_action' ) && did_action( 'shutdown' ) ) {
496 + self::flush_nginx_helper();
497 + return;
498 + }
499 + if ( self::$nginx['armed'] || ! function_exists( 'add_action' ) ) {
500 + return;
501 + }
502 + self::$nginx['armed'] = true;
503 + add_action( 'shutdown', array( __CLASS__, 'flush_nginx_helper' ), 20, 0 );
504 + }
505 +
506 + /**
507 + * Send this request's nginx work: one zone clear, or each queued URL.
508 + *
509 + * Sent in the request that raised it, not handed to WP-Cron the way the
510 + * Cloudflare module defers its edge calls. On a site behind an nginx page
511 + * cache, anonymous visits are answered by nginx and never run PHP, so
512 + * WP-Cron can go a long time without a request to run on, and the pages
513 + * would stay stale until it did. The visitor requests that reach here (a
514 + * comment, a stock change at checkout) name about four URLs, and Nginx
515 + * Helper's own comment and post hooks already purge inline in those same
516 + * requests. NGINX_URL_SECONDS bounds the wait when the purge endpoint is
517 + * slow.
518 + *
519 + * Public because it is a `shutdown` callback; not part of the contract.
520 + */
521 + public static function flush_nginx_helper(): void {
522 + $work = self::$nginx;
523 + self::$nginx['urls'] = array();
524 + self::$nginx['overflow'] = false;
525 + self::$nginx['zone_again'] = false;
526 + self::$nginx['armed'] = false;
527 +
528 + if ( ! Host_Page_Caches::nginx_helper_is_fastcgi() ) {
529 + return;
530 + }
531 + if ( $work['zone_again'] ) {
532 + Host_Page_Caches::purge_nginx_helper();
533 + return;
534 + }
535 + if ( $work['overflow'] ) {
536 + self::record_nginx_zone_fallback();
537 + if ( Host_Page_Caches::purge_nginx_helper() ) {
538 + self::$nginx['zone_done'] = true;
539 + }
540 + return;
541 + }
542 + if ( array() === $work['urls'] ) {
543 + return;
544 + }
545 + Host_Page_Caches::purge_nginx_helper_urls( array_keys( $work['urls'] ), self::NGINX_URL_SECONDS );
546 + }
547 +
548 + /**
549 + * How many URLs a request may send to Nginx Helper one by one.
550 + */
551 + private static function nginx_url_limit(): int {
552 + if ( ! function_exists( 'apply_filters' ) ) {
553 + return self::NGINX_URL_LIMIT;
554 + }
555 + /**
556 + * Filter how many URLs one request sends to Nginx Helper one by one
557 + * before clearing the whole nginx zone instead.
558 + *
559 + * @param int $limit URLs per request.
560 + */
561 + $limit = (int) apply_filters( 'xspeed_nginx_helper_url_purge_limit', self::NGINX_URL_LIMIT );
562 + return $limit > 0 ? $limit : self::NGINX_URL_LIMIT;
563 + }
564 +
565 + /** Say in the activity log that a batch became a zone clear. */
566 + private static function record_nginx_zone_fallback(): void {
567 + if ( ! class_exists( __NAMESPACE__ . '\\Activity_Log' ) || ! function_exists( '__' ) ) {
568 + return;
569 + }
570 + Activity_Log::record(
571 + 'cache_purged',
572 + sprintf(
573 + /* translators: %d: most URLs purged one by one. */
574 + __( 'Cleared the whole nginx cache: more than %d pages changed in one request, too many to purge one by one', 'xspeed' ),
575 + self::nginx_url_limit()
576 + )
577 + );
578 + }
579 +
580 + /**
581 + * Whether a site-wide content purge fell back from a narrow one for a
582 + * reason Nginx Helper's own rules do not cover.
583 + *
584 + * Nginx Helper purges the post, the homepage and the first page of the
585 + * post's archives. That is enough for `limit`: the pages were all named,
586 + * only too many of them, and the ones past the first archive page wait
587 + * for the server TTL, as every content purge did before narrow purges.
588 + * It is not enough for the others:
589 + *
590 + * - `theme_list`: a list the theme draws on every page changed.
591 + * - `pending`: the save could not be worked out at all.
592 + * - `filter`: a site's own code said the named pages are not enough.
593 + * - `listing`: pages that run a post list of their own (a page builder
594 + * grid) may have changed, and they are not ones Nginx Helper purges.
595 + *
596 + * An excluded post type (a WooCommerce product) carries no reason and
597 + * stays with Nginx Helper.
598 + *
599 + * @param array<string,mixed> $context Public purge context.
600 + */
601 + private static function fallback_needs_whole_zone( array $context ): bool {
602 + $reason = isset( $context['fallback'] ) && is_string( $context['fallback'] ) ? $context['fallback'] : '';
603 + return in_array( $reason, array( Cache::FALLBACK_THEME_LIST, Cache::FALLBACK_PENDING, Cache::FALLBACK_FILTER, Cache::FALLBACK_LISTING ), true );
363 604 }
364 605
365 606 /**
366 607 * Whether WordPress is importing content right now.