Skip to content

Commit fbbae68

Browse files
committed
refactor: optimize cloudfront invalidation dispatch with chunking and retries
1 parent 86faa65 commit fbbae68

1 file changed

Lines changed: 55 additions & 29 deletions

File tree

src/CloudProvider/Aws/CloudFrontClient.php

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,26 @@ public function clearUrls($urls)
8787
});
8888
}
8989

90+
/**
91+
* Invalidate the given paths.
92+
*/
93+
public function invalidatePaths(array $paths)
94+
{
95+
$concretePaths = array_filter($paths, function (string $path) {
96+
return !str_ends_with($path, '*');
97+
});
98+
$wildcardPaths = array_filter($paths, function (string $path) {
99+
return str_ends_with($path, '*');
100+
});
101+
102+
while (!empty($wildcardPaths) || !empty($concretePaths)) {
103+
$batch = array_splice($wildcardPaths, 0, 15);
104+
$batch = array_merge($batch, array_splice($concretePaths, 0, 3000 - count($batch)));
105+
106+
$this->sendInvalidation($batch);
107+
}
108+
}
109+
90110
/**
91111
* {@inheritdoc}
92112
*/
@@ -104,7 +124,7 @@ public function sendClearRequest(?callable $guard = null)
104124
return;
105125
}
106126

107-
$this->createInvalidation($paths);
127+
$this->invalidatePaths($paths);
108128
}
109129

110130
/**
@@ -139,28 +159,6 @@ private function addPath(string $path)
139159
$this->invalidationPaths[] = $path;
140160
}
141161

142-
/**
143-
* Create an invalidation request.
144-
*/
145-
private function createInvalidation($paths)
146-
{
147-
if (is_string($paths)) {
148-
$paths = [$paths];
149-
} elseif (!is_array($paths)) {
150-
throw new \InvalidArgumentException('"paths" argument must be an array or a string');
151-
}
152-
153-
if (count($paths) > 1) {
154-
$paths = $this->filterUniquePaths($paths);
155-
}
156-
157-
$response = $this->request('post', "/2020-05-31/distribution/{$this->distributionId}/invalidation", $this->generateInvalidationPayload($paths));
158-
159-
if (201 !== $this->parseResponseStatusCode($response)) {
160-
throw new \RuntimeException($this->createExceptionMessage('Invalidation request failed', $response));
161-
}
162-
}
163-
164162
/**
165163
* Filter all paths and only keep unique ones.
166164
*/
@@ -180,20 +178,16 @@ private function filterUniquePaths(array $paths): array
180178
});
181179

182180
$wildcardPaths = $wildcardPaths->map(function (string $path) use ($wildcardPaths) {
183-
$filteredWildcardPaths = preg_grep(sprintf('/%s/', str_replace('\*', '.*', preg_quote($path, '/'))), $wildcardPaths->all(), PREG_GREP_INVERT);
181+
$filteredWildcardPaths = preg_grep(sprintf('/^%s/', str_replace('\*', '.*', preg_quote($path, '/'))), $wildcardPaths->all(), PREG_GREP_INVERT);
184182
$filteredWildcardPaths[] = $path;
185183

186184
return $filteredWildcardPaths;
187185
});
188186

189187
$wildcardPaths = new Collection(!$wildcardPaths->isEmpty() ? array_intersect(...$wildcardPaths->all()) : []);
190188

191-
if ($wildcardPaths->count() > 15) {
192-
throw new \RuntimeException('CloudFront only allows for a maximum of 15 wildcard invalidations');
193-
}
194-
195189
$wildcardPaths->each(function (string $path) use (&$filteredPaths) {
196-
$filteredPaths = preg_grep(sprintf('/%s/', str_replace('\*', '.*', preg_quote($path, '/'))), $filteredPaths, PREG_GREP_INVERT);
190+
$filteredPaths = preg_grep(sprintf('/^%s/', str_replace('\*', '.*', preg_quote($path, '/'))), $filteredPaths, PREG_GREP_INVERT);
197191
});
198192

199193
return array_merge($wildcardPaths->all(), $filteredPaths);
@@ -244,4 +238,36 @@ private function generateInvalidationPayload(array $paths): string
244238

245239
return $xml;
246240
}
241+
242+
/**
243+
* Send an invalidation request.
244+
*/
245+
private function sendInvalidation(array $paths)
246+
{
247+
$attempts = 0;
248+
$maxAttempts = 3;
249+
$payload = $this->generateInvalidationPayload($paths);
250+
251+
while ($attempts < $maxAttempts) {
252+
++$attempts;
253+
254+
$response = $this->request('post', "/2020-05-31/distribution/{$this->distributionId}/invalidation", $payload);
255+
$statusCode = $this->parseResponseStatusCode($response);
256+
257+
if (201 === $statusCode) {
258+
return;
259+
}
260+
261+
$awsError = $this->parseAwsError($response['body'] ?? '');
262+
$errorCode = $awsError['code'] ?? '';
263+
264+
$shouldRetry = $statusCode >= 500 || 429 === $statusCode || (400 === $statusCode && in_array($errorCode, ['Throttling', 'TooManyInvalidationsInProgress'], true));
265+
266+
if (!$shouldRetry || $attempts >= $maxAttempts) {
267+
throw new \RuntimeException($this->createExceptionMessage('Invalidation request failed', $response));
268+
}
269+
270+
sleep($attempts * 2);
271+
}
272+
}
247273
}

0 commit comments

Comments
 (0)