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