Description
Preconditions:
\Magento\CacheInvalidate\Model\PurgeCache::sendPurgeRequest is responsible for taking arbitrarily large tag patterns and chunking it into HTTP PURGE requests based on the maximum header length Varnish will accept.
Currently, \Magento\CacheInvalidate\Observer\InvalidateVarnishObserver::execute will join tags with '|' and call sendPurgeRequest. Those tags themselves may include '|' characters.
\Magento\CacheInvalidate\Model\PurgeCache::splitTags will then separate on '|' and re-join components until the length of those components reaches the max. header length. This is too naive, since it may now split an individual tag (one which itself included '|' characters) across two requests. Both of those requests now have an invalid pattern - this would mean that at the very least, some purges will be missed. In the worst-case scenario, I expect this could also lead to Varnish being completely purged, depending on the tag that was incorrectly split.
For the sake of brevity, the example below has been limited to a small subset of my real-world example; as such let's pretend the max header size is 256 bytes. Note that this is a typical pattern that would result from a product save event.
((^|,)cat_p_1468(,|$))|((^|,)cat_p_1361(,|$))|((^|,)cat_p_1473(,|$))|((^|,)cat_p_930(,|$))|((^|,)cat_p_933(,|$))|((^|,)cat_p_934(,|$))|((^|,)cat_p_664(,|$))|((^|,)cat_p_487(,|$))|((^|,)cat_p_490(,|$))|((^|,)cat_p_491(,|$))|((^|,)cat_p_153(,|$))|((^|,)cat_p_156(,|$))|((^|,)cat_p_157(,|$))|((^|,)cat_p_497(,|$))|((^|,)cat_p_498(,|$))|((^|,)cat_p_499(,|$))|((^|,)cat_p_227(,|$))|((^|,)cat_p_228(,|$))|((^|,)cat_p_229(,|$))|((^|,)cat_p_242(,|$))|((^|,)cat_p_244(,|$))|((^|,)cat_p_245(,|$))
Based on my desired max. header size, this will be chunked into two separate requests:
((^|,)cat_p_1468(,|$))|((^|,)cat_p_1361(,|$))|((^|,)cat_p_1473(,|$))|((^|,)cat_p_930(,|$))|((^|,)cat_p_933(,|$))|((^|,)cat_p_934(,|$))|((^|,)cat_p_664(,|$))|((^|,)cat_p_487(,|$))|((^|,)cat_p_490(,|$))|((^|,)cat_p_491(,|$))|((^|,)cat_p_153(,|$))|((^
,)cat_p_156(,|$))|((^|,)cat_p_157(,|$))|((^|,)cat_p_497(,|$))|((^|,)cat_p_498(,|$))|((^|,)cat_p_499(,|$))|((^|,)cat_p_227(,|$))|((^|,)cat_p_228(,|$))|((^|,)cat_p_229(,|$))|((^|,)cat_p_242(,|$))|((^|,)cat_p_244(,|$))|((^|,)cat_p_245(,|$))|((^|,)cat_p_245(,|$))
Expected split behaviour:
((^|,)cat_p_1468(,|$))|((^|,)cat_p_1361(,|$))|((^|,)cat_p_1473(,|$))|((^|,)cat_p_930(,|$))|((^|,)cat_p_933(,|$))|((^|,)cat_p_934(,|$))|((^|,)cat_p_664(,|$))|((^|,)cat_p_487(,|$))|((^|,)cat_p_490(,|$))|((^|,)cat_p_491(,|$))|((^|,)cat_p_153(,|$))
((^|,)cat_p_156(,|$))|((^|,)cat_p_157(,|$))|((^|,)cat_p_497(,|$))|((^|,)cat_p_498(,|$))|((^|,)cat_p_499(,|$))|((^|,)cat_p_227(,|$))|((^|,)cat_p_228(,|$))|((^|,)cat_p_229(,|$))|((^|,)cat_p_242(,|$))|((^|,)cat_p_244(,|$))|((^|,)cat_p_245(,|$))
Steps to reproduce:
- Have full_page cache enabled
- Have caching application configured to Varnish
- Run code below
<?php
require __DIR__ . '/app/bootstrap.php';
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
class DummyEntity implements \Magento\Framework\DataObject\IdentityInterface
{
public function getIdentities()
{
$ids = range(807, 1193);
return array_map(function ($id) {
return 'cat_p_' . $id;
}, $ids);
}
}
$objectManager = $bootstrap->getObjectManager();
$appState = $objectManager->get('Magento\Framework\App\State');
$appState->setAreaCode('frontend');
$eventManager = $objectManager->get('\Magento\Framework\Event\Manager');
$myObject = new DummyEntity();
$eventManager->dispatch('clean_cache_by_tags', ['object' => $myObject]);
Actual Result:
Observe two resulting PURGE requests sent to Varnish with invalid patterns (e.g. xdebug_break inside \Magento\CacheInvalidate\Model\PurgeCache::sendPurgeRequestToServers and observe the X-Magento-Tags-Pattern header)
Expected Result: