Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Fastly support #451

Merged
merged 13 commits into from
Dec 9, 2019
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpC
### General

* Use `LegacyEventDispatcherProxy` for Symfony >= 4.3 to avoid deprecation messages.
* Added Fastly ProxyClient Adapter with ClearCapable, PurgeCapable, RefreshCapable, & TagCapable.
Fastly is a CDN originally based on Varnish 2.x, so with many of the same capabilities like VCL and more.

2.7.0
-----
Expand Down
40 changes: 40 additions & 0 deletions doc/proxy-clients.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This table provides of methods supported by each proxy client:
Client Purge Refresh Ban Tagging Clear
============= ======= ======= ======= ======= =======
Varnish ✓ ✓ ✓ ✓
Fastly ✓ ✓ ✓ ✓
NGINX ✓ ✓
Symfony Cache ✓ ✓ ✓ (1) ✓ (1)
Noop ✓ ✓ ✓ ✓
Expand Down Expand Up @@ -158,6 +159,44 @@ default ``xkey-softpurge``.
the ``ResponseTagger``, set it up with a
:ref:`custom TagHeaderFormatter <response_tagger_optional_parameters>`.

Fastly Client
~~~~~~~~~~~~~~

The Fastly client sends HTTP requests with the ``HttpDispatcher``. Create the
dispatcher as explained above and pass it to the Fastly client::

use FOS\HttpCache\ProxyClient\Fastly;

$varnish = new Fastly($httpDispatcher);
andrerom marked this conversation as resolved.
Show resolved Hide resolved

.. note::

Unlike other supported proxies there is no configuration needed for the proxy itself as all invalidation is done
against `Fastly Purge API`_. But for optimal use make sure to tune configuration together with Fastly.

You need to pass the following options to the Fastly client:

* ``service_identifier``: Identifier for your Fastly service account.
* ``authentication_token``: User token for authentication against Fastly APIs.
* NB: To be able to clear all cache(``->clear()``), you'll need a token for user with Fastly "Engineer permissions".
* ``soft_purge`` (default: true): Boolean for doing soft purges or not on tag & url purging.
Soft purges expires the cache unlike hard purge (removal), and allow grace/stale handling within Fastly VCL.

Additionally, you can specify the request factory used to build the
invalidation HTTP requests. If not specified, auto discovery is used - which
usually is fine.

A full example could look like this::

$options = [
'service_identifier' => '<my-app-identifier>',
'authentication_token' => '<user-authentication-token>',
'soft_purge' => false
];
$requestFactory = new MyRequestFactory();

$varnish = new Fastly($httpDispatcher, $options, $requestFactory);
andrerom marked this conversation as resolved.
Show resolved Hide resolved

NGINX Client
~~~~~~~~~~~~

Expand Down Expand Up @@ -339,3 +378,4 @@ requests.
.. _HTTPlug plugins: http://php-http.readthedocs.io/en/latest/plugins/index.html
.. _message factory and URI factory: http://php-http.readthedocs.io/en/latest/message/message-factory.html
.. _Toflar Psr6Store: https://github.com/Toflar/psr6-symfony-http-cache-store
.. _Fastly Purge API: https://docs.fastly.com/api/purge
182 changes: 182 additions & 0 deletions src/ProxyClient/Fastly.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?php

/*
* This file is part of the FOSHttpCache package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\HttpCache\ProxyClient;

use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable;
use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable;
use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable;
use FOS\HttpCache\ProxyClient\Invalidation\TagCapable;
use Symfony\Component\HttpFoundation\Request;

/**
* Fastly HTTP cache invalidator.
*
* Additional constructor options:
* - service_identifier Identifier for your Fastly service account.
* - authentication_token Token for authentication against Fastly APIs.
* For full capabilities (incl ClearCapable) you'll need one with Fastly Engineer permissions.
* - soft_purge Boolean for doing soft purges or not on tag invalidation and url purging, default true.
* Soft purges expires cache instead of hard purge, and allow grace/stale handling.
*
* @see https://docs.fastly.com/api/purge Fastly Purge API documentation.
*
* @author Simone Fumagalli <simone.fumagalli@musement.com>
*/
class Fastly extends HttpProxyClient implements ClearCapable, PurgeCapable, RefreshCapable, TagCapable
{
/**
* @internal
*/
const HTTP_METHOD_PURGE = 'PURGE';

/**
* @internal
*
* @see https://docs.fastly.com/api/purge#purge_db35b293f8a724717fcf25628d713583 Fastly's limit on batch tag purges.
*/
const TAG_BATCH_PURGE_LIMIT = 256;

/**
* @internal
*
* @see https://docs.fastly.com/api/purge Base url endpoint used on anything but url PURGE/GET/HEAD.
*/
const API_ENDPOINT = 'https://api.fastly.com';

/**
* {@inheritdoc}
*
* @see https://docs.fastly.com/api/purge#purge_db35b293f8a724717fcf25628d713583
*/
public function invalidateTags(array $tags)
{
$url = sprintf(self::API_ENDPOINT.'/service/%s/purge', $this->options['service_identifier']);
$headers = ['Accept' => 'application/json'];
if (true === $this->options['soft_purge']) {
$headers['Fastly-Soft-Purge'] = 1;
}

// Split tag invalidations across several requests within Fastly's tag batch invalidations limits.
foreach (\array_chunk($tags, self::TAG_BATCH_PURGE_LIMIT) as $tagChunk) {
$this->queueRequest(
Request::METHOD_POST,
$url,
$headers,
false,
json_encode(['surrogate_keys' => $tagChunk])
);
}

return $this;
}

/**
* {@inheritdoc}
*
* @see https://docs.fastly.com/api/purge#soft_purge_0c4f56f3d68e9bed44fb8b638b78ea36
* @see https://docs.fastly.com/guides/purging/authenticating-api-purge-requests#purging-urls-with-an-api-token
*/
public function purge($url, array $headers = [])
{
if (true === $this->options['soft_purge']) {
$headers['Fastly-Soft-Purge'] = 1;
}

$this->queueRequest(
self::HTTP_METHOD_PURGE,
$url,
$headers,
false
);

return $this;
}

/**
* {@inheritdoc}
*/
public function refresh($url, array $headers = [])
{
// First soft purge url
$this->queueRequest(
self::HTTP_METHOD_PURGE,
$url,
['Fastly-Soft-Purge' => 1] + $headers,
false
);

// Secondly make sure refresh is triggered with a HEAD request
$this->queueRequest(
Request::METHOD_HEAD,
$url,
$headers,
false
);

return $this;
}

/**
* {@inheritdoc}
*
* @see https://docs.fastly.com/api/purge#purge_bee5ed1a0cfd541e8b9f970a44718546
*
* Warning:
* - Does not support soft purge, for that use an "all" key.
* - Requires a API token of a user with at least Engineer permissions.
*/
public function clear()
{
$this->queueRequest(
Request::METHOD_POST,
sprintf(self::API_ENDPOINT.'/service/%s/purge_all', $this->options['service_identifier']),
['Accept' => 'application/json'],
false
);

return $this;
}

/**
* {@inheritdoc} Always provides default authentication token on "Fastly-Key" header.
*/
protected function queueRequest($method, $url, array $headers, $validateHost = true, $body = null)
{
parent::queueRequest(
$method,
$url,
$headers + ['Fastly-Key' => $this->options['authentication_token']],
$validateHost,
$body
);
}

/**
* {@inheritdoc}
*/
protected function configureOptions()
{
$resolver = parent::configureOptions();

$resolver->setRequired([
'authentication_token',
'service_identifier',
andrerom marked this conversation as resolved.
Show resolved Hide resolved
'soft_purge',
]);

$resolver->setDefaults([
'soft_purge' => true,
]);

return $resolver;
}
}
14 changes: 8 additions & 6 deletions src/ProxyClient/HttpProxyClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Http\Discovery\MessageFactoryDiscovery;
use Http\Message\RequestFactory;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

Expand Down Expand Up @@ -83,15 +84,16 @@ protected function configureOptions()
/**
* Create a request and queue it with the HTTP dispatcher.
*
* @param string $method
* @param string|UriInterface $url
* @param array $headers
* @param bool $validateHost see Dispatcher::invalidate
* @param string $method
* @param string|UriInterface $url
* @param array $headers
* @param bool $validateHost see Dispatcher::invalidate
* @param resource|string|StreamInterface|null $body
*/
protected function queueRequest($method, $url, array $headers, $validateHost = true)
protected function queueRequest($method, $url, array $headers, $validateHost = true, $body = null)
{
$this->httpDispatcher->invalidate(
$this->requestFactory->createRequest($method, $url, $headers),
$this->requestFactory->createRequest($method, $url, $headers, $body),
$validateHost
);
}
Expand Down
Loading