-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
VarnishPurger.php
134 lines (109 loc) · 3.83 KB
/
VarnishPurger.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?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\HttpCache;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* Purges Varnish.
*
* @author Kévin Dunglas <[email protected]>
*/
final class VarnishPurger implements PurgerInterface
{
private const DEFAULT_VARNISH_MAX_HEADER_LENGTH = 8000;
private const REGEXP_PATTERN = '(%s)($|\,)';
private readonly int $maxHeaderLength;
/**
* @param HttpClientInterface[] $clients
*/
public function __construct(private readonly iterable $clients, int $maxHeaderLength = self::DEFAULT_VARNISH_MAX_HEADER_LENGTH)
{
$this->maxHeaderLength = $maxHeaderLength - mb_strlen(self::REGEXP_PATTERN) + 2; // 2 for %s
}
/**
* Calculate how many tags fit into the header.
*
* This assumes that the tags are separated by one character.
*
* From https://github.com/FriendsOfSymfony/FOSHttpCache/blob/2.8.0/src/ProxyClient/HttpProxyClient.php#L137
*
* @param string[] $escapedTags
* @param string $glue The concatenation string to use
*
* @return int Number of tags per tag invalidation request
*/
private function determineTagsPerHeader(array $escapedTags, string $glue): int
{
if (mb_strlen(implode($glue, $escapedTags)) < $this->maxHeaderLength) {
return \count($escapedTags);
}
/*
* estimate the amount of tags to invalidate by dividing the max
* header length by the largest tag (minus the glue length)
*/
$tagsize = max(array_map('mb_strlen', $escapedTags));
$gluesize = \strlen($glue);
return (int) floor(($this->maxHeaderLength + $gluesize) / ($tagsize + $gluesize)) ?: 1;
}
/**
* {@inheritdoc}
*/
public function purge(array $iris): void
{
if (!$iris) {
return;
}
$chunkSize = $this->determineTagsPerHeader($iris, '|');
$irisChunks = array_chunk($iris, $chunkSize);
foreach ($irisChunks as $irisChunk) {
$this->purgeRequest($irisChunk);
}
}
/**
* {@inheritdoc}
*/
public function getResponseHeaders(array $iris): array
{
return ['Cache-Tags' => implode(',', $iris)];
}
private function purgeRequest(array $iris): void
{
// Create the regex to purge all tags in just one request
$parts = array_map(static fn ($iri): string => // here we should remove the prefix as it's not discriminent and cost a lot to compute
preg_quote($iri), $iris);
foreach ($this->chunkRegexParts($parts) as $regex) {
$regex = \sprintf(self::REGEXP_PATTERN, $regex);
$this->banRegex($regex);
}
}
private function banRegex(string $regex): void
{
foreach ($this->clients as $client) {
$client->request('BAN', '', ['headers' => ['ApiPlatform-Ban-Regex' => $regex]]);
}
}
private function chunkRegexParts(array $parts): iterable
{
if (1 === \count($parts)) {
yield $parts[0];
return;
}
$concatenatedParts = implode("\n", $parts);
if (\strlen($concatenatedParts) <= $this->maxHeaderLength) {
yield str_replace("\n", '|', $concatenatedParts);
return;
}
$lastSeparator = strrpos(substr($concatenatedParts, 0, $this->maxHeaderLength + 1), "\n");
$chunk = substr($concatenatedParts, 0, $lastSeparator);
yield str_replace("\n", '|', $chunk);
$nextParts = \array_slice($parts, substr_count($chunk, "\n") + 1);
yield from $this->chunkRegexParts($nextParts);
}
}