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