PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.4.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.4.1
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 / 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
461 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 /**
137 * @return string
138 * @throws WPStagingException
139 */
140 public static function getJobName()
141 {
142 throw new WPStagingException('Any extending class MUST override the getJobName method.');
143 }
144
145 /** @return array */
146 abstract protected function getJobTasks();
147
148 /** @return TaskResponseDto */
149 abstract protected function execute();
150
151 /** @return void */
152 abstract protected function init();
153
154 /** @return TaskResponseDto */
155 public function prepareAndExecute()
156 {
157 try {
158 // Check if the last request bailed with a Disk Write failure flag.
159 $this->diskFullCheck->hasDiskWriteTestFailed();
160 } catch (DiskNotWritableException $e) {
161 $response = new TaskResponseDto();
162 $response->setIsRunning(false);
163 $response->setJobStatus('JOB_FAIL');
164 $response->addMessage([
165 'type' => 'critical',
166 'date' => $this->getFormattedDate(),
167 'message' => $e->getMessage(),
168 ]);
169
170 $this->jobDataCache->delete();
171
172 return $response;
173 }
174
175 try {
176 try {
177 $this->prepare();
178 } catch (TaskHealthException $e) {
179 $response = new TaskResponseDto();
180
181 if ($e->getCode() === TaskHealthException::CODE_TASK_FAILED_TOO_MANY_TIMES) {
182 // Signal to JavaScript that this Job failed if no further requests should be made.
183 $response->setIsRunning(false);
184 $response->setJobStatus('JOB_FAIL');
185 $response->addMessage([
186 'type' => 'critical',
187 'date' => $this->getFormattedDate(),
188 'message' => $e->getMessage(),
189 ]);
190
191 $this->jobDataCache->delete();
192 } else {
193 $response->setIsRunning(true);
194 $response->setJobStatus('JOB_RETRY');
195 $response->addMessage([
196 'type' => 'warning',
197 'date' => $this->getFormattedDate(),
198 'message' => $e->getMessage(),
199 ]);
200 }
201
202 return $response;
203 } catch (RuntimeException $ex) {
204 $response = new TaskResponseDto();
205
206 $response->setIsRunning(false);
207 $response->setJobStatus('JOB_FAIL');
208
209 $response->addMessage([
210 'type' => 'critical',
211 'date' => $this->getFormattedDate(),
212 'message' => $ex->getMessage(),
213 ]);
214
215 $this->jobDataCache->delete();
216
217 return $response;
218 }
219
220 $this->processLock->lockProcess();
221
222 /** @var TaskResponseDto $response */
223 $response = $this->execute();
224
225 $this->processLock->unlockProcess();
226
227 /*
228 * Let's display the name of the task running now, instead
229 * of the task that just run to the user.
230 *
231 * Since we already popped from the queue to get here,
232 * the current item now is the next.
233 */
234 $nextTask = $this->jobDataDto->getCurrentTask();
235
236 if (is_subclass_of($nextTask, AbstractTask::class)) {
237 $response->setStatusTitle(call_user_func("$nextTask::getTaskTitle"));
238 }
239
240 $this->removeMemoryExhaustErrorTmpFile();
241 return $response;
242 } catch (DiskNotWritableException $e) {
243 /**
244 * Assume a DiskWriteCheck flag has been set, so the next request can pick it up.
245 *
246 * @see DiskWriteCheck::testDiskIsWriteable()
247 * @see DiskWriteCheck::hasDiskWriteTestFailed()
248 */
249 $response = new TaskResponseDto();
250 $response->setIsRunning(false);
251 $response->setJobStatus('JOB_RETRY');
252 $response->addMessage([
253 'type' => 'warning',
254 'date' => $this->getFormattedDate(),
255 'message' => $e->getMessage(),
256 ]);
257
258 return $response;
259 }
260 }
261
262 /**
263 * @return JobDataDto
264 */
265 public function getJobDataDto()
266 {
267 return $this->jobDataDto;
268 }
269
270 /**
271 * @var $jobDataDto JobDataDto
272 */
273 public function setJobDataDto($jobDataDto)
274 {
275 $this->jobDataDto = $jobDataDto;
276 }
277
278 /**
279 * @return void
280 */
281 protected function checkLastTaskHealth()
282 {
283 // Early bail: No task health on a task that is retrying a failed request. We will evaluate that on the next request.
284 if ($this->jobDataDto->getTaskHealthIsRetrying()) {
285 $this->processLock->unlockProcess();
286 $this->jobDataDto->setTaskHealthIsRetrying(false);
287
288 return;
289 }
290
291 if (!$this->jobDataDto->getTaskHealthResponded()) {
292 // This happens when the previous task started but never generated a response.
293 $this->jobDataDto->setTaskHealthSequentialFailedRetries($this->jobDataDto->getTaskHealthSequentialFailedRetries() + 1);
294 $this->jobDataCache->save($this->jobDataDto);
295
296 if ($this->jobDataDto->getTaskHealthSequentialFailedRetries() >= 10) {
297 throw TaskHealthException::taskFailedTooManyTimes();
298 } else {
299 $this->jobDataDto->setTaskHealthIsRetrying(true);
300 throw TaskHealthException::retryingTask($this->jobDataDto->getTaskHealthSequentialFailedRetries(), 10);
301 }
302 }
303 }
304
305 public function prepare()
306 {
307 $data = $this->jobDataCache->get([]);
308
309 if ($data) {
310 $this->jobDataDto->hydrate($data);
311 }
312
313 // From now on, classes that require a JobDataDto will receive this instance.
314 WPStaging::getInstance()->getContainer()->singleton(JobDataDto::class, $this->jobDataDto);
315
316 // TODO RPoC Hack
317 $this->jobDataDto->setStatusCheck(!empty($_GET['action']) && $_GET['action'] === 'wpstg--backups--status');
318
319 if ($this->jobDataDto->isStatusCheck()) {
320 return;
321 }
322
323 if ($this->jobDataDto->isInit()) {
324 $this->cleanup();
325 $this->init();
326 $this->jobDataDto->setCurrentTaskIndex(0);
327 $this->jobDataDto->setCurrentTaskData([]);
328 $this->addTasks($this->getJobTasks());
329 } else {
330 $this->checkLastTaskHealth();
331 }
332
333 $retry = isset($_REQUEST['retry']) ? Sanitize::sanitizeBool($_REQUEST['retry']) : false;
334 try {
335 if ($retry) {
336 $this->processLock->unlockProcess();
337 }
338
339 $this->processLock->checkProcessLocked();
340 } catch (ProcessLockedException $e) {
341 wp_send_json_error($e->getMessage(), $e->getCode());
342 }
343
344 $this->jobDataDto->setInit(false);
345
346 $this->currentTaskName = $this->jobDataDto->getCurrentTask();
347
348 if (empty($this->currentTaskName)) {
349 throw new \RuntimeException('Internal error: Next task of queue job is null or invalid.');
350 }
351
352 /** @var AbstractTask currentTask */
353 $this->currentTask = WPStaging::getInstance()->get($this->currentTaskName);
354
355 if (!$this->currentTask instanceof AbstractTask) {
356 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));
357 }
358
359 if (!$this->jobDataDto instanceof AbstractDto) {
360 throw new \RuntimeException('Job Queue DTO is null or invalid.');
361 }
362
363 $this->currentTask->setJobContext($this);
364 $this->currentTask->setJobDataDto($this->jobDataDto);
365 $this->currentTask->setJobId($this->jobDataDto->getId());
366 $this->currentTask->setJobName($this::getJobName());
367 $this->currentTask->setDebug(defined('WPSTG_DEBUG') && WPSTG_DEBUG);
368
369 // Initialize Task Health Status
370 $this->jobDataDto->setTaskHealthName($this->currentTaskName);
371 $this->jobDataDto->setTaskHealthResponded(false);
372 }
373
374 /** @return AbstractTask */
375 public function getCurrentTask()
376 {
377 return $this->currentTask;
378 }
379
380 /**
381 * @param string $memoryExhaustErrorTmpFile
382 * @return void
383 */
384 public function setMemoryExhaustErrorTmpFile(string $memoryExhaustErrorTmpFile)
385 {
386 $this->memoryExhaustErrorTmpFile = $memoryExhaustErrorTmpFile;
387 }
388
389 protected function removeMemoryExhaustErrorTmpFile()
390 {
391 if ($this->memoryExhaustErrorTmpFile === '') {
392 return;
393 }
394
395 if (file_exists($this->memoryExhaustErrorTmpFile)) {
396 unlink($this->memoryExhaustErrorTmpFile);
397 }
398 }
399
400 protected function cleanup()
401 {
402 // This excludes all files except cache files from deleting i.e. only delete .cache files
403 $this->filesystem->setExcludePaths(['*.*', '!*.cache.php', '!*.cache', '!*.wpstg', '!*.sql']);
404 $this->filesystem->delete($this->directory->getCacheDirectory(), $deleteSelf = false);
405 $this->filesystem->setExcludePaths([]);
406 $this->filesystem->mkdir($this->directory->getCacheDirectory(), true);
407 }
408
409 /**
410 * @param TaskResponseDto $response
411 *
412 * @return TaskResponseDto
413 */
414 protected function getResponse(TaskResponseDto $response)
415 {
416 $this->jobDataDto->setTaskHealthResponded(true);
417 $this->jobDataDto->setTaskHealthSequentialFailedRetries(0);
418
419 $response->setJob(substr($this->findCurrentJob(), 3));
420
421 // Task is not done yet, add it to beginning of the queue again
422 if ($response->isRunning()) {
423 $className = get_class($this->currentTask);
424 }
425
426 try {
427 if (!$response->isRunning()) {
428 $this->jobDataDto->moveToNextTask();
429 }
430 } catch (FinishedQueueException $e) {
431 $this->jobDataDto->setFinished(true);
432
433 return $response;
434 }
435
436 $response->setIsRunning(true);
437
438 return $response;
439 }
440
441 private function findCurrentJob()
442 {
443 $class = explode('\\', static::class);
444
445 return end($class);
446 }
447
448 protected function addTasks(array $tasks = [])
449 {
450 $this->jobDataDto->setTaskQueue($tasks);
451 }
452
453 /**
454 * @return string Formatted date string
455 */
456 private function getFormattedDate()
457 {
458 return current_time(Logger::LOG_DATETIME_FORMAT);
459 }
460 }
461