PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.0
4.11.0 4.10.0 4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Framework / Job / AbstractJob.php
wp-staging / Framework / Job Last commit date
Ajax 1 day ago BackgroundProcessing 1 day ago Dto 1 day ago Exception 1 day ago Interfaces 1 day ago Jobs 1 day ago Task 1 day ago Traits 10 months ago AbstractJob.php 1 day ago JobProvider.php 1 day ago JobServiceProvider.php 1 day ago JobTransientCache.php 1 day ago ProcessLock.php 1 day ago
AbstractJob.php
600 lines
1 <?php
2
3
4
5
6 namespace WPStaging\Framework\Job;
7
8 use RuntimeException;
9 use WPStaging\Core\Utils\Logger;
10 use WPStaging\Core\WPStaging;
11 use WPStaging\Framework\Adapter\Directory;
12 use WPStaging\Framework\Assets\Assets;
13 use WPStaging\Framework\Exceptions\WPStagingException;
14 use WPStaging\Framework\Filesystem\DiskWriteCheck;
15 use WPStaging\Framework\Filesystem\Filesystem;
16 use WPStaging\Framework\Interfaces\ShutdownableInterface;
17 use WPStaging\Framework\Job\Dto\AbstractDto;
18 use WPStaging\Framework\Job\Dto\JobDataDto;
19 use WPStaging\Framework\Job\Dto\TaskResponseDto;
20 use WPStaging\Framework\Job\Exception\DiskNotWritableException;
21 use WPStaging\Framework\Job\Exception\TaskHealthException;
22 use WPStaging\Framework\Job\Task\AbstractTask;
23 use WPStaging\Framework\Traits\BenchmarkTrait;
24 use WPStaging\Framework\Utils\Cache\Cache;
25 use WPStaging\Framework\Queue\FinishedQueueException;
26
27 use function WPStaging\functions\debug_log;
28
29 abstract class AbstractJob implements ShutdownableInterface
30 {
31 use BenchmarkTrait;
32
33
34 protected $jobDataDto;
35
36
37 private $jobDataCache;
38
39
40 private $hasPersisted = false;
41
42
43 private $hasShutdownBackstop = false;
44
45
46 protected $currentTaskName;
47
48
49 protected $currentTask;
50
51
52 protected $filesystem;
53
54
55 protected $directory;
56
57
58 protected $processLock;
59
60
61 protected $diskFullCheck;
62
63
64 protected $jobTransientCache;
65
66
67 protected $memoryExhaustErrorTmpFile = false;
68
69 protected $maxRetries = 10;
70
71
72
73
74 protected $isCancelJob = false;
75
76 public function __construct(
77 Cache $jobDataCache,
78 JobDataDto $jobDataDto,
79 Filesystem $filesystem,
80 Directory $directory,
81 ProcessLock $processLock,
82 DiskWriteCheck $diskFullCheck,
83 JobTransientCache $jobTransientCache
84 ) {
85 $this->jobDataDto = $jobDataDto;
86 $this->jobDataCache = $jobDataCache;
87 $this->filesystem = $filesystem;
88 $this->directory = $directory;
89
90 $this->jobDataCache->setLifetime(HOUR_IN_SECONDS);
91 $this->jobDataCache->setFilename('jobCache_' . $this::getJobName());
92
93 $this->processLock = $processLock;
94 $this->diskFullCheck = $diskFullCheck;
95 $this->maxRetries = apply_filters(Assets::FILTER_TESTS_MAXIMUM_RETRIES, $this->maxRetries);
96
97 $this->jobTransientCache = $jobTransientCache;
98 }
99
100
101
102
103
104
105
106
107
108
109 public function persist()
110 {
111 if ($this->jobDataDto->isStatusCheck()) {
112 return;
113 }
114
115 try {
116 $this->diskFullCheck->testDiskIsWriteable();
117 } catch (DiskNotWritableException $e) {
118
119 }
120
121 if ($this->jobDataDto->isFinished() && !$this->jobDataDto->isCleaned()) {
122 $this->cleanup();
123 $this->jobDataDto->setCleaned();
124 $this->hasPersisted = true;
125 return;
126 }
127
128 if ($this->currentTask instanceof AbstractTask) {
129 $this->jobDataDto->setQueueOffset($this->currentTask->getQueue()->getOffset());
130 $this->currentTask->persistStepsDto();
131 }
132
133 $this->persistJobDataDto();
134
135 $this->hasPersisted = true;
136 }
137
138
139
140
141 public function persistJobDataDto()
142 {
143 $data = $this->jobDataDto->toArray();
144
145 try {
146
147
148 if ($this->jobDataCache->save($data, true) === false) {
149 throw new \RuntimeException('Could not persist Job data to cache.');
150 }
151 } catch (\Exception $e) {
152 debug_log("Could not persist Job data to cache:" . $e->getMessage());
153 throw new \RuntimeException('Could not persist Job data to cache: ' . $e->getMessage(), 0, $e);
154 }
155 }
156
157
158
159
160
161
162
163
164 public function onWpShutdown()
165 {
166 if ($this->hasPersisted) {
167 return;
168 }
169
170 $this->persist();
171 }
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188 public function persistIfShutdownActionDidNotRun()
189 {
190 if ($this->hasPersisted) {
191 return;
192 }
193
194 try {
195 $this->persist();
196 } catch (\Throwable $e) {
197
198
199 debug_log('Job state could not be persisted on shutdown: ' . $e->getMessage());
200 }
201 }
202
203
204
205
206 protected function registerShutdownBackstop()
207 {
208 if ($this->hasShutdownBackstop) {
209 return;
210 }
211
212 $this->hasShutdownBackstop = true;
213
214 register_shutdown_function([$this, 'persistIfShutdownActionDidNotRun']);
215 }
216
217
218
219
220
221 public static function getJobName()
222 {
223 throw new WPStagingException('Any extending class MUST override the getJobName method.');
224 }
225
226
227 abstract protected function getJobTasks();
228
229
230 abstract protected function execute();
231
232
233 abstract protected function init();
234
235
236 public function prepareAndExecute()
237 {
238
239
240
241
242
243 $this->processLock->lockProcess();
244
245 try {
246
247 $this->diskFullCheck->hasDiskWriteTestFailed();
248 } catch (DiskNotWritableException $e) {
249 $this->jobDataCache->delete();
250
251 return $this->getJobFailResponse($e->getMessage());
252 }
253
254 if ($this->getIsCancelled()) {
255 $this->jobDataCache->delete();
256
257 return $this->getJobCancelResponse();
258 }
259
260 try {
261 try {
262 $this->prepare();
263 } catch (TaskHealthException $e) {
264 if ($e->getCode() === TaskHealthException::CODE_TASK_FAILED_TOO_MANY_TIMES) {
265 $this->jobDataCache->delete();
266
267 return $this->getJobFailResponse($e->getMessage());
268 } else {
269 return $this->getJobRetryResponse($e->getMessage());
270 }
271 } catch (RuntimeException $ex) {
272 $this->jobDataCache->delete();
273
274 return $this->getJobFailResponse($ex->getMessage());
275 }
276
277 $this->registerShutdownBackstop();
278
279
280 $response = $this->execute();
281
282
283
284
285
286
287
288
289 $nextTask = $this->jobDataDto->getCurrentTask();
290
291 if (is_subclass_of($nextTask, AbstractTask::class)) {
292 $response->setStatusTitle(call_user_func("$nextTask::getTaskTitle"));
293 }
294
295 $this->removeMemoryExhaustErrorTmpFile();
296
297 if ($this->getIsCancelled()) {
298 $this->jobDataCache->delete();
299
300 return $this->getJobCancelResponse();
301 }
302
303 return $response;
304 } catch (DiskNotWritableException $e) {
305
306
307
308
309
310
311 return $this->getJobRetryResponse($e->getMessage());
312 }
313 }
314
315
316
317
318 public function updateTasks()
319 {
320 $this->init();
321 $this->addTasks($this->getJobTasks());
322 }
323
324
325
326
327 public function getTransientCache(): JobTransientCache
328 {
329 return $this->jobTransientCache;
330 }
331
332
333
334
335 public function getJobDataDto()
336 {
337 return $this->jobDataDto;
338 }
339
340
341
342
343 public function setJobDataDto($jobDataDto)
344 {
345 $this->jobDataDto = $jobDataDto;
346 }
347
348 public function getIsCancelled(): bool
349 {
350 if ($this->isCancelJob) {
351 return false;
352 }
353
354 try {
355 return $this->jobTransientCache->getJobStatus() === JobTransientCache::STATUS_CANCELLED;
356 } catch (\Throwable $e) {
357
358 return false;
359 }
360 }
361
362
363
364
365 protected function checkLastTaskHealth()
366 {
367
368 if ($this->jobDataDto->getTaskHealthIsRetrying()) {
369 $this->jobDataDto->setTaskHealthIsRetrying(false);
370
371 return;
372 }
373
374 if (!$this->jobDataDto->getTaskHealthResponded()) {
375
376 $this->jobDataDto->setTaskHealthSequentialFailedRetries($this->jobDataDto->getTaskHealthSequentialFailedRetries() + 1);
377 $this->jobDataCache->save($this->jobDataDto);
378
379 if ($this->jobDataDto->getTaskHealthSequentialFailedRetries() >= $this->maxRetries) {
380 throw TaskHealthException::taskFailedTooManyTimes();
381 } else {
382 $this->jobDataDto->setTaskHealthIsRetrying(true);
383 throw TaskHealthException::retryingTask($this->jobDataDto->getTaskHealthSequentialFailedRetries(), $this->maxRetries);
384 }
385 }
386 }
387
388 public function prepare()
389 {
390 $data = $this->jobDataCache->get([]);
391
392 if ($data) {
393 $this->jobDataDto->hydrate($data);
394 }
395
396
397 WPStaging::getInstance()->getContainer()->singleton(JobDataDto::class, $this->jobDataDto);
398
399 $action = empty($_GET['action']) ? '' : sanitize_text_field($_GET['action']);
400 if (empty($action)) {
401 $action = empty($_POST['action']) ? '' : sanitize_text_field($_POST['action']);
402 }
403
404 $this->jobDataDto->setStatusCheck(in_array($action, ['wpstg--backups--status', 'wpstg--job--status'], true));
405 if ($this->jobDataDto->isStatusCheck()) {
406 return;
407 }
408
409 if ($this->jobDataDto->isInit()) {
410 $this->cleanup();
411 $this->init();
412 $this->jobDataDto->setCurrentTaskIndex(0);
413 $this->jobDataDto->setCurrentTaskData([]);
414 $this->addTasks($this->getJobTasks());
415 } else {
416 $this->checkLastTaskHealth();
417 }
418
419 $this->jobDataDto->setInit(false);
420
421 $this->currentTaskName = $this->jobDataDto->getCurrentTask();
422
423 if (empty($this->currentTaskName)) {
424 throw new \RuntimeException('Internal error: Next task of queue job is null or invalid.');
425 }
426
427
428 $this->currentTask = WPStaging::getInstance()->get($this->currentTaskName);
429
430 if (!$this->currentTask instanceof AbstractTask) {
431 throw new \RuntimeException('Is there enough free disk space? Please free up some space. Delete old backup files and staging sites and try again. Error: Next task of queue job is null or invalid. Task name: ' . $this->currentTaskName . ' Task: ' . print_r($this->currentTask, true));
432 }
433
434 if (!$this->jobDataDto instanceof AbstractDto) {
435 throw new \RuntimeException('Job Queue DTO is null or invalid.');
436 }
437
438 $this->currentTask->setJobContext($this);
439 $this->currentTask->setJobDataDto($this->jobDataDto);
440 $this->currentTask->setJobId($this->jobDataDto->getId());
441 $this->currentTask->setJobName($this::getJobName());
442 $this->currentTask->setDebug(defined('WPSTG_DEBUG') && WPSTG_DEBUG);
443 $this->currentTask->setupLogger();
444
445
446 $this->jobDataDto->setTaskHealthName($this->currentTaskName);
447 $this->jobDataDto->setTaskHealthResponded(false);
448 }
449
450 public function commitLogs()
451 {
452 if ($this->currentTask instanceof AbstractTask) {
453 $this->currentTask->commitLogs();
454 }
455 }
456
457
458 public function getCurrentTask()
459 {
460 return $this->currentTask;
461 }
462
463
464
465
466
467 public function setMemoryExhaustErrorTmpFile(string $memoryExhaustErrorTmpFile)
468 {
469 $this->memoryExhaustErrorTmpFile = $memoryExhaustErrorTmpFile;
470 }
471
472 protected function removeMemoryExhaustErrorTmpFile()
473 {
474 if ($this->memoryExhaustErrorTmpFile === '') {
475 return;
476 }
477
478 if (file_exists($this->memoryExhaustErrorTmpFile)) {
479 unlink($this->memoryExhaustErrorTmpFile);
480 }
481 }
482
483 protected function cleanup()
484 {
485
486 $this->filesystem->setExcludePaths(['*.*', '!*.cache.php', '!*.cache', '!*.wpstg', '!*.sql']);
487 $this->filesystem->delete($this->directory->getCacheDirectory(), $deleteSelf = false);
488 $this->filesystem->setExcludePaths([]);
489 $this->filesystem->mkdir($this->directory->getCacheDirectory(), true);
490 }
491
492
493
494
495 protected function deleteJobDataCache()
496 {
497 $this->jobDataCache->delete();
498 }
499
500
501
502
503
504
505 protected function getResponse(TaskResponseDto $response)
506 {
507 $this->jobDataDto->setTaskHealthResponded(true);
508 $this->jobDataDto->setTaskHealthSequentialFailedRetries(0);
509
510 $response->setJob(substr($this->findCurrentJob(), 3));
511
512
513 if ($response->isRunning()) {
514 $className = get_class($this->currentTask);
515 }
516
517 try {
518 if (!$response->isRunning()) {
519 $this->jobDataDto->moveToNextTask();
520
521
522
523 $this->persistJobDataDto();
524 }
525 } catch (FinishedQueueException $e) {
526 $this->jobDataDto->setFinished(true);
527
528 $this->persistJobDataDto();
529
530 return $response;
531 }
532
533 $response->setIsRunning(true);
534
535 return $response;
536 }
537
538 private function findCurrentJob()
539 {
540 $class = explode('\\', static::class);
541
542 return end($class);
543 }
544
545 protected function addTasks(array $tasks = [])
546 {
547 $this->jobDataDto->setTaskQueue($tasks);
548 }
549
550 protected function getJobCancelResponse(): TaskResponseDto
551 {
552 $response = new TaskResponseDto();
553 $response->setIsRunning(false);
554 $response->setJobStatus('JOB_CANCEL');
555 $response->addMessage([
556 'type' => 'critical',
557 'date' => $this->getFormattedDate(),
558 'message' => esc_html__('Job is cancelled', 'wp-staging'),
559 ]);
560
561 return $response;
562 }
563
564 protected function getJobFailResponse(string $message): TaskResponseDto
565 {
566 $response = new TaskResponseDto();
567 $response->setIsRunning(false);
568 $response->setJobStatus('JOB_FAIL');
569 $response->addMessage([
570 'type' => 'critical',
571 'date' => $this->getFormattedDate(),
572 'message' => esc_html($message, 'wp-staging'),
573 ]);
574
575 return $response;
576 }
577
578 protected function getJobRetryResponse(string $message): TaskResponseDto
579 {
580 $response = new TaskResponseDto();
581 $response->setIsRunning(true);
582 $response->setJobStatus('JOB_RETRY');
583 $response->addMessage([
584 'type' => 'warning',
585 'date' => $this->getFormattedDate(),
586 'message' => esc_html($message, 'wp-staging'),
587 ]);
588
589 return $response;
590 }
591
592
593
594
595 private function getFormattedDate()
596 {
597 return current_time(Logger::LOG_DATETIME_FORMAT);
598 }
599 }
600