Skip to content
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
58 changes: 34 additions & 24 deletions app/code/Magento/CacheInvalidate/Model/PurgeCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,24 @@
namespace Magento\CacheInvalidate\Model;

use Magento\Framework\Cache\InvalidateLogger;
use Magento\PageCache\Model\Cache\Server;
use Laminas\Http\Client\Adapter\Socket;
use Laminas\Uri\Uri;

/**
* PurgeCache model
* Invalidate external HTTP cache(s) based on tag pattern
*/
class PurgeCache
{
const HEADER_X_MAGENTO_TAGS_PATTERN = 'X-Magento-Tags-Pattern';

/**
* @var \Magento\PageCache\Model\Cache\Server
* @var Server
*/
protected $cacheServer;

/**
* @var \Magento\CacheInvalidate\Model\SocketFactory
* @var SocketFactory
*/
protected $socketAdapterFactory;

Expand All @@ -39,39 +42,46 @@ class PurgeCache
*
* @var int
*/
private $requestSize = 7680;
private $maxHeaderSize;

/**
* Constructor
*
* @param \Magento\PageCache\Model\Cache\Server $cacheServer
* @param \Magento\CacheInvalidate\Model\SocketFactory $socketAdapterFactory
* @param Server $cacheServer
* @param SocketFactory $socketAdapterFactory
* @param InvalidateLogger $logger
* @param int $maxHeaderSize
*/
public function __construct(
\Magento\PageCache\Model\Cache\Server $cacheServer,
\Magento\CacheInvalidate\Model\SocketFactory $socketAdapterFactory,
InvalidateLogger $logger
Server $cacheServer,
SocketFactory $socketAdapterFactory,
InvalidateLogger $logger,
int $maxHeaderSize = 7680
) {
$this->cacheServer = $cacheServer;
$this->socketAdapterFactory = $socketAdapterFactory;
$this->logger = $logger;
$this->maxHeaderSize = $maxHeaderSize;
}

/**
* Send curl purge request to invalidate cache by tags pattern
*
* @param string $tagsPattern
* @param array|string $tags
* @return bool Return true if successful; otherwise return false
*/
public function sendPurgeRequest($tagsPattern)
public function sendPurgeRequest($tags)
{
if (is_string($tags)) {
$tags = [$tags];
}

$successful = true;
$socketAdapter = $this->socketAdapterFactory->create();
$servers = $this->cacheServer->getUris();
$socketAdapter->setOptions(['timeout' => 10]);

$formattedTagsChunks = $this->splitTags($tagsPattern);
$formattedTagsChunks = $this->chunkTags($tags);
foreach ($formattedTagsChunks as $formattedTagsChunk) {
if (!$this->sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk)) {
$successful = false;
Expand All @@ -82,24 +92,24 @@ public function sendPurgeRequest($tagsPattern)
}

/**
* Split tags by batches
* Split tags into batches to suit Varnish max. header size
*
* @param string $tagsPattern
* @param array $tags
* @return \Generator
*/
private function splitTags($tagsPattern)
private function chunkTags(array $tags): \Generator
{
$tagsBatchSize = 0;
$currentBatchSize = 0;
$formattedTagsChunk = [];
$formattedTags = explode('|', $tagsPattern);
foreach ($formattedTags as $formattedTag) {
if ($tagsBatchSize + strlen($formattedTag) > $this->requestSize - count($formattedTagsChunk) - 1) {
foreach ($tags as $formattedTag) {
// Check if (currentBatchSize + length of next tag + number of pipe delimiters) would exceed header size.
if ($currentBatchSize + strlen($formattedTag) + count($formattedTagsChunk) > $this->maxHeaderSize) {
yield implode('|', $formattedTagsChunk);
$formattedTagsChunk = [];
$tagsBatchSize = 0;
$currentBatchSize = 0;
}

$tagsBatchSize += strlen($formattedTag);
$currentBatchSize += strlen($formattedTag);
$formattedTagsChunk[] = $formattedTag;
}
if (!empty($formattedTagsChunk)) {
Expand All @@ -110,12 +120,12 @@ private function splitTags($tagsPattern)
/**
* Send curl purge request to servers to invalidate cache by tags pattern
*
* @param \Laminas\Http\Client\Adapter\Socket $socketAdapter
* @param \Laminas\Uri\Uri[] $servers
* @param Socket $socketAdapter
* @param Uri[] $servers
* @param string $formattedTagsChunk
* @return bool Return true if successful; otherwise return false
*/
private function sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk)
private function sendPurgeRequestToServers(Socket $socketAdapter, array $servers, string $formattedTagsChunk): bool
{
$headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk];
$unresponsiveServerError = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

use Magento\Framework\Event\ObserverInterface;

/**
* Clear configured Varnish hosts when triggering a full cache flush (e.g. from the Cache Management admin dashboard)
*/
class FlushAllCacheObserver implements ObserverInterface
{
/**
Expand Down Expand Up @@ -43,7 +46,7 @@ public function __construct(
public function execute(\Magento\Framework\Event\Observer $observer)
{
if ($this->config->getType() == \Magento\PageCache\Model\Config::VARNISH && $this->config->isEnabled()) {
$this->purgeCache->sendPurgeRequest('.*');
$this->purgeCache->sendPurgeRequest(['.*']);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public function execute(Observer $observer)
$tags[] = sprintf($pattern, $tag);
}
if (!empty($tags)) {
$this->purgeCache->sendPurgeRequest(implode('|', array_unique($tags)));
$this->purgeCache->sendPurgeRequest(array_unique($tags));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ protected function setUp(): void
'cacheServer' => $this->cacheServer,
'socketAdapterFactory' => $socketFactoryMock,
'logger' => $this->loggerMock,
'maxHeaderSize' => 256
]
);
}
Expand All @@ -64,6 +65,7 @@ protected function setUp(): void
public function testSendPurgeRequest($hosts)
{
$uris = [];
/** @var array $host */
foreach ($hosts as $host) {
$port = isset($host['port']) ? $host['port'] : Server::DEFAULT_PORT;
$uris[] = UriFactory::factory('')->setHost($host['host'])
Expand Down Expand Up @@ -92,7 +94,50 @@ public function testSendPurgeRequest($hosts)
$this->loggerMock->expects($this->once())
->method('execute');

$this->assertTrue($this->model->sendPurgeRequest('tags'));
$this->assertTrue($this->model->sendPurgeRequest(['tags']));
}

public function testSendMultiPurgeRequest()
{
$tags = [
'(^|,)cat_p_95(,|$)', '(^|,)cat_p_96(,|$)', '(^|,)cat_p_97(,|$)', '(^|,)cat_p_98(,|$)',
'(^|,)cat_p_99(,|$)', '(^|,)cat_p_100(,|$)', '(^|,)cat_p_10038(,|$)', '(^|,)cat_p_142985(,|$)',
'(^|,)cat_p_199(,|$)', '(^|,)cat_p_300(,|$)', '(^|,)cat_p_12038(,|$)', '(^|,)cat_p_152985(,|$)',
'(^|,)cat_p_299(,|$)', '(^|,)cat_p_400(,|$)', '(^|,)cat_p_13038(,|$)', '(^|,)cat_p_162985(,|$)',
];

$tagsSplitA = array_slice($tags, 0, 12);
$tagsSplitB = array_slice($tags, 12, 4);

$uri = UriFactory::factory('')->setHost('localhost')
->setPort(80)
->setScheme('http');

$this->cacheServer->expects($this->once())
->method('getUris')
->willReturn([$uri]);

$this->socketAdapterMock->expects($this->exactly(2))
->method('connect')
->with($uri->getHost(), $uri->getPort());

$this->socketAdapterMock->expects($this->exactly(2))
->method('write')
->withConsecutive(
[
'PURGE', $uri, '1.1',
['X-Magento-Tags-Pattern' => implode('|', $tagsSplitA), 'Host' => $uri->getHost()]
],
[
'PURGE', $uri, '1.1',
['X-Magento-Tags-Pattern' => implode('|', $tagsSplitB), 'Host' => $uri->getHost()]
]
);

$this->socketAdapterMock->expects($this->exactly(2))
->method('close');

$this->assertTrue($this->model->sendPurgeRequest($tags));
}

/**
Expand Down Expand Up @@ -130,6 +175,6 @@ public function testSendPurgeRequestWithException()
$this->loggerMock->expects($this->once())
->method('critical');

$this->assertFalse($this->model->sendPurgeRequest('tags'));
$this->assertFalse($this->model->sendPurgeRequest(['tags']));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function testFlushAllCache()
Config::VARNISH
);

$this->purgeCache->expects($this->once())->method('sendPurgeRequest')->with('.*');
$this->purgeCache->expects($this->once())->method('sendPurgeRequest')->with(['.*']);
$this->model->execute($this->observerMock);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ protected function setUp(): void
public function testInvalidateVarnish()
{
$tags = ['cache_1', 'cache_group'];
$pattern = '((^|,)cache_1(,|$))|((^|,)cache_group(,|$))';
$pattern = ['((^|,)cache_1(,|$))', '((^|,)cache_group(,|$))'];

$this->configMock->expects($this->once())->method('isEnabled')->willReturn(true);
$this->configMock->expects(
Expand Down