You are here

class AcquiaCloudPurger in Acquia Purge 8

Acquia Cloud.

Plugin annotation


@PurgePurger(
  id = "acquia_purge",
  label = @Translation("Acquia Cloud"),
  configform = "",
  cooldown_time = 0.2,
  description = @Translation("Invalidate content from Acquia Cloud."),
  multi_instance = FALSE,
  types = {"url", "wildcardurl", "tag", "everything"},
)

Hierarchy

Expanded class hierarchy of AcquiaCloudPurger

File

src/Plugin/Purge/Purger/AcquiaCloudPurger.php, line 28

Namespace

Drupal\acquia_purge\Plugin\Purge\Purger
View source
class AcquiaCloudPurger extends PurgerBase implements DebuggerAwareInterface, PurgerInterface {
  use DebuggerAwareTrait;

  /**
   * Maximum number of requests to send concurrently.
   */
  const CONCURRENCY = 6;

  /**
   * Float: the number of seconds to wait while trying to connect to a server.
   */
  const CONNECT_TIMEOUT = 1.5;

  /**
   * Float: the timeout of the request in seconds.
   */
  const TIMEOUT = 3.0;

  /**
   * Groups of tags per request.
   *
   * Batches of cache tags are split up into multiple requests to prevent HTTP
   * request headers from growing too large or Varnish refusing to process them.
   */
  const TAGS_GROUPED_BY = 15;

  /**
   * Information object interfacing with the Acquia platform.
   *
   * @var \Drupal\acquia_purge\AcquiaCloud\PlatformInfoInterface
   */
  protected $platformInfo;

  /**
   * The Guzzle HTTP client.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;

  /**
   * Constructs a AcquiaCloudPurger object.
   *
   * @param \Drupal\acquia_purge\AcquiaCloud\PlatformInfoInterface $acquia_purge_platforminfo
   *   Information object interfacing with the Acquia platform.
   * @param \GuzzleHttp\ClientInterface $http_client
   *   An HTTP client that can perform remote requests.
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   */
  public final function __construct(PlatformInfoInterface $acquia_purge_platforminfo, ClientInterface $http_client, array $configuration, $plugin_id, $plugin_definition) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->platformInfo = $acquia_purge_platforminfo;
    $this->httpClient = $http_client;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static($container
      ->get('acquia_purge.platforminfo'), $container
      ->get('http_client'), $configuration, $plugin_id, $plugin_definition);
  }

  /**
   * Retrieve request options used for all of Acquia Purge's balancer requests.
   *
   * @param array[] $extra
   *   Associative array of options to merge onto the standard ones.
   *
   * @return mixed[]
   *   Guzzle option array.
   */
  protected function getGlobalOptions(array $extra = []) {
    $opt = [
      // Disable exceptions for 4XX HTTP responses, those aren't failures to us.
      'http_errors' => FALSE,
      // Prevent inactive balancers from sucking all runtime up.
      'connect_timeout' => self::CONNECT_TIMEOUT,
      // Prevent unresponsive balancers from making Drupal slow.
      'timeout' => self::TIMEOUT,
      // Deliberately disable SSL verification to prevent unsigned certificates
      // from breaking down a website when purging a https:// URL!
      'verify' => FALSE,
      // Trigger \Drupal\acquia_purge\Http\AcquiaCloudBalancerMiddleware which
      // inspects Acquia Cloud responses and throws exceptions for failures.
      'acquia_purge_balancer_middleware' => TRUE,
    ];

    // Trigger the debugging middleware when Purge's debug mode is enabled.
    if ($this
      ->debugger()
      ->enabled()) {
      $opt['acquia_purge_debugger'] = $this
        ->debugger();
    }
    return array_merge($opt, $extra);
  }

  /**
   * Concurrently execute the given requests.
   *
   * @param \Closure $requests
   *   Generator yielding requests which will be passed to \GuzzleHttp\Pool.
   */
  protected function getResultsConcurrently(\Closure $requests) {
    $this
      ->debugger()
      ->callerAdd(__METHOD__);
    $results = [];

    // Create a concurrently executed Pool which collects a boolean per request.
    $pool = new Pool($this->httpClient, $requests(), [
      'options' => $this
        ->getGlobalOptions(),
      'concurrency' => self::CONCURRENCY,
      'fulfilled' => function ($response, $result_id) use (&$results) {
        $this
          ->debugger()
          ->callerAdd(__METHOD__ . '::fulfilled');
        $results[$result_id][] = TRUE;
        $this
          ->debugger()
          ->callerRemove(__METHOD__ . '::fulfilled');
      },
      'rejected' => function ($exception, $result_id) use (&$results) {
        $this
          ->debugger()
          ->callerAdd(__METHOD__ . '::rejected');
        $this
          ->debugger()
          ->logFailedRequest($exception);
        $results[$result_id][] = FALSE;
        $this
          ->debugger()
          ->callerRemove(__METHOD__ . '::rejected');
      },
    ]);

    // Initiate the transfers and create a promise.
    $promise = $pool
      ->promise();

    // Force the pool of requests to complete.
    $promise
      ->wait();
    $this
      ->debugger()
      ->callerRemove(__METHOD__);
    return $results;
  }

  /**
   * {@inheritdoc}
   */
  public function getIdealConditionsLimit() {

    // The max amount of outgoing HTTP requests that can be made during script
    // execution time. Although always respected as outer limit, it will be
    // lower in practice as PHP resource limits (max execution time) bring it
    // further down. However, the maximum amount of requests will be higher on
    // the CLI.
    $balancers = count($this->platformInfo
      ->getBalancerAddresses());
    if ($balancers) {
      return intval(ceil(200 / $balancers));
    }
    return 100;
  }

  /**
   * {@inheritdoc}
   */
  public function hasRuntimeMeasurement() {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function invalidate(array $invalidations) {

    // Since we implemented ::routeTypeToMethod(), this Latin preciousness
    // shouldn't ever occur and when it does, will be easily recognized.
    throw new \Exception("Malum consilium quod mutari non potest!");
  }

  /**
   * Invalidate a set of tag invalidations.
   *
   * @see \Drupal\purge\Plugin\Purge\Purger\PurgerInterface::invalidate()
   * @see \Drupal\purge\Plugin\Purge\Purger\PurgerInterface::routeTypeToMethod()
   */
  public function invalidateTags(array $invalidations) {
    $this
      ->debugger()
      ->callerAdd(__METHOD__);

    // Set invalidation states to PROCESSING. Detect tags with spaces in them,
    // as space is the only character Drupal core explicitely forbids in tags.
    foreach ($invalidations as $invalidation) {
      $tag = $invalidation
        ->getExpression();
      if (strpos($tag, ' ') !== FALSE) {
        $invalidation
          ->setState(InvalidationInterface::FAILED);
        $this->logger
          ->error("Tag '%tag' contains a space, this is forbidden.", [
          '%tag' => $tag,
        ]);
      }
      else {
        $invalidation
          ->setState(InvalidationInterface::PROCESSING);
      }
    }

    // Create grouped sets of 12 so that we can spread out the BAN load.
    $group = 0;
    $groups = [];
    foreach ($invalidations as $invalidation) {
      if ($invalidation
        ->getState() !== InvalidationInterface::PROCESSING) {
        continue;
      }
      if (!isset($groups[$group])) {
        $groups[$group] = [
          'tags' => [],
          [
            'objects' => [],
          ],
        ];
      }
      if (count($groups[$group]['tags']) >= self::TAGS_GROUPED_BY) {
        $group++;
      }
      $groups[$group]['objects'][] = $invalidation;
      $groups[$group]['tags'][] = $invalidation
        ->getExpression();
    }

    // Test if we have at least one group of tag(s) to purge, if not, bail.
    if (!count($groups)) {
      foreach ($invalidations as $invalidation) {
        $invalidation
          ->setState(InvalidationInterface::FAILED);
      }
      return;
    }

    // Now create requests for all groups of tags.
    $site = $this->platformInfo
      ->getSiteIdentifier();
    $ipv4_addresses = $this->platformInfo
      ->getBalancerAddresses();
    $requests = function () use ($groups, $ipv4_addresses, $site) {
      foreach ($groups as $group_id => $group) {
        $tags = new TagsHeaderValue($group['tags'], Hash::cacheTags($group['tags']));
        foreach ($ipv4_addresses as $ipv4) {
          (yield $group_id => function ($poolopt) use ($site, $tags, $ipv4) {
            $opt = [
              'headers' => [
                'X-Acquia-Purge' => $site,
                'X-Acquia-Purge-Tags' => $tags
                  ->__toString(),
                'Accept-Encoding' => 'gzip',
                'User-Agent' => 'Acquia Purge',
              ],
            ];

            // Pass the TagsHeaderValue to DebuggerMiddleware (when loaded).
            if ($this
              ->debugger()
              ->enabled()) {
              $opt['acquia_purge_tags'] = $tags;
            }
            if (is_array($poolopt) && count($poolopt)) {
              $opt = array_merge($poolopt, $opt);
            }
            return $this->httpClient
              ->requestAsync('BAN', "http://{$ipv4}/tags", $opt);
          });
        }
      }
    };

    // Execute the requests generator and retrieve the results.
    $results = $this
      ->getResultsConcurrently($requests);

    // Triage the results and set all invalidation states correspondingly.
    foreach ($groups as $group_id => $group) {
      if (!isset($results[$group_id]) || !count($results[$group_id])) {
        foreach ($group['objects'] as $invalidation) {
          $invalidation
            ->setState(InvalidationInterface::FAILED);
        }
      }
      else {
        if (in_array(FALSE, $results[$group_id])) {
          foreach ($group['objects'] as $invalidation) {
            $invalidation
              ->setState(InvalidationInterface::FAILED);
          }
        }
        else {
          foreach ($group['objects'] as $invalidation) {
            $invalidation
              ->setState(InvalidationInterface::SUCCEEDED);
          }
        }
      }
    }
    $this
      ->debugger()
      ->callerRemove(__METHOD__);
  }

  /**
   * Invalidate a set of URL invalidations.
   *
   * @see \Drupal\purge\Plugin\Purge\Purger\PurgerInterface::invalidate()
   * @see \Drupal\purge\Plugin\Purge\Purger\PurgerInterface::routeTypeToMethod()
   */
  public function invalidateUrls(array $invalidations) {
    $this
      ->debugger()
      ->callerAdd(__METHOD__);

    // Change all invalidation objects into the PROCESS state before kickoff.
    foreach ($invalidations as $inv) {
      $inv
        ->setState(InvalidationInterface::PROCESSING);
    }

    // Generate request objects for each balancer/invalidation combination.
    $ipv4_addresses = $this->platformInfo
      ->getBalancerAddresses();
    $token = $this->platformInfo
      ->getBalancerToken();
    $requests = function () use ($invalidations, $ipv4_addresses, $token) {
      foreach ($invalidations as $inv) {
        foreach ($ipv4_addresses as $ipv4) {
          (yield $inv
            ->getId() => function ($poolopt) use ($inv, $ipv4, $token) {
            $uri = $inv
              ->getExpression();
            $host = parse_url($uri, PHP_URL_HOST);
            $uri = str_replace($host, $ipv4, $uri);
            $opt = [
              'headers' => [
                'X-Acquia-Purge' => $token,
                'Accept-Encoding' => 'gzip',
                'User-Agent' => 'Acquia Purge',
                'Host' => $host,
              ],
            ];
            if (is_array($poolopt) && count($poolopt)) {
              $opt = array_merge($poolopt, $opt);
            }
            return $this->httpClient
              ->requestAsync('PURGE', $uri, $opt);
          });
        }
      }
    };

    // Execute the requests generator and retrieve the results.
    $results = $this
      ->getResultsConcurrently($requests);

    // Triage the results and set all invalidation states correspondingly.
    foreach ($invalidations as $invalidation) {
      $inv_id = $invalidation
        ->getId();
      if (!isset($results[$inv_id]) || !count($results[$inv_id])) {
        $invalidation
          ->setState(InvalidationInterface::FAILED);
      }
      else {
        if (in_array(FALSE, $results[$inv_id])) {
          $invalidation
            ->setState(InvalidationInterface::FAILED);
        }
        else {
          $invalidation
            ->setState(InvalidationInterface::SUCCEEDED);
        }
      }
    }
    $this
      ->debugger()
      ->callerRemove(__METHOD__);
  }

  /**
   * Invalidate URLs that contain the wildcard character "*".
   *
   * @see \Drupal\purge\Plugin\Purge\Purger\PurgerInterface::invalidate()
   * @see \Drupal\purge\Plugin\Purge\Purger\PurgerInterface::routeTypeToMethod()
   */
  public function invalidateWildcardUrls(array $invalidations) {
    $this
      ->debugger()
      ->callerAdd(__METHOD__);

    // Change all invalidation objects into the PROCESS state before kickoff.
    foreach ($invalidations as $inv) {
      $inv
        ->setState(InvalidationInterface::PROCESSING);
    }

    // Generate request objects for each balancer/invalidation combination.
    $ipv4_addresses = $this->platformInfo
      ->getBalancerAddresses();
    $token = $this->platformInfo
      ->getBalancerToken();
    $requests = function () use ($invalidations, $ipv4_addresses, $token) {
      foreach ($invalidations as $inv) {
        foreach ($ipv4_addresses as $ipv4) {
          (yield $inv
            ->getId() => function ($poolopt) use ($inv, $ipv4, $token) {
            $uri = str_replace('https://', 'http://', $inv
              ->getExpression());
            $host = parse_url($uri, PHP_URL_HOST);
            $uri = str_replace($host, $ipv4, $uri);
            $opt = [
              'headers' => [
                'X-Acquia-Purge' => $token,
                'Accept-Encoding' => 'gzip',
                'User-Agent' => 'Acquia Purge',
                'Host' => $host,
              ],
            ];
            if (is_array($poolopt) && count($poolopt)) {
              $opt = array_merge($poolopt, $opt);
            }
            return $this->httpClient
              ->requestAsync('BAN', $uri, $opt);
          });
        }
      }
    };

    // Execute the requests generator and retrieve the results.
    $results = $this
      ->getResultsConcurrently($requests);

    // Triage the results and set all invalidation states correspondingly.
    foreach ($invalidations as $invalidation) {
      $inv_id = $invalidation
        ->getId();
      if (!isset($results[$inv_id]) || !count($results[$inv_id])) {
        $invalidation
          ->setState(InvalidationInterface::FAILED);
      }
      else {
        if (in_array(FALSE, $results[$inv_id])) {
          $invalidation
            ->setState(InvalidationInterface::FAILED);
        }
        else {
          $invalidation
            ->setState(InvalidationInterface::SUCCEEDED);
        }
      }
    }
    $this
      ->debugger()
      ->callerRemove(__METHOD__);
  }

  /**
   * Invalidate the entire website.
   *
   * This supports invalidation objects of the type 'everything'. Because many
   * load balancers on Acquia Cloud host multiple websites (e.g. sites in a
   * multisite) this will only affect the current site instance. This works
   * because all Varnish-cached resources are tagged with a unique identifier
   * coming from platformInfo::getSiteIdentifier().
   *
   * @see \Drupal\purge\Plugin\Purge\Purger\PurgerInterface::invalidate()
   * @see \Drupal\purge\Plugin\Purge\Purger\PurgerInterface::routeTypeToMethod()
   */
  public function invalidateEverything(array $invalidations) {
    $this
      ->debugger()
      ->callerAdd(__METHOD__);

    // Set the 'everything' object(s) into processing mode.
    foreach ($invalidations as $invalidation) {
      $invalidation
        ->setState(InvalidationInterface::PROCESSING);
    }

    // Fetch the site identifier and start with a successive outcome.
    $overall_success = TRUE;

    // Synchronously request each balancer to wipe out everything for this site.
    $opt = $this
      ->getGlobalOptions();
    $opt['headers'] = [
      'X-Acquia-Purge' => $this->platformInfo
        ->getSiteIdentifier(),
      'Accept-Encoding' => 'gzip',
      'User-Agent' => 'Acquia Purge',
    ];
    foreach ($this->platformInfo
      ->getBalancerAddresses() as $ip_address) {
      try {
        $this->httpClient
          ->request('BAN', 'http://' . $ip_address . '/site', $opt);
      } catch (\Exception $e) {
        $this
          ->debugger()
          ->logFailedRequest($e);
        $overall_success = FALSE;
      }
    }

    // Set the object states according to our overall result.
    foreach ($invalidations as $invalidation) {
      if ($overall_success) {
        $invalidation
          ->setState(InvalidationInterface::SUCCEEDED);
      }
      else {
        $invalidation
          ->setState(InvalidationInterface::FAILED);
      }
    }
    $this
      ->debugger()
      ->callerRemove(__METHOD__);
  }

  /**
   * {@inheritdoc}
   */
  public function routeTypeToMethod($type) {
    $methods = [
      'tag' => 'invalidateTags',
      'url' => 'invalidateUrls',
      'wildcardurl' => 'invalidateWildcardUrls',
      'everything' => 'invalidateEverything',
    ];
    return isset($methods[$type]) ? $methods[$type] : 'invalidate';
  }

}

Members

Namesort descending Modifiers Type Description Overrides
AcquiaCloudPurger::$httpClient protected property The Guzzle HTTP client.
AcquiaCloudPurger::$platformInfo protected property Information object interfacing with the Acquia platform.
AcquiaCloudPurger::CONCURRENCY constant Maximum number of requests to send concurrently.
AcquiaCloudPurger::CONNECT_TIMEOUT constant Float: the number of seconds to wait while trying to connect to a server.
AcquiaCloudPurger::create public static function Creates an instance of the plugin. Overrides PurgerBase::create
AcquiaCloudPurger::getGlobalOptions protected function Retrieve request options used for all of Acquia Purge's balancer requests.
AcquiaCloudPurger::getIdealConditionsLimit public function Get the maximum number of invalidations that this purger can process. Overrides PurgerBase::getIdealConditionsLimit
AcquiaCloudPurger::getResultsConcurrently protected function Concurrently execute the given requests.
AcquiaCloudPurger::hasRuntimeMeasurement public function Indicates whether your purger utilizes dynamic runtime measurement. Overrides PurgerCapacityDataInterface::hasRuntimeMeasurement
AcquiaCloudPurger::invalidate public function Invalidate content from external caches. Overrides PurgerInterface::invalidate
AcquiaCloudPurger::invalidateEverything public function Invalidate the entire website.
AcquiaCloudPurger::invalidateTags public function Invalidate a set of tag invalidations.
AcquiaCloudPurger::invalidateUrls public function Invalidate a set of URL invalidations.
AcquiaCloudPurger::invalidateWildcardUrls public function Invalidate URLs that contain the wildcard character "*".
AcquiaCloudPurger::routeTypeToMethod public function Route certain type of invalidations to other methods. Overrides PurgerBase::routeTypeToMethod
AcquiaCloudPurger::TAGS_GROUPED_BY constant Groups of tags per request.
AcquiaCloudPurger::TIMEOUT constant Float: the timeout of the request in seconds.
AcquiaCloudPurger::__construct final public function Constructs a AcquiaCloudPurger object. Overrides PurgerBase::__construct
DebuggerAwareTrait::$debuggerInstance private property The debugger instance.
DebuggerAwareTrait::debugger public function
DebuggerAwareTrait::setDebugger public function
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 3
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PurgeLoggerAwareTrait::$logger protected property Channel logger.
PurgeLoggerAwareTrait::logger public function
PurgerBase::$id protected property Unique instance ID for this purger.
PurgerBase::$runtimeMeasurement protected property The runtime measurement counter.
PurgerBase::delete public function The current instance of this purger plugin is about to be deleted. Overrides PurgerInterface::delete 1
PurgerBase::getCooldownTime public function Get the time in seconds to wait after invalidation. Overrides PurgerCapacityDataInterface::getCooldownTime
PurgerBase::getId public function Retrieve the unique instance ID for this purger instance. Overrides PurgerInterface::getId
PurgerBase::getLabel public function Retrieve the user-readable label for this purger instance. Overrides PurgerInterface::getLabel
PurgerBase::getRuntimeMeasurement public function Get the runtime measurement counter. Overrides PurgerCapacityDataInterface::getRuntimeMeasurement
PurgerBase::getTimeHint public function Get the maximum number of seconds, processing a single invalidation takes. Overrides PurgerCapacityDataInterface::getTimeHint
PurgerBase::getTypes public function Retrieve the list of supported invalidation types. Overrides PurgerInterface::getTypes
PurgerBase::setRuntimeMeasurement public function Inject the runtime measurement counter. Overrides PurgerCapacityDataInterface::setRuntimeMeasurement
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.