PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.5.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.5.0
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 / Backup / Job / AbstractJob.php
wp-staging / Backup / Job Last commit date
Jobs 2 years ago AbstractJob.php 2 years ago JobBackupProvider.php 3 years ago JobProvider.php 3 years ago JobRestoreProvider.php 3 years ago
AbstractJob.php
470 lines
1 <?php
2
3 // TODO PHP7.x; declare(strict_types=1);
4 // TODO PHP7.x; return types && type-hints
5
6 namespace WPStaging\Backup\Job;
7
8 use RuntimeException;
9 use WPStaging\Core\Utils\Logger;
10 use WPStaging\Framework\Adapter\Directory;
11 use WPStaging\Framework\Exceptions\WPStagingException;
12 use WPStaging\Framework\Filesystem\DiskWriteCheck;
13 use WPStaging\Framework\Filesystem\Filesystem;
14 use WPStaging\Framework\Interfaces\ShutdownableInterface;
15 use WPStaging\Framework\Traits\BenchmarkTrait;
16 use WPStaging\Backup\BackupProcessLock;
17 use WPStaging\Backup\Dto\AbstractDto;
18 use WPStaging\Backup\Exceptions\DiskNotWritableException;
19 use WPStaging\Backup\Exceptions\ProcessLockedException;
20 use WPStaging\Backup\Exceptions\TaskHealthException;
21 use WPStaging\Backup\Task\AbstractTask;
22 use WPStaging\Backup\Dto\TaskResponseDto;
23 use WPStaging\Framework\Utils\Cache\Cache;
24 use WPStaging\Core\WPStaging;
25 use WPStaging\Framework\Facades\Sanitize;
26 use WPStaging\Framework\Queue\FinishedQueueException;
27 use WPStaging\Backup\Dto\JobDataDto;
28
29 use function WPStaging\functions\debug_log;
30
31 abstract class AbstractJob implements ShutdownableInterface
32 {
33 use BenchmarkTrait;
34
35 /**
36 * Filter name for storing the number of maximum request retries
37 * @var string
38 */
39 const TEST_FILTER_MAXIMUM_RETRIES = 'wpstg.tests.maximum_retries';
40
41 /** @var JobDataDto */
42 protected $jobDataDto;
43
44 /** @var Cache $jobDataCache Persists the JobDataDto in the filesystem. */
45 private $jobDataCache;
46
47 /** @var string */
48 protected $currentTaskName;
49
50 /** @var AbstractTask */
51 protected $currentTask;
52
53 /** @var Filesystem */
54 protected $filesystem;
55
56 /** @var Directory */
57 protected $directory;
58
59 /** @var BackupProcessLock */
60 protected $processLock;
61
62 /** @var DiskWriteCheck */
63 protected $diskFullCheck;
64
65 /** @var string|false */
66 protected $memoryExhaustErrorTmpFile = false;
67
68 protected $maxRetries = 10;
69
70 public function __construct(
71 Cache $jobDataCache,
72 JobDataDto $jobDataDto,
73 Filesystem $filesystem,
74 Directory $directory,
75 BackupProcessLock $processLock,
76 DiskWriteCheck $diskFullCheck
77 ) {
78 $this->jobDataDto = $jobDataDto;
79 $this->jobDataCache = $jobDataCache;
80 $this->filesystem = $filesystem;
81 $this->directory = $directory;
82
83 $this->jobDataCache->setLifetime(HOUR_IN_SECONDS);
84 $this->jobDataCache->setFilename('jobCache_' . $this::getJobName());
85
86 $this->processLock = $processLock;
87 $this->diskFullCheck = $diskFullCheck;
88 $this->maxRetries = apply_filters(self::TEST_FILTER_MAXIMUM_RETRIES, $this->maxRetries);
89 }
90
91 /**
92 * Persists the Job status to the current cross-request caching system.
93 *
94 * This method will be invoked in the context of the WordPress `shutdown` hook and should
95 * not be invoked out of that context if not with full knowledge of its side-effects.
96 *
97 * @return void The method has the side-effect of persisting the Job status to the caching
98 * system.
99 */
100 public function persist()
101 {
102 if ($this->jobDataDto->isStatusCheck()) {
103 return;
104 }
105
106 try {
107 $this->diskFullCheck->testDiskIsWriteable();
108 } catch (DiskNotWritableException $e) {
109 // no-op, this is handled on the beginning of the next request
110 }
111
112 if ($this->jobDataDto->isFinished() && !$this->jobDataDto->isCleaned()) {
113 $this->cleanup();
114 $this->jobDataDto->setCleaned();
115 return;
116 }
117
118 if ($this->currentTask instanceof AbstractTask) {
119 $this->jobDataDto->setQueueOffset($this->currentTask->getQueue()->getOffset());
120 $this->currentTask->persistStepsDto();
121 }
122
123 $data = $this->jobDataDto->toArray();
124
125 try {
126 $this->jobDataCache->save($data, true);
127 } catch (\Exception $e) {
128 debug_log("Could not persist Job data to cache:" . $e->getMessage());
129 throw new \RuntimeException('Could not persist Job data to cache: ' . $e->getMessage(), 0, $e);
130 }
131 }
132
133 /**
134 * This method will be called in the context of the WordPress `shutdown` action to
135 * persist the Job status once and only once.
136 *
137 * @return void The method has the side-effect of persisting the Job status to the caching
138 * system.
139 */
140 public function onWpShutdown()
141 {
142 $this->persist();
143 }
144
145 /**
146 * @return string
147 * @throws WPStagingException
148 */
149 public static function getJobName()
150 {
151 throw new WPStagingException('Any extending class MUST override the getJobName method.');
152 }
153
154 /** @return array */
155 abstract protected function getJobTasks();
156
157 /** @return TaskResponseDto */
158 abstract protected function execute();
159
160 /** @return void */
161 abstract protected function init();
162
163 /** @return TaskResponseDto */
164 public function prepareAndExecute()
165 {
166 try {
167 // Check if the last request bailed with a Disk Write failure flag.
168 $this->diskFullCheck->hasDiskWriteTestFailed();
169 } catch (DiskNotWritableException $e) {
170 $response = new TaskResponseDto();
171 $response->setIsRunning(false);
172 $response->setJobStatus('JOB_FAIL');
173 $response->addMessage([
174 'type' => 'critical',
175 'date' => $this->getFormattedDate(),
176 'message' => $e->getMessage(),
177 ]);
178
179 $this->jobDataCache->delete();
180
181 return $response;
182 }
183
184 try {
185 try {
186 $this->prepare();
187 } catch (TaskHealthException $e) {
188 $response = new TaskResponseDto();
189
190 if ($e->getCode() === TaskHealthException::CODE_TASK_FAILED_TOO_MANY_TIMES) {
191 // Signal to JavaScript that this Job failed if no further requests should be made.
192 $response->setIsRunning(false);
193 $response->setJobStatus('JOB_FAIL');
194 $response->addMessage([
195 'type' => 'critical',
196 'date' => $this->getFormattedDate(),
197 'message' => $e->getMessage(),
198 ]);
199
200 $this->jobDataCache->delete();
201 } else {
202 $response->setIsRunning(true);
203 $response->setJobStatus('JOB_RETRY');
204 $response->addMessage([
205 'type' => 'warning',
206 'date' => $this->getFormattedDate(),
207 'message' => $e->getMessage(),
208 ]);
209 }
210
211 return $response;
212 } catch (RuntimeException $ex) {
213 $response = new TaskResponseDto();
214
215 $response->setIsRunning(false);
216 $response->setJobStatus('JOB_FAIL');
217
218 $response->addMessage([
219 'type' => 'critical',
220 'date' => $this->getFormattedDate(),
221 'message' => $ex->getMessage(),
222 ]);
223
224 $this->jobDataCache->delete();
225
226 return $response;
227 }
228
229 $this->processLock->lockProcess();
230
231 /** @var TaskResponseDto $response */
232 $response = $this->execute();
233
234 $this->processLock->unlockProcess();
235
236 /*
237 * Let's display the name of the task running now, instead
238 * of the task that just run to the user.
239 *
240 * Since we already popped from the queue to get here,
241 * the current item now is the next.
242 */
243 $nextTask = $this->jobDataDto->getCurrentTask();
244
245 if (is_subclass_of($nextTask, AbstractTask::class)) {
246 $response->setStatusTitle(call_user_func("$nextTask::getTaskTitle"));
247 }
248
249 $this->removeMemoryExhaustErrorTmpFile();
250 return $response;
251 } catch (DiskNotWritableException $e) {
252 /**
253 * Assume a DiskWriteCheck flag has been set, so the next request can pick it up.
254 *
255 * @see DiskWriteCheck::testDiskIsWriteable()
256 * @see DiskWriteCheck::hasDiskWriteTestFailed()
257 */
258 $response = new TaskResponseDto();
259 $response->setIsRunning(false);
260 $response->setJobStatus('JOB_RETRY');
261 $response->addMessage([
262 'type' => 'warning',
263 'date' => $this->getFormattedDate(),
264 'message' => $e->getMessage(),
265 ]);
266
267 return $response;
268 }
269 }
270
271 /**
272 * @return JobDataDto
273 */
274 public function getJobDataDto()
275 {
276 return $this->jobDataDto;
277 }
278
279 /**
280 * @var $jobDataDto JobDataDto
281 */
282 public function setJobDataDto($jobDataDto)
283 {
284 $this->jobDataDto = $jobDataDto;
285 }
286
287 /**
288 * @return void
289 */
290 protected function checkLastTaskHealth()
291 {
292 // Early bail: No task health on a task that is retrying a failed request. We will evaluate that on the next request.
293 if ($this->jobDataDto->getTaskHealthIsRetrying()) {
294 $this->processLock->unlockProcess();
295 $this->jobDataDto->setTaskHealthIsRetrying(false);
296
297 return;
298 }
299
300 if (!$this->jobDataDto->getTaskHealthResponded()) {
301 // This happens when the previous task started but never generated a response.
302 $this->jobDataDto->setTaskHealthSequentialFailedRetries($this->jobDataDto->getTaskHealthSequentialFailedRetries() + 1);
303 $this->jobDataCache->save($this->jobDataDto);
304
305 if ($this->jobDataDto->getTaskHealthSequentialFailedRetries() >= $this->maxRetries) {
306 throw TaskHealthException::taskFailedTooManyTimes();
307 } else {
308 $this->jobDataDto->setTaskHealthIsRetrying(true);
309 throw TaskHealthException::retryingTask($this->jobDataDto->getTaskHealthSequentialFailedRetries(), $this->maxRetries);
310 }
311 }
312 }
313
314 public function prepare()
315 {
316 $data = $this->jobDataCache->get([]);
317
318 if ($data) {
319 $this->jobDataDto->hydrate($data);
320 }
321
322 // From now on, classes that require a JobDataDto will receive this instance.
323 WPStaging::getInstance()->getContainer()->singleton(JobDataDto::class, $this->jobDataDto);
324
325 // TODO RPoC Hack
326 $this->jobDataDto->setStatusCheck(!empty($_GET['action']) && $_GET['action'] === 'wpstg--backups--status');
327
328 if ($this->jobDataDto->isStatusCheck()) {
329 return;
330 }
331
332 if ($this->jobDataDto->isInit()) {
333 $this->cleanup();
334 $this->init();
335 $this->jobDataDto->setCurrentTaskIndex(0);
336 $this->jobDataDto->setCurrentTaskData([]);
337 $this->addTasks($this->getJobTasks());
338 } else {
339 $this->checkLastTaskHealth();
340 }
341
342 $retry = isset($_REQUEST['retry']) ? Sanitize::sanitizeBool($_REQUEST['retry']) : false;
343 try {
344 if ($retry) {
345 $this->processLock->unlockProcess();
346 }
347
348 $this->processLock->checkProcessLocked();
349 } catch (ProcessLockedException $e) {
350 wp_send_json_error($e->getMessage(), $e->getCode());
351 }
352
353 $this->jobDataDto->setInit(false);
354
355 $this->currentTaskName = $this->jobDataDto->getCurrentTask();
356
357 if (empty($this->currentTaskName)) {
358 throw new \RuntimeException('Internal error: Next task of queue job is null or invalid.');
359 }
360
361 /** @var AbstractTask currentTask */
362 $this->currentTask = WPStaging::getInstance()->get($this->currentTaskName);
363
364 if (!$this->currentTask instanceof AbstractTask) {
365 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));
366 }
367
368 if (!$this->jobDataDto instanceof AbstractDto) {
369 throw new \RuntimeException('Job Queue DTO is null or invalid.');
370 }
371
372 $this->currentTask->setJobContext($this);
373 $this->currentTask->setJobDataDto($this->jobDataDto);
374 $this->currentTask->setJobId($this->jobDataDto->getId());
375 $this->currentTask->setJobName($this::getJobName());
376 $this->currentTask->setDebug(defined('WPSTG_DEBUG') && WPSTG_DEBUG);
377
378 // Initialize Task Health Status
379 $this->jobDataDto->setTaskHealthName($this->currentTaskName);
380 $this->jobDataDto->setTaskHealthResponded(false);
381 }
382
383 /** @return AbstractTask */
384 public function getCurrentTask()
385 {
386 return $this->currentTask;
387 }
388
389 /**
390 * @param string $memoryExhaustErrorTmpFile
391 * @return void
392 */
393 public function setMemoryExhaustErrorTmpFile(string $memoryExhaustErrorTmpFile)
394 {
395 $this->memoryExhaustErrorTmpFile = $memoryExhaustErrorTmpFile;
396 }
397
398 protected function removeMemoryExhaustErrorTmpFile()
399 {
400 if ($this->memoryExhaustErrorTmpFile === '') {
401 return;
402 }
403
404 if (file_exists($this->memoryExhaustErrorTmpFile)) {
405 unlink($this->memoryExhaustErrorTmpFile);
406 }
407 }
408
409 protected function cleanup()
410 {
411 // This excludes all files except cache files from deleting i.e. only delete .cache files
412 $this->filesystem->setExcludePaths(['*.*', '!*.cache.php', '!*.cache', '!*.wpstg', '!*.sql']);
413 $this->filesystem->delete($this->directory->getCacheDirectory(), $deleteSelf = false);
414 $this->filesystem->setExcludePaths([]);
415 $this->filesystem->mkdir($this->directory->getCacheDirectory(), true);
416 }
417
418 /**
419 * @param TaskResponseDto $response
420 *
421 * @return TaskResponseDto
422 */
423 protected function getResponse(TaskResponseDto $response)
424 {
425 $this->jobDataDto->setTaskHealthResponded(true);
426 $this->jobDataDto->setTaskHealthSequentialFailedRetries(0);
427
428 $response->setJob(substr($this->findCurrentJob(), 3));
429
430 // Task is not done yet, add it to beginning of the queue again
431 if ($response->isRunning()) {
432 $className = get_class($this->currentTask);
433 }
434
435 try {
436 if (!$response->isRunning()) {
437 $this->jobDataDto->moveToNextTask();
438 }
439 } catch (FinishedQueueException $e) {
440 $this->jobDataDto->setFinished(true);
441
442 return $response;
443 }
444
445 $response->setIsRunning(true);
446
447 return $response;
448 }
449
450 private function findCurrentJob()
451 {
452 $class = explode('\\', static::class);
453
454 return end($class);
455 }
456
457 protected function addTasks(array $tasks = [])
458 {
459 $this->jobDataDto->setTaskQueue($tasks);
460 }
461
462 /**
463 * @return string Formatted date string
464 */
465 private function getFormattedDate()
466 {
467 return current_time(Logger::LOG_DATETIME_FORMAT);
468 }
469 }
470