PluginProbe
Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution / 1.4.0
Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution v1.4.0
2.0.1 trunk 1.0.0 1.1.0 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 1.4.2 1.5.0 1.6.0 1.6.1 1.7.0 1.7.1 1.8.0 1.9.0 2.0.0
solid-performance / src / Performance / Preload / Preloader.php

Preloader.php in Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution 1.4.0, at src/Performance/Preload/Preloader.php

309 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The preloader that concurrently processes crawled sitemap URLs.
4 *
5 * @package SolidWP\Performance
6 */
7
8 declare( strict_types=1 );
9
10 namespace SolidWP\Performance\Preload;
11
12 use SolidWP\Performance\Preload\Contracts\Preloadable;
13 use SolidWP\Performance\Preload\Limiter\Batch_Limiter;
14 use SolidWP\Performance\Preload\Sitemap\Repositories\Contracts\Sitemap_Repository;
15 use SolidWP\Performance\Preload\Traits\With_Request_Params;
16 use SolidWP\Performance\Preload\Traits\With_Timeout;
17 use SolidWP\Performance\Psr\Log\LoggerInterface;
18 use SolidWP\Performance\StellarWP\SuperGlobals\SuperGlobals;
19 use SolidWP\Performance\Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
20 use SolidWP\Performance\Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
21 use SolidWP\Performance\Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;
22 use SolidWP\Performance\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
23 use SolidWP\Performance\Symfony\Contracts\HttpClient\ResponseInterface;
24 use Throwable;
25
26 /**
27 * The preloader that concurrently processes crawled sitemap URLs.
28 *
29 * @package SolidWP\Performance
30 */
31 final class Preloader implements Preloadable {
32
33 use With_Timeout;
34 use With_Request_Params;
35
36 /**
37 * @var Sitemap_Repository
38 */
39 private Sitemap_Repository $repository;
40
41 /**
42 * @var Preload_Scheduler
43 */
44 private Preload_Scheduler $scheduler;
45
46 /**
47 * @var Client
48 */
49 private Client $client;
50
51 /**
52 * @var Batch_Limiter
53 */
54 private Batch_Limiter $batch_limiter;
55
56 /**
57 * @var LoggerInterface
58 */
59 private LoggerInterface $logger;
60
61 /**
62 * @var Preload_Mode_Manager
63 */
64 private Preload_Mode_Manager $preload_mode_manager;
65
66 /**
67 * How many seconds to sleep between batches.
68 *
69 * @var int
70 */
71 private int $sleep;
72
73 /**
74 * The maximum number of errors before high performance mode is disabled.
75 *
76 * @var int
77 */
78 private int $max_errors_per_batch;
79
80 /**
81 * @var int[]
82 */
83 private array $error_codes;
84
85 /**
86 * @param Sitemap_Repository $repository The Sitemap Repository.
87 * @param Preload_Scheduler $scheduler The Preload Scheduler.
88 * @param Client $client The Preload Client.
89 * @param Batch_Limiter $batch_limiter The batch rate limiter.
90 * @param LoggerInterface $logger The logger.
91 * @param Preload_Mode_Manager $preload_mode_manager The preload mode manager.
92 * @param int $sleep How many seconds to sleep between batches.
93 * @param int $max_errors_per_batch The maximum number of errors before high performance mode is disabled.
94 * @param int[] $error_codes The http response codes considered errors.
95 */
96 public function __construct(
97 Sitemap_Repository $repository,
98 Preload_Scheduler $scheduler,
99 Client $client,
100 Batch_Limiter $batch_limiter,
101 LoggerInterface $logger,
102 Preload_Mode_Manager $preload_mode_manager,
103 int $sleep = 1,
104 int $max_errors_per_batch = 3,
105 array $error_codes = [
106 429,
107 503,
108 508,
109 ]
110 ) {
111 $this->repository = $repository;
112 $this->scheduler = $scheduler;
113 $this->client = $client;
114 $this->batch_limiter = $batch_limiter;
115 $this->logger = $logger;
116 $this->preload_mode_manager = $preload_mode_manager;
117 $this->sleep = $sleep;
118 $this->max_errors_per_batch = $max_errors_per_batch;
119 $this->error_codes = $error_codes;
120 }
121
122 /**
123 * Send a preload request.
124 *
125 * @action solidwp/performance/cache/purge/url/after
126 *
127 * @param string $url The full or relative URL to preload.
128 * @param array<string, mixed> $params The query parameters to attach to the request.
129 *
130 * @throws TransportExceptionInterface When an error happens at the transport level.
131 *
132 * @return ResponseInterface
133 */
134 public function preload_url( string $url, array $params = [] ): ResponseInterface {
135 /**
136 * Filters the Accept-Encoding HTTP header to decide the order of which cache file type to create when preloading.
137 *
138 * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
139 *
140 * @param string $accept_encoding The valid Accept-Encoding string.
141 */
142 $encoding = apply_filters( 'solidwp/performance/preload/accept_encoding', 'gzip, deflate, br, zstd' );
143
144 return $this->client->get(
145 $url,
146 $params,
147 [
148 'Cache-Control' => 'no-cache',
149 'Accept-Encoding' => $encoding,
150 ],
151 [
152 'timeout' => $this->get_request_timeout(),
153 ]
154 );
155 }
156
157 /**
158 * Preload a batch of crawled sitemap URLs.
159 *
160 * @action template_redirect
161 *
162 * @return void
163 */
164 public function preload(): void {
165 // The preloader query string parameter isn't included.
166 if ( ! SuperGlobals::get_get_var( self::PRELOAD_TRIGGER_PARAM ) ) {
167 return;
168 }
169
170 // This should only run on the frontend.
171 if ( is_admin() || wp_doing_ajax() || wp_is_serving_rest_request() ) {
172 return;
173 }
174
175 // Check if the preloader is actually running.
176 if ( ! $this->scheduler->is_running() ) {
177 return;
178 }
179
180 // We're out of URls to crawl, complete the preloading.
181 if ( $this->repository->count() <= 0 ) {
182 $this->logger->debug( 'Completing Preloader: No more URLs' );
183 $this->scheduler->complete();
184
185 return;
186 }
187
188 $batch_size = $this->batch_limiter->get_batch_size();
189
190 $this->logger->debug(
191 'URL batch size: {batch_size}',
192 [
193 'batch_size' => $batch_size,
194 ]
195 );
196
197 // Only sleep if the batch limiter is at the minimum.
198 if ( $batch_size === Batch_Limiter::MIN_BATCH_SIZE ) {
199 $this->logger->debug(
200 'Minimum batch size detected. Sleeping for {sleep_seconds}s',
201 [
202 'sleep_seconds' => $this->sleep,
203 ]
204 );
205
206 sleep( $this->sleep );
207 }
208
209 $urls = $this->repository->pull( $batch_size );
210
211 $responses = [];
212
213 foreach ( $urls as $url ) {
214 $params = [];
215
216 if ( $this->scheduler->is_forced() ) {
217 // We're doing an entire site preload, cache bust every request.
218 $params = $this->get_preload_cache_bust_params();
219 }
220
221 try {
222 $responses[] = $this->preload_url( $url, $params );
223 } catch ( TimeoutExceptionInterface $e ) {
224 // We expected timeouts.
225 $this->logger->debug( $e->getMessage() );
226 }
227 }
228
229 if ( $responses ) {
230 // Trigger the next batch, use the homepage because it has to exist, right?
231 try {
232 $responses[] = $this->preload_url(
233 '/',
234 $this->get_preload_trigger_params()
235 );
236 } catch ( TimeoutExceptionInterface $e ) {
237 // We expected timeouts.
238 $this->logger->debug( $e->getMessage() );
239 }
240 }
241
242 $errors = 0;
243
244 // Process responses for the batch.
245 foreach ( $responses as $response ) {
246 try {
247 $response->getHeaders();
248 } catch ( ServerExceptionInterface |ClientExceptionInterface $e ) {
249 $this->logger->debug(
250 $e->getMessage(),
251 [
252 'exception' => $e,
253 ]
254 );
255
256 try {
257 $status_code = $e->getResponse()->getStatusCode();
258
259 // Check if we're hitting any arbitrary resource limits or the server is rate limiting us.
260 if ( ! in_array( $status_code, $this->error_codes, true ) ) {
261 continue;
262 }
263
264 ++$errors;
265
266 if ( $errors < $this->max_errors_per_batch ) {
267 continue;
268 }
269
270 if ( $this->preload_mode_manager->is_high_performance_mode() ) {
271 $this->logger->error(
272 'Max response errors "{errors}" reached. Disabling high performance mode.',
273 [
274 'errors' => $errors,
275 'last_status_code' => $status_code,
276 'response_info' => $e->getResponse()->getInfo(),
277 ]
278 );
279
280 // This is currently permanent, but can be reversed with `wp solid perf preload perf-mode on`.
281 $this->preload_mode_manager->disable_high_performance_mode();
282 } else {
283 $this->logger->warning(
284 'Max response errors "{errors}" reached. High performance mode already disabled!',
285 [
286 'errors' => $errors,
287 'last_status_code' => $status_code,
288 'response_info' => $e->getResponse()->getInfo(),
289 ]
290 );
291 }
292 } catch ( TransportExceptionInterface $e ) {
293 $this->logger->warning(
294 'Response threw exception when checking status code',
295 [
296 'exception' => $e,
297 ]
298 );
299 }
300 } catch ( Throwable $e ) {
301 // Ignore any requests that failed for any other reason.
302 $this->logger->debug( $e->getMessage() );
303 } finally {
304 $response->cancel();
305 }
306 }
307 }
308 }
309