Skip to content

feat(http_cache): improve xkey purger implementation #4695

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

Merged
merged 5 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
* A new configuration is available to keep old services (IriConverter, IdentifiersExtractor and OpenApiFactory) `metadata_backward_compatibility_layer` (defaults to false) (#4351)
* Add support for `security_post_validation` attribute
* Mark the GraphQL subsystem as stable (#4500)
* feat(test): add `Client::loginUser()`
* feat(test): add `Client::loginUser()` (#4588)
* feat(http_cache): use symfony/http-client instead of guzzlehttp/guzzle, `ApiPlatform\Core\HttpCache\PurgerInterface` is deprecated in favor of `ApiPlatform\HttpCache\PurgerInterface`, new purger that uses PURGE (#4695)

## 2.6.8

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@
"suggest": {
"doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.",
"elasticsearch/elasticsearch": "To support Elasticsearch.",
"guzzlehttp/guzzle": "To use the HTTP cache invalidation system.",
"ocramius/package-versions": "To display the API Platform's version in the debug bar.",
"phpdocumentor/reflection-docblock": "To support extracting metadata from PHPDoc.",
"psr/cache-implementation": "To use metadata caching.",
Expand All @@ -106,6 +105,7 @@
"symfony/cache": "To have metadata caching when using Symfony integration.",
"symfony/config": "To load XML configuration files.",
"symfony/expression-language": "To use authorization features.",
"symfony/http-client": "To use the HTTP cache invalidation system.",
"symfony/security": "To use authorization features.",
"symfony/twig-bundle": "To use the Swagger UI integration.",
"symfony/uid": "To support Symfony UUID/ULID identifiers.",
Expand Down
18 changes: 0 additions & 18 deletions features/http_cache/tags.feature
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,12 @@ Feature: Cache invalidation through HTTP Cache tags
"""
Then the response status code should be 201
And the header "Cache-Tags" should not exist
And the header "xkey" should not exist
And "/relation_embedders,/related_dummies,/third_levels" IRIs should be purged
And "/relation_embedders /related_dummies /third_levels" IRIs should be purged with xkey

Scenario: Tags must be set for items
When I send a "GET" request to "/relation_embedders/1"
Then the response status code should be 200
And the header "Cache-Tags" should be equal to "/relation_embedders/1,/related_dummies/1,/third_levels/1"
And the header "xkey" should be equal to "/relation_embedders/1 /related_dummies/1 /third_levels/1"

Scenario: Create some more resources
When I add "Content-Type" header equal to "application/ld+json"
Expand All @@ -41,13 +38,11 @@ Feature: Cache invalidation through HTTP Cache tags
"""
Then the response status code should be 201
And the header "Cache-Tags" should not exist
And the header "xkey" should not exist

Scenario: Tags must be set for collections
When I send a "GET" request to "/relation_embedders"
Then the response status code should be 200
And the header "Cache-Tags" should be equal to "/relation_embedders/1,/related_dummies/1,/third_levels/1,/relation_embedders/2,/related_dummies/2,/third_levels/2,/relation_embedders"
And the header "xkey" should be equal to "/relation_embedders/1 /related_dummies/1 /third_levels/1 /relation_embedders/2 /related_dummies/2 /third_levels/2 /relation_embedders"

Scenario: Purge item on update
When I add "Content-Type" header equal to "application/ld+json"
Expand All @@ -59,18 +54,14 @@ Feature: Cache invalidation through HTTP Cache tags
"""
Then the response status code should be 200
And the header "Cache-Tags" should not exist
And the header "xkey" should not exist
And "/relation_embedders,/relation_embedders/1,/related_dummies/1" IRIs should be purged
And "/relation_embedders /relation_embedders/1 /related_dummies/1" IRIs should be purged with xkey

Scenario: Purge item and the related collection on update
When I add "Content-Type" header equal to "application/ld+json"
And I send a "DELETE" request to "/relation_embedders/1"
Then the response status code should be 204
And the header "Cache-Tags" should not exist
And the header "xkey" should not exist
And "/relation_embedders,/relation_embedders/1,/related_dummies/1" IRIs should be purged
And "/relation_embedders /relation_embedders/1 /related_dummies/1" IRIs should be purged with xkey

Scenario: Create two Relation2
When I add "Content-Type" header equal to "application/ld+json"
Expand All @@ -90,7 +81,6 @@ Feature: Cache invalidation through HTTP Cache tags
Scenario: Embedded collection must be listed in cache tags
When I send a "GET" request to "/relation2s/1"
Then the header "Cache-Tags" should be equal to "/relation2s/1"
Then the header "xkey" should be equal to "/relation2s/1"

Scenario: Create a Relation1
When I add "Content-Type" header equal to "application/ld+json"
Expand All @@ -102,7 +92,6 @@ Feature: Cache invalidation through HTTP Cache tags
"""
Then the response status code should be 201
And "/relation1s,/relation2s/1" IRIs should be purged
And "/relation1s /relation2s/1" IRIs should be purged with xkey

Scenario: Update a Relation1
When I add "Content-Type" header equal to "application/ld+json"
Expand All @@ -114,7 +103,6 @@ Feature: Cache invalidation through HTTP Cache tags
"""
Then the response status code should be 200
And "/relation1s,/relation1s/1,/relation2s/2,/relation2s/1" IRIs should be purged
And "/relation1s /relation1s/1 /relation2s/2 /relation2s/1" IRIs should be purged with xkey

Scenario: Create a Relation3 with many to many
When I add "Content-Type" header equal to "application/ld+json"
Expand All @@ -126,14 +114,12 @@ Feature: Cache invalidation through HTTP Cache tags
"""
Then the response status code should be 201
And "/relation3s,/relation2s/1,/relation2s/2" IRIs should be purged
And "/relation3s /relation2s/1 /relation2s/2" IRIs should be purged with xkey

Scenario: Get a Relation3
When I add "Content-Type" header equal to "application/ld+json"
And I send a "GET" request to "/relation3s"
Then the response status code should be 200
And the header "Cache-Tags" should be equal to "/relation3s/1,/relation2s/1,/relation2s/2,/relation3s"
And the header "xkey" should be equal to "/relation3s/1 /relation2s/1 /relation2s/2 /relation3s"

Scenario: Update a collection member only
When I add "Content-Type" header equal to "application/ld+json"
Expand All @@ -145,16 +131,12 @@ Feature: Cache invalidation through HTTP Cache tags
"""
Then the response status code should be 200
And the header "Cache-Tags" should not exist
And the header "xkey" should not exist
And "/relation3s,/relation3s/1,/relation2s/2,/relation2s,/relation2s/1" IRIs should be purged
And "/relation3s /relation3s/1 /relation2s/2 /relation2s /relation2s/1" IRIs should be purged with xkey

Scenario: Delete the collection owner
When I add "Content-Type" header equal to "application/ld+json"
And I send a "DELETE" request to "/relation3s/1"
Then the response status code should be 204
And the header "Cache-Tags" should not exist
And the header "xkey" should not exist
And "/relation3s,/relation3s/1,/relation2s/2" IRIs should be purged
And "/relation3s /relation3s/1 /relation2s/2" IRIs should be purged with xkey

31 changes: 31 additions & 0 deletions src/Core/HttpCache/PurgerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\HttpCache;

/**
* Purges resources from the cache.
*
* @author Kévin Dunglas <[email protected]>
*
* @experimental
*/
interface PurgerInterface
{
/**
* Purges all responses containing the given resources from the cache.
*
* @param string[] $iris
*/
public function purge(array $iris): void;
}
16 changes: 2 additions & 14 deletions src/Doctrine/EventListener/PurgeHttpCacheListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,8 @@ final class PurgeHttpCacheListener
private $resourceClassResolver;
private $propertyAccessor;
private $tags = [];
private $xKeyPurger;
private $xkeyEnabled;
private $httpTagsEnabled;

public function __construct(PurgerInterface $purger, $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, PurgerInterface $xKeyPurger = null, bool $xkeyEnabled = false, bool $httpTagsEnabled = true)
public function __construct(PurgerInterface $purger, $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null)
{
$this->purger = $purger;
$this->iriConverter = $iriConverter;
Expand All @@ -56,9 +53,6 @@ public function __construct(PurgerInterface $purger, $iriConverter, ResourceClas

$this->resourceClassResolver = $resourceClassResolver;
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
$this->xKeyPurger = $xKeyPurger;
$this->xkeyEnabled = $xkeyEnabled;
$this->httpTagsEnabled = $httpTagsEnabled;
}

/**
Expand Down Expand Up @@ -115,13 +109,7 @@ public function postFlush(): void
return;
}

if ($this->httpTagsEnabled) {
$this->purger->purge(array_values($this->tags));
}

if ($this->xkeyEnabled && $this->xKeyPurger) {
$this->xKeyPurger->purge(array_values($this->tags));
}
$this->purger->purge(array_values($this->tags));

$this->tags = [];
}
Expand Down
29 changes: 19 additions & 10 deletions src/HttpCache/EventListener/AddTagsListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Api\UrlGeneratorInterface;
use ApiPlatform\Core\HttpCache\PurgerInterface as LegacyPurgerInterface;
use ApiPlatform\HttpCache\PurgerInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\State\UriVariablesResolverTrait;
use ApiPlatform\Util\OperationRequestInitiatorTrait;
Expand All @@ -41,17 +43,19 @@ final class AddTagsListener
use UriVariablesResolverTrait;

private $iriConverter;
private $xkeyEnabled;
private $xkeyGlue;
private $httpTagsEnabled;
private $purger;

public function __construct($iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, bool $xkeyEnabled = false, string $xkeyGlue = ' ', bool $httpTagsEnabled = true)
/**
* @var LegacyPurgerInterface|PurgerInterface
*
* @param mixed $iriConverter
* @param mixed|null $purger
*/
public function __construct($iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, $purger = null)
{
$this->iriConverter = $iriConverter;
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->xkeyEnabled = $xkeyEnabled;
$this->xkeyGlue = $xkeyGlue;
$this->httpTagsEnabled = $httpTagsEnabled;
$this->purger = $purger;
}

/**
Expand Down Expand Up @@ -83,12 +87,17 @@ public function onKernelResponse(ResponseEvent $event): void
return;
}

if ($this->httpTagsEnabled) {
if ($this->purger instanceof LegacyPurgerInterface || !$this->purger) {
$response->headers->set('Cache-Tags', implode(',', $resources));
trigger_deprecation('api-platform/core', '2.7', sprintf('The interface "%s" is deprecated, use "%s" instead.', LegacyPurgerInterface::class, PurgerInterface::class));

return;
}

if ($this->xkeyEnabled) {
$response->headers->set('xkey', implode($this->xkeyGlue, $resources));
$headers = $this->purger->getResponseHeaders($resources);

foreach ($headers as $key => $value) {
$response->headers->set($key, $value);
}
}
}
7 changes: 7 additions & 0 deletions src/HttpCache/PurgerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,11 @@ interface PurgerInterface
* @param string[] $iris
*/
public function purge(array $iris);

/**
* Get the response header containing purged tags.
*
* @param string[] $iris
*/
public function getResponseHeaders(array $iris): array;
}
12 changes: 10 additions & 2 deletions src/HttpCache/VarnishPurger.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace ApiPlatform\HttpCache;

use GuzzleHttp\ClientInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* Purges Varnish.
Expand All @@ -31,7 +31,7 @@ final class VarnishPurger implements PurgerInterface
private $maxHeaderLength;

/**
* @param ClientInterface[] $clients
* @param HttpClientInterface[] $clients
*/
public function __construct(array $clients, int $maxHeaderLength = self::DEFAULT_VARNISH_MAX_HEADER_LENGTH)
{
Expand Down Expand Up @@ -83,6 +83,14 @@ public function purge(array $iris)
}
}

/**
* {@inheritdoc}
*/
public function getResponseHeaders(array $iris): array
{
return ['Cache-Tags' => implode(',', $iris)];
}

private function purgeRequest(array $iris)
{
// Create the regex to purge all tags in just one request
Expand Down
12 changes: 10 additions & 2 deletions src/HttpCache/VarnishXKeyPurger.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace ApiPlatform\HttpCache;

use GuzzleHttp\ClientInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* Purges Varnish XKey.
Expand All @@ -31,7 +31,7 @@ final class VarnishXKeyPurger implements PurgerInterface
private $xkeyGlue;

/**
* @param ClientInterface[] $clients
* @param HttpClientInterface[] $clients
*/
public function __construct(array $clients, int $maxHeaderLength = self::VARNISH_MAX_HEADER_LENGTH, string $xkeyGlue = ' ')
{
Expand All @@ -56,6 +56,14 @@ public function purge(array $iris)
}
}

/**
* {@inheritdoc}
*/
public function getResponseHeaders(array $iris): array
{
return ['xkey' => implode($this->xkeyGlue, $iris)];
}

private function purgeIris(array $iris): void
{
foreach ($this->chunkKeys($iris) as $keys) {
Expand Down
Loading