PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.3.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.3.1
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
456 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->addTasks($this->getJobTasks());
328 } else {
329 $this->checkLastTaskHealth();
330 }
331
332 $retry = isset($_REQUEST['retry']) ? Sanitize::sanitizeBool($_REQUEST['retry']) : false;
333 try {
334 if ($retry) {
335 $this->processLock->unlockProcess();
336 }
337
338 $this->processLock->checkProcessLocked();
339 } catch (ProcessLockedException $e) {
340 wp_send_json_error($e->getMessage(), $e->getCode());
341 }
342
343 $this->jobDataDto->setInit(false);
344
345 $this->currentTaskName = $this->jobDataDto->getCurrentTask();
346
347 /** @var AbstractTask currentTask */
348 $this->currentTask = WPStaging::getInstance()->get($this->currentTaskName);
349
350 if (!$this->currentTask instanceof AbstractTask) {
351 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));
352 }
353
354 if (!$this->jobDataDto instanceof AbstractDto) {
355 throw new \RuntimeException('Job Queue DTO is null or invalid.');
356 }
357
358 $this->currentTask->setJobContext($this);
359 $this->currentTask->setJobDataDto($this->jobDataDto);
360 $this->currentTask->setJobId($this->jobDataDto->getId());
361 $this->currentTask->setJobName($this::getJobName());
362 $this->currentTask->setDebug(defined('WPSTG_DEBUG') && WPSTG_DEBUG);
363
364 // Initialize Task Health Status
365 $this->jobDataDto->setTaskHealthName($this->currentTaskName);
366 $this->jobDataDto->setTaskHealthResponded(false);
367 }
368
369 /** @return AbstractTask */
370 public function getCurrentTask()
371 {
372 return $this->currentTask;
373 }
374
375 /**
376 * @param string $memoryExhaustErrorTmpFile
377 * @return void
378 */
379 public function setMemoryExhaustErrorTmpFile(string $memoryExhaustErrorTmpFile)
380 {
381 $this->memoryExhaustErrorTmpFile = $memoryExhaustErrorTmpFile;
382 }
383
384 protected function removeMemoryExhaustErrorTmpFile()
385 {
386 if ($this->memoryExhaustErrorTmpFile === '') {
387 return;
388 }
389
390 if (file_exists($this->memoryExhaustErrorTmpFile)) {
391 unlink($this->memoryExhaustErrorTmpFile);
392 }
393 }
394
395 protected function cleanup()
396 {
397 // This excludes all files except cache files from deleting i.e. only delete .cache files
398 $this->filesystem->setExcludePaths(['*.*', '!*.cache.php', '!*.cache', '!*.wpstg', '!*.sql']);
399 $this->filesystem->delete($this->directory->getCacheDirectory(), $deleteSelf = false);
400 $this->filesystem->setExcludePaths([]);
401 $this->filesystem->mkdir($this->directory->getCacheDirectory(), true);
402 }
403
404 /**
405 * @param TaskResponseDto $response
406 *
407 * @return TaskResponseDto
408 */
409 protected function getResponse(TaskResponseDto $response)
410 {
411 $this->jobDataDto->setTaskHealthResponded(true);
412 $this->jobDataDto->setTaskHealthSequentialFailedRetries(0);
413
414 $response->setJob(substr($this->findCurrentJob(), 3));
415
416 // Task is not done yet, add it to beginning of the queue again
417 if ($response->isRunning()) {
418 $className = get_class($this->currentTask);
419 }
420
421 try {
422 if (!$response->isRunning()) {
423 $this->jobDataDto->moveToNextTask();
424 }
425 } catch (FinishedQueueException $e) {
426 $this->jobDataDto->setFinished(true);
427
428 return $response;
429 }
430
431 $response->setIsRunning(true);
432
433 return $response;
434 }
435
436 private function findCurrentJob()
437 {
438 $class = explode('\\', static::class);
439
440 return end($class);
441 }
442
443 protected function addTasks(array $tasks = [])
444 {
445 $this->jobDataDto->setTaskQueue($tasks);
446 }
447
448 /**
449 * @return string Formatted date string
450 */
451 private function getFormattedDate()
452 {
453 return current_time(Logger::LOG_DATETIME_FORMAT);
454 }
455 }
456