PluginProbe
Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution / 1.9.0
Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution v1.9.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 / Preload_Scheduler.php

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

356 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 preload scheduler which manages scheduling preloading.
4 *
5 * @package SolidWP\Performance
6 */
7
8 declare( strict_types=1 );
9
10 namespace SolidWP\Performance\Preload;
11
12 use Countable;
13 use InvalidArgumentException;
14 use SolidWP\Performance\Lock\Contracts\Blockable_Lock;
15 use SolidWP\Performance\Preload\Exceptions\PreloaderInProgressException;
16 use SolidWP\Performance\Preload\Exceptions\PreloadException;
17 use SolidWP\Performance\Preload\Monitor\Exceptions\PreloadMonitorMaxRetriesException;
18 use SolidWP\Performance\Preload\Monitor\Monitor;
19 use SolidWP\Performance\Preload\Sitemap\Sitemap_Service;
20 use SolidWP\Performance\Preload\State\State;
21 use SolidWP\Performance\Preload\Traits\With_Request_Params;
22 use SolidWP\Performance\Preload\Traits\With_Timeout;
23 use SolidWP\Performance\Psr\Log\LoggerInterface;
24 use SolidWP\Performance\Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;
25 use Throwable;
26
27 /**
28 * The preload scheduler which manages scheduling preloading.
29 *
30 * @package SolidWP\Performance
31 */
32 class Preload_Scheduler implements Countable {
33
34 use With_Timeout;
35 use With_Request_Params;
36
37 /**
38 * @var Blockable_Lock
39 */
40 private Blockable_Lock $lock;
41
42 /**
43 * @var Sitemap_Service
44 */
45 private Sitemap_Service $sitemap;
46
47 /**
48 * @var Client
49 */
50 private Client $client;
51
52 /**
53 * @var State
54 */
55 private State $state;
56
57 /**
58 * @var Monitor
59 */
60 private Monitor $monitor;
61
62 /**
63 * @var LoggerInterface
64 */
65 private LoggerInterface $logger;
66
67 /**
68 * @var Trigger_Url
69 */
70 private Trigger_Url $trigger_url;
71
72 /**
73 * @param Blockable_Lock $lock The preloading lock.
74 * @param Sitemap_Service $sitemap The sitemap service.
75 * @param Client $client The preloading client.
76 * @param State $state The preloader state.
77 * @param Monitor $monitor The preloader monitor.
78 * @param LoggerInterface $logger The logger.
79 * @param Trigger_Url $trigger_url The home URL to use to trigger a preloader batch.
80 */
81 public function __construct(
82 Blockable_Lock $lock,
83 Sitemap_Service $sitemap,
84 Client $client,
85 State $state,
86 Monitor $monitor,
87 LoggerInterface $logger,
88 Trigger_Url $trigger_url
89 ) {
90 $this->lock = $lock;
91 $this->sitemap = $sitemap;
92 $this->client = $client;
93 $this->state = $state;
94 $this->monitor = $monitor;
95 $this->logger = $logger;
96 $this->trigger_url = $trigger_url;
97 }
98
99 /**
100 * Start the preloading process.
101 *
102 * @param bool $force Whether we are force preloading the entire site.
103 *
104 * @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
105 *
106 * @throws PreloaderInProgressException If a preloader is already running.
107 * @throws PreloadException If queuing the batch fails.
108 * @throws InvalidArgumentException If an invalid source passed.
109 *
110 * @return void
111 */
112 public function start( bool $force = false ): void {
113 $this->logger->debug( 'Starting preloader...', [ 'force' => $force ] );
114
115 if ( ! $this->lock->acquire() ) {
116 throw new PreloaderInProgressException(
117 sprintf(
118 /* translators: %s: The source of what initiated or canceled the preloader. */
119 __( 'A preloader is already running via "%s"', 'solid-performance' ),
120 $this->lock->owner()
121 )
122 );
123 }
124
125 $this->state->start( $this->lock->owner(), $force );
126 $this->sitemap->crawl();
127 $this->queue_batch();
128
129 $this->logger->debug(
130 'Preloader started',
131 [
132 'owner' => $this->lock->owner(),
133 ]
134 );
135 }
136
137 /**
138 * Starts the next batch of preloading.
139 *
140 * @param string $url The URL to use to initiate the batch of URLs to preload.
141 *
142 * @throws PreloadException If queuing the batch failed.
143 *
144 * @return void
145 */
146 public function queue_batch( string $url = '' ): void {
147 $url = $url ?: $this->trigger_url->get();
148
149 $this->logger->debug( 'Queuing batch', [ 'url' => $url ] );
150
151 if ( ! $this->is_running() ) {
152 return;
153 }
154
155 try {
156 $response = $this->client->get(
157 $url,
158 $this->get_preload_trigger_params(),
159 [
160 'timeout' => $this->get_request_timeout(),
161 ]
162 );
163
164 $response->getContent();
165
166 $response->cancel();
167 } catch ( TimeoutExceptionInterface $e ) {
168 // We expect the request to throw a timeout exception.
169 $this->logger->debug( $e->getMessage() );
170 } catch ( Throwable $e ) {
171 throw new PreloadException( $e->getMessage(), $e->getCode(), $e );
172 }
173 }
174
175 /**
176 * Check if the preloader is currently running.
177 *
178 * @return bool
179 */
180 public function is_running(): bool {
181 return $this->lock->is_acquired();
182 }
183
184 /**
185 * Count the number of URLs we're going to preload.
186 *
187 * This includes any URLs excluded by Solid Performance, so it's
188 * not 100% accurate.
189 *
190 * @return int
191 */
192 public function count(): int {
193 return $this->sitemap->count();
194 }
195
196 /**
197 * Mark the preloader as completed.
198 *
199 * @throws PreloadException If we fail to mark the preloader as completed.
200 *
201 * @return self
202 */
203 public function complete(): self {
204 $this->logger->debug( 'Completing preloader.' );
205
206 try {
207 $this->lock->force_release();
208 $this->sitemap->clear();
209 $this->state->complete();
210 $this->monitor->clean();
211 } catch ( Throwable $e ) {
212 throw new PreloadException( $e->getMessage(), $e->getCode(), $e );
213 }
214
215 return $this;
216 }
217
218 /**
219 * Cancel the preloader, keep in mind any existing
220 * requests in the current batch will continue to complete.
221 *
222 * @throws PreloadException If we fail to cancel the preloader.
223 *
224 * @return self
225 */
226 public function cancel(): self {
227 $this->logger->debug( 'Canceling preloader.' );
228
229 try {
230 $this->lock->force_release();
231 $this->sitemap->clear();
232 $this->state->cancel();
233 $this->monitor->clean();
234 } catch ( Throwable $e ) {
235 throw new PreloadException( $e->getMessage(), $e->getCode(), $e );
236 }
237
238 return $this;
239 }
240
241 /**
242 * Mark the preloader as failed.
243 *
244 * @param string|null $debug_message An optional debug message.
245 *
246 * @throws PreloadException If something goes wrong when failing the preloader.
247 *
248 * @return self
249 */
250 public function fail( ?string $debug_message = null ): self {
251 $this->logger->debug(
252 'Failing preloader: {reason}',
253 [
254 'reason' => $debug_message,
255 ]
256 );
257
258 try {
259 $this->lock->force_release();
260 $this->sitemap->clear();
261 $this->state->fail();
262 $this->monitor->clean();
263 } catch ( Throwable $e ) {
264 throw new PreloadException( $e->getMessage(), $e->getCode(), $e );
265 }
266
267 return $this;
268 }
269
270 /**
271 * Get the percentage progress of the preloader.
272 *
273 * @throws PreloadException If the preloader stalled and we can't restart it.
274 * @throws PreloadMonitorMaxRetriesException If we tried to restart the preloader too many times.
275 *
276 * @return int
277 */
278 public function progress(): int {
279 $total = $this->sitemap->total();
280 $remaining = $this->count();
281
282 if ( ! $total ) {
283 return 0;
284 }
285
286 $this->logger->debug(
287 'Progress remaining URLs: {remaining}',
288 [
289 'total' => $total,
290 'remaining' => $remaining,
291 ]
292 );
293
294 // Check if the preloader is stalled and restart it.
295 $this->is_stalled( $remaining );
296
297 // Preloading complete.
298 if ( ! $remaining ) {
299 return 100;
300 }
301
302 $progress = (int) ceil( ( ( $total - $remaining ) / $total ) * 100 );
303
304 return min( $progress, 100 );
305 }
306
307 /**
308 * Check if the preloader is stalled and try to restart it.
309 *
310 * @param int|null $remaining_urls The remaining URL count.
311 *
312 * @throws PreloadException If the preloader stalled and we can't restart it.
313 * @throws PreloadMonitorMaxRetriesException If we tried to restart the preloader too many.
314 *
315 * @return bool
316 */
317 public function is_stalled( ?int $remaining_urls = null ): bool {
318 $remaining_urls ??= $this->count();
319
320 $this->logger->debug(
321 'Checking if preloader is stalled with {remaining_urls} URLs remaining',
322 [
323 'remaining_urls' => $remaining_urls,
324 ]
325 );
326
327 $is_stalled = $this->monitor->is_stalled( $remaining_urls );
328
329 if ( $is_stalled ) {
330 $this->logger->warning( 'Preloader stalled, attempting a retry' );
331
332 $this->queue_batch();
333 }
334
335 return $is_stalled;
336 }
337
338 /**
339 * Get the preloader state manager.
340 *
341 * @return State
342 */
343 public function state(): State {
344 return $this->state;
345 }
346
347 /**
348 * Whether we are force preloading the entire site.
349 *
350 * @return bool
351 */
352 public function is_forced(): bool {
353 return $this->state->get()->force;
354 }
355 }
356