PluginProbe
Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution / 1.8.0
Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution v1.8.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.8.0, at src/Performance/Preload/Preloader.php

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