Skip to content
Snippets Groups Projects
Commit 110adb2d authored by catch's avatar catch
Browse files

Merge branch '3257725-cache-prewarm' into '11.x'

Issue #3257725: Add a cache prewarm API and use it to distribute cache rebuids after cache clears / during stampedes

See merge request !4923
parents ec1b6713 e0fed68d
No related branches found
No related tags found
No related merge requests found
Pipeline #319188 failed
Pipeline: drupal

#319192

    Showing
    with 311 additions and 5 deletions
    ......@@ -323,6 +323,12 @@ services:
    - { name: cache.bin, default_backend: cache.backend.chainedfast }
    factory: ['@cache_factory', 'get']
    arguments: [discovery]
    cache_prewarmer:
    class: Drupal\Core\PreWarm\CachePreWarmer
    arguments: ['@class_resolver']
    tags:
    - { name: service_id_collector, tag: cache_prewarmable }
    Drupal\Core\PreWarm\CachePreWarmerInterface: '@cache_prewarmer'
    variation_cache.access_policy:
    class: Drupal\Core\Cache\VariationCacheInterface
    factory: ['@variation_cache_factory', 'get']
    ......@@ -729,6 +735,8 @@ services:
    entity_field.manager:
    class: Drupal\Core\Entity\EntityFieldManager
    arguments: ['@entity_type.manager', '@entity_type.bundle.info', '@entity_display.repository', '@typed_data_manager', '@language_manager', '@keyvalue', '@module_handler', '@cache.discovery', '@entity.last_installed_schema.repository']
    tags:
    - { name: cache_prewarmable }
    Drupal\Core\Entity\EntityFieldManagerInterface: '@entity_field.manager'
    entity_type.listener:
    class: Drupal\Core\Entity\EntityTypeListener
    ......@@ -1480,6 +1488,8 @@ services:
    plugin.manager.element_info:
    class: Drupal\Core\Render\ElementInfoManager
    arguments: ['@container.namespaces', '@cache.discovery', '@theme_handler', '@module_handler', '@theme.manager']
    tags:
    - { name: cache_prewarmable }
    Drupal\Core\Render\ElementInfoManagerInterface: '@plugin.manager.element_info'
    stream_wrapper_manager:
    class: Drupal\Core\StreamWrapper\StreamWrapperManager
    ......
    ......@@ -706,7 +706,18 @@ public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TR
    $this->initializeSettings($request);
    $this->boot();
    }
    $response = $this->getHttpKernel()->handle($request, $type, $catch);
    // Wrap request handling in a Fiber, this allows us to call the cache
    // prewarming service if any code tries to suspend a fiber.
    $fiber = new \Fiber(fn() => $this->getHttpKernel()->handle($request, $type, $catch));
    $fiber->start();
    while ($fiber->isSuspended()) {
    $this->container->get('cache_prewarmer')->preWarmOneCache();
    $fiber->resume();
    }
    // If the fiber isn't suspended, it's either terminated or an exception
    // has been thrown, so it should be safe to get the return value without
    // explicitly checking \Fiber::isTerminated().
    $response = $fiber->getReturn();
    }
    catch (\Exception $e) {
    if ($catch === FALSE) {
    ......
    ......@@ -10,6 +10,7 @@
    use Drupal\Core\Field\FieldDefinition;
    use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
    use Drupal\Core\Language\LanguageManagerInterface;
    use Drupal\Core\PreWarm\PreWarmableInterface;
    use Drupal\Core\StringTranslation\StringTranslationTrait;
    use Drupal\Core\TypedData\TypedDataManagerInterface;
    ......@@ -19,7 +20,7 @@
    * This includes field definitions, base field definitions, and field storage
    * definitions.
    */
    class EntityFieldManager implements EntityFieldManagerInterface {
    class EntityFieldManager implements EntityFieldManagerInterface, PreWarmableInterface {
    use UseCacheBackendTrait;
    use StringTranslationTrait;
    ......@@ -693,4 +694,11 @@ protected function loadExtraFields(): array {
    return $extra;
    }
    /**
    * {@inheritdoc}
    */
    public function preWarm(): void {
    $this->getFieldMap();
    }
    }
    <?php
    namespace Drupal\Core\Plugin;
    /**
    * Provides a trait for Drupal\Core\PreWarm\PreWarmableInterface.
    *
    * @phpstan-require-implements \Drupal\Component\Plugin\Discovery\DiscoveryInterface
    * @phpstan-require-implements \Drupal\Core\PreWarm\PreWarmableInterface
    */
    trait PreWarmablePluginManagerTrait {
    /**
    * Implements \Drupal\Core\PreWarm\PreWarmableInterface.
    */
    public function preWarm(): void {
    $this->getDefinitions();
    }
    }
    <?php
    namespace Drupal\Core\PreWarm;
    use Drupal\Core\DependencyInjection\ClassResolverInterface;
    // cspell:ignore ABCDEF FCDABE BEDAFC
    /**
    * Prewarms caches for services that implement PreWarmableInterface.
    *
    * Takes a list of prewarmable services and prewarms them at random.
    * Randomization is used because whenever two or more requests are building
    * caches, the most benefit is gained by minimizing duplication. For example
    * two requests rely on the same six services but these services are requested
    * at different times, one request builds caches for the other and vice versa.
    *
    * No randomization:
    *
    * ABCDEF
    * ABCDEF
    *
    * Randomization:
    *
    * ABCDEF
    * FCDABE
    *
    * Randomization and three requests:
    *
    * ABCDEF
    * FCDABE
    * BEDAFC
    *
    * @see Drupal\Core\PreWarm\PreWarmableInterface
    * @see Drupal\Core\DrupalKernel::handle()
    * @see Drupal\Core\LockBackendAbstract::wait()
    * @see Drupal\Core\Routing\RouteProvider::preLoadRoutes()
    */
    class CachePreWarmer implements CachePreWarmerInterface {
    /**
    * Whether to prewarm caches at the end of the request.
    */
    protected bool $needsPreWarming = FALSE;
    /**
    * Called services.
    *
    * A list of services we have already prewarmed, so they can be skipped on
    * subsequent calls.
    *
    * @var string[]
    */
    protected array $calledServices = [];
    public function __construct(
    protected readonly ClassResolverInterface $classResolver,
    protected readonly array $serviceIds,
    ) {}
    /**
    * {@inheritdoc}
    */
    public function preWarmOneCache(): void {
    $candidates = array_diff($this->serviceIds, $this->calledServices);
    // If we've tried to prewarm all the available services, don't try to do it
    // again. We're most likely to hit this case if a request comes in late
    // during a stampede and everything was warmed up just before we reached
    // here.
    if ($candidates) {
    // Pick a prewarmable service to prewarm the cache for at random.
    $key = array_rand($candidates);
    $this->calledServices[] = $key;
    $service = $this->classResolver->getInstanceFromDefinition($this->serviceIds[$key]);
    $service->preWarm();
    }
    }
    /**
    * {@inheritdoc}
    */
    public function preWarmAllCaches(): void {
    $candidates = $this->serviceIds;
    shuffle($candidates);
    while ($candidates) {
    $key = key($candidates);
    $service = $this->classResolver->getInstanceFromDefinition($candidates[$key]);
    unset($candidates[$key]);
    $service->preWarm();
    }
    }
    }
    <?php
    namespace Drupal\Core\PreWarm;
    /**
    * Interface for cache prewarmers.
    *
    * Drupal has multiple registries that are fairly expensive to build: plugins,
    * theme hooks etc. These registries are required to serve most requests, and
    * therefore are in the critical path. When the cache for one of them is empty,
    * it is likely that the rest are too, usually due to a deployment.
    *
    * After a full cache clear on a high traffic site, a cache stampede may occur,
    * where multiple simultaneous requests all hit the site before caches have been
    * built. This either results in the same expensive cache item being built
    * multiple times, or in requests being caught in a lock wait pattern while
    * others build them, if this has been implemented (e.g. router rebuilds). In
    * the worst cases, it can take several seconds before any pages can be served
    * at all, meanwhile more requests are coming in, affecting both server loads
    * and concurrent request limits.
    *
    * The cache prewarm API attempts to mitigate this situation significantly.
    * Except for via the lock system, Drupal can't detect that it's in a cache
    * stampede situation itself, but there are particular caches we can assume that
    * if they're empty, then we might be. On most sites, even in a stampede
    * situation, these caches have to be built sequentially, i.e. the router has to
    * exist before a controller can be rendered, Views plugins have to be available
    * for a Views query to run, entity/field caches have to be built before
    * entities can be rendered, theme and element info caches have to be built
    * before templates can be rendered. Very few requests will try to render
    * a template without first running routing, even if some minor details will be
    * different between different routes and sites.
    *
    * To reduce duplicate work, and to enable those first pages after a cache clear
    * to be served faster, we want to divide up cache building between different
    * requests that are coming in. This is achieved by the cache_prewarmable
    * service tag and Drupal\Core\PreWarm\PreWarmableInterface* where any service
    * can define itself as prewarmable with a common method to call to warm caches.
    *
    * By default, prewarming is triggered when DrupalKernel::handle() reaches
    * a Fiber::suspend() call. A service can call Fiber::suspend() either when it
    * detects a cache miss in the critical path, for example
    * Drupal\Core\Routing\RouteProvider::preLoadRoutes(), or because it is about to
    * execute an async i/o operation. In either case, this allows the caller to
    * execute some different code, either a different callback in a Fiber, or in
    * the case of DrupalKernel::handle(), this prewarming service.
    *
    * The default implementation takes the list of prewarmable services, and picks
    * one at random. By choosing the service at random, it increases the likelihood
    * that when multiple requests all try to prewarm at the same time, that they'll
    * try to prewarm different things. If we always chose the service to prewarm
    * sequentially, we could end up reproducing the cache stampede situation.
    *
    * @see Drupal\Core\PreWarm\PreWarmableInterface
    * @see Drupal\Core\DrupalKernel::handle()
    * @see Drupal\Core\LockBackendAbstract::wait()
    * @see Drupal\Core\Routing\RouteProvider::preload()
    */
    interface CachePreWarmerInterface {
    /**
    * Prewarms one PreWarmable service.
    */
    public function preWarmOneCache(): void;
    /**
    * Prewarms all PreWarmable services.
    */
    public function preWarmAllCaches(): void;
    }
    <?php
    namespace Drupal\Core\PreWarm;
    /**
    * Interface for services with prewarmable caches.
    *
    * This interface should be implemented alongside the cache_prewarmable
    * service tag.
    *
    * You should consider carefully whether your service will benefit from
    * implementing this interface, it should only be used when:
    * 1. Your service has an expensive cache rebuild.
    * 2. Your service is in the critical path of most requests to the site and is
    * likely to be impacted by a cache stampede. If it's mainly used on cron or
    * admin pages, then prewarming would be counter-productive.
    * Additionally note that there is no guaranteed code path by which your service
    * will be called, so it can not (for example) assume that routing has been
    * completed. You should either ensure that you can prewarm your cache without
    * knowing the route or current theme, or return early if these aren't
    * available. You should also ensure that if your ::preWarm() method is called
    * early in a request, that later requests to your service retrieve the cached
    * information from memory rather than requesting it from the cache bin again.
    *
    * @see Drupal\Core\Prewarm\PreWarmerInterface
    */
    interface PreWarmableInterface {
    /**
    * Build any cache item or items that this service relies on.
    */
    public function preWarm(): void;
    }
    ......@@ -5,7 +5,9 @@
    use Drupal\Core\Cache\CacheBackendInterface;
    use Drupal\Core\Extension\ModuleHandlerInterface;
    use Drupal\Core\Extension\ThemeHandlerInterface;
    use Drupal\Core\Plugin\PreWarmablePluginManagerTrait;
    use Drupal\Core\Plugin\DefaultPluginManager;
    use Drupal\Core\PreWarm\PreWarmableInterface;
    use Drupal\Core\Render\Attribute\RenderElement;
    use Drupal\Core\Render\Element\FormElementInterface;
    use Drupal\Core\Theme\ThemeManagerInterface;
    ......@@ -21,7 +23,9 @@
    * @see \Drupal\Core\Render\Element\FormElementInterface
    * @see plugin_api
    */
    class ElementInfoManager extends DefaultPluginManager implements ElementInfoManagerInterface {
    class ElementInfoManager extends DefaultPluginManager implements ElementInfoManagerInterface, PreWarmableInterface {
    use PreWarmablePluginManagerTrait;
    /**
    * Stores the available element information.
    ......
    ......@@ -443,6 +443,10 @@ prerendered
    presave
    pretransaction
    preuninstall
    prewarmable
    prewarmables
    prewarmer
    prewarmers
    proname
    prophesize
    prophesized
    ......@@ -663,6 +667,11 @@ versionable
    versionless
    vfsstream
    viewports
    <<<<<<< HEAD
    vocabs
    warmable
    =======
    >>>>>>> 11.x
    wcag
    webcal
    webflo
    ......
    ......@@ -7,6 +7,8 @@
    use Drupal\Core\Cache\CacheBackendInterface;
    use Drupal\Core\Extension\ModuleHandlerInterface;
    use Drupal\Core\Plugin\DefaultPluginManager;
    use Drupal\Core\Plugin\PreWarmablePluginManagerTrait;
    use Drupal\Core\PreWarm\PreWarmableInterface;
    use Drupal\views\Plugin\views\ViewsHandlerInterface;
    use Drupal\views\ViewsData;
    use Symfony\Component\DependencyInjection\Container;
    ......@@ -15,7 +17,9 @@
    /**
    * Plugin type manager for all views handlers.
    */
    class ViewsHandlerManager extends DefaultPluginManager implements FallbackPluginManagerInterface {
    class ViewsHandlerManager extends DefaultPluginManager implements FallbackPluginManagerInterface, PreWarmableInterface {
    use PreWarmablePluginManagerTrait;
    /**
    * The views data cache.
    ......
    ......@@ -6,6 +6,8 @@
    use Drupal\Core\Cache\CacheBackendInterface;
    use Drupal\Core\Extension\ModuleHandlerInterface;
    use Drupal\Core\Plugin\DefaultPluginManager;
    use Drupal\Core\Plugin\PreWarmablePluginManagerTrait;
    use Drupal\Core\PreWarm\PreWarmableInterface;
    use Symfony\Component\DependencyInjection\Container;
    /**
    ......@@ -13,7 +15,9 @@
    *
    * @ingroup views_plugins
    */
    class ViewsPluginManager extends DefaultPluginManager {
    class ViewsPluginManager extends DefaultPluginManager implements PreWarmableInterface {
    use PreWarmablePluginManagerTrait;
    /**
    * Constructs a ViewsPluginManager object.
    ......
    ......@@ -4,60 +4,98 @@ services:
    plugin.manager.views.access:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [access, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.area:
    class: Drupal\views\Plugin\ViewsHandlerManager
    arguments: [area, '@container.namespaces', '@views.views_data', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.argument:
    class: Drupal\views\Plugin\ViewsHandlerManager
    arguments: [argument, '@container.namespaces', '@views.views_data', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.argument_default:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [argument_default, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.argument_validator:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [argument_validator, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.cache:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [cache, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.display_extender:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [display_extender, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.display:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [display, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.exposed_form:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [exposed_form, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.field:
    class: Drupal\views\Plugin\ViewsHandlerManager
    arguments: [field, '@container.namespaces', '@views.views_data', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.filter:
    class: Drupal\views\Plugin\ViewsHandlerManager
    arguments: [filter, '@container.namespaces', '@views.views_data', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.join:
    class: Drupal\views\Plugin\ViewsHandlerManager
    arguments: [join, '@container.namespaces', '@views.views_data', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.pager:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [pager, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.query:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [query, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.relationship:
    class: Drupal\views\Plugin\ViewsHandlerManager
    arguments: [relationship, '@container.namespaces', '@views.views_data', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.row:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [row, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.sort:
    class: Drupal\views\Plugin\ViewsHandlerManager
    arguments: [sort, '@container.namespaces', '@views.views_data', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.style:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [style, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    plugin.manager.views.wizard:
    class: Drupal\views\Plugin\ViewsPluginManager
    arguments: [wizard, '@container.namespaces', '@cache.discovery', '@module_handler']
    tags:
    - { name: cache_prewarmable }
    views.views_data:
    class: Drupal\views\ViewsData
    arguments: ['@cache.default', '@module_handler', '@language_manager']
    ......
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment