PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.0
4.11.0 4.10.0 4.9.5 4.9.4 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 / Framework / BackgroundProcessing / BackgroundProcessingServiceProvider.php
wp-staging / Framework / BackgroundProcessing Last commit date
Exceptions 1 day ago Job 1 day ago Action.php 1 day ago BackgroundProcessingServiceProvider.php 1 day ago Demo.php 1 day ago FeatureDetection.php 1 day ago Queue.php 1 day ago QueueActionAware.php 1 day ago QueueProcessor.php 1 day ago WithQueueAwareness.php 1 day ago
BackgroundProcessingServiceProvider.php
344 lines
1 <?php
2
3
4
5
6
7
8
9
10
11
12 namespace WPStaging\Framework\BackgroundProcessing;
13
14 use WPStaging\Core\Cron\Cron;
15 use WPStaging\Framework\Adapter\Database;
16 use WPStaging\Framework\Adapter\Database\InterfaceDatabaseClient;
17 use WPStaging\Framework\DI\FeatureServiceProvider;
18
19 use function WPStaging\functions\debug_log;
20
21 /**
22 * Class BackgroundProcessingServiceProvider
23 *
24 * @property \tad_DI52_Container container
25 * @package WPStaging\Framework\BackgroundProcessing
26 */
27 class BackgroundProcessingServiceProvider extends FeatureServiceProvider
28 {
29
30 const ACTION_QUEUE_MAINTAIN = 'wpstg_queue_maintain';
31
32
33 const TRANSIENT_STALL_PROBE_LOCK = 'wpstg_queue_stall_probe_lock';
34
35
36 const TRANSIENT_QUEUE_HAS_WORK = 'wpstg_queue_has_work';
37
38
39 const QUEUE_HAS_WORK_TTL = DAY_IN_SECONDS;
40
41
42 const STALL_PROBE_THROTTLE_SECONDS = 15;
43
44
45 const STALL_IDLE_SECONDS = 20;
46
47
48 const STUCK_PROCESSING_SECONDS = 60;
49
50
51
52
53 public static function getFeatureTrigger()
54 {
55 return 'WPSTG_FEATURE_ENABLE_BACKGROUND_PROCESSING';
56 }
57
58
59
60
61
62
63 public function register()
64 {
65
66 if (!static::isEnabledInProduction()) {
67 return false;
68 }
69
70 $database = $this->container->make(Database::class)->getClient();
71
72
73 $this->container->when(Queue::class)
74 ->needs(InterfaceDatabaseClient::class)
75 ->give($database);
76
77
78 $this->container->singleton(Queue::class, Queue::class);
79
80 $this->container->singleton(QueueProcessor::class, QueueProcessor::class);
81
82 $this->registerFeatureDetection();
83 $this->scheduleQueueMaintenance();
84 $this->setupQueueProcessingEntrypoints();
85 $this->setupStallDetector();
86
87
88 if (did_action('init')) {
89 $this->scheduleStaticCronEvents();
90 } else {
91 add_action('init', [$this, 'scheduleStaticCronEvents']);
92 }
93
94 return true;
95 }
96
97
98
99
100
101
102 public function runQueueMaintenance()
103 {
104 debug_log('Running Queue Maintenance.', 'info', false);
105
106
107 $queue = $this->container->make(Queue::class);
108
109
110 $queue->markDanglingAs(Queue::STATUS_FAILED);
111
112 $queue->cleanup();
113 }
114
115
116
117
118
119 public function scheduleStaticCronEvents()
120 {
121
122 $cron = $this->container->make(Cron::class);
123
124
125 if (!wp_next_scheduled(self::ACTION_QUEUE_MAINTAIN)) {
126 wp_schedule_event($cron->getFirstRunTimestamp(Cron::DAILY), Cron::DAILY, self::ACTION_QUEUE_MAINTAIN);
127 }
128
129
130 if (!wp_next_scheduled(QueueProcessor::ACTION_QUEUE_PROCESS)) {
131 wp_schedule_event($cron->getFirstRunTimestamp(Cron::HOURLY), Cron::HOURLY, QueueProcessor::ACTION_QUEUE_PROCESS);
132 }
133
134
135 if (!wp_next_scheduled(FeatureDetection::ACTION_AJAX_SUPPORT_FEATURE_DETECTION)) {
136 wp_schedule_event($cron->getFirstRunTimestamp(Cron::WEEKLY), Cron::WEEKLY, FeatureDetection::ACTION_AJAX_SUPPORT_FEATURE_DETECTION);
137 }
138 }
139
140
141
142
143
144
145
146
147
148
149
150
151 private function scheduleQueueMaintenance()
152 {
153
154 add_action(self::ACTION_QUEUE_MAINTAIN, [$this, 'runQueueMaintenance']); // phpcs:ignore WPStaging.Security.FirstArgNotAString
155 }
156
157
158
159
160
161
162
163
164
165
166
167
168
169 private function setupQueueProcessingEntrypoints()
170 {
171
172
173
174
175
176 $wpActions = [
177 QueueProcessor::ACTION_QUEUE_PROCESS,
178 'wp_ajax_nopriv_' . QueueProcessor::ACTION_QUEUE_PROCESS,
179 'wp_ajax_' . QueueProcessor::ACTION_QUEUE_PROCESS,
180 ];
181 $queueProcessorProcess = $this->container->callback(QueueProcessor::class, 'process');
182
183 foreach ($wpActions as $wpAction) {
184 if (!has_action($wpAction, $queueProcessorProcess)) {
185 add_action($wpAction, $queueProcessorProcess); // phpcs:ignore WPStaging.Security.FirstArgNotAString -- Queue action callbacks should not take input from request.
186 }
187 }
188
189
190
191
192
193
194
195
196
197
198
199 }
200
201
202
203
204
205 private function setupStallDetector()
206 {
207 add_action('init', [$this, 'detectAndRecoverStall'], 100);
208 }
209
210
211
212
213 public function detectAndRecoverStall()
214 {
215 if (!get_site_transient(self::TRANSIENT_QUEUE_HAS_WORK)) {
216 return;
217 }
218
219 if (function_exists('wp_doing_cron') && wp_doing_cron()) {
220 return;
221 }
222
223 if (defined('DOING_AJAX') && DOING_AJAX) {
224 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
225 $requestAction = isset($_REQUEST['action']) ? sanitize_text_field(wp_unslash($_REQUEST['action'])) : '';
226 if ($requestAction === QueueProcessor::ACTION_QUEUE_PROCESS || $requestAction === FeatureDetection::ACTION_AJAX_TEST) {
227 return;
228 }
229 }
230
231 if (get_site_transient(self::TRANSIENT_STALL_PROBE_LOCK)) {
232 return;
233 }
234
235 set_site_transient(self::TRANSIENT_STALL_PROBE_LOCK, 1, self::STALL_PROBE_THROTTLE_SECONDS);
236
237 try {
238
239 $queue = $this->container->make(Queue::class);
240 } catch (\Throwable $e) {
241 return;
242 }
243
244 $revived = 0;
245 try {
246 $breakpoint = $this->getStuckProcessingBreakpoint();
247 if ($breakpoint !== null) {
248 $revived = (int)$queue->markDanglingAs(Queue::STATUS_READY, $breakpoint, true);
249 if ($revived > 0) {
250 debug_log('[Background Processing] Revived ' . $revived . ' stuck-in-processing action(s). Claim age threshold: ' . self::STUCK_PROCESSING_SECONDS . 's.', 'info', true);
251 }
252 }
253 } catch (\Throwable $e) {
254
255 }
256
257 if ((int)$queue->count(Queue::STATUS_READY) === 0) {
258 if ((int)$queue->count(Queue::STATUS_PROCESSING) === 0) {
259 delete_site_transient(self::TRANSIENT_QUEUE_HAS_WORK);
260 }
261
262 return;
263 }
264
265 if ($revived > 0) {
266 $this->recoverStalledQueue($queue, 0, $revived);
267 return;
268 }
269
270 $lastUpdate = $queue->getLastUpdatedAtTimestamp();
271 if ($lastUpdate === 0) {
272
273 $this->recoverStalledQueue($queue, 0, 0);
274 return;
275 }
276
277 $idleSeconds = time() - $lastUpdate;
278 if ($idleSeconds < self::STALL_IDLE_SECONDS) {
279 return;
280 }
281
282 $this->recoverStalledQueue($queue, $idleSeconds, 0);
283 }
284
285
286
287
288 private function recoverStalledQueue(Queue $queue, $idleSeconds, $revivedCount)
289 {
290 debug_log('[Background Processing] Stall detected: ready=' . $queue->count(Queue::STATUS_READY) . ' idle_seconds=' . (int)$idleSeconds . ' revived=' . (int)$revivedCount . '. Recovering via inline process().', 'info', true);
291
292 try {
293
294 $processor = $this->container->make(QueueProcessor::class);
295 } catch (\Throwable $e) {
296 return;
297 }
298
299 $processor->process();
300 }
301
302
303
304
305 private function getStuckProcessingBreakpoint()
306 {
307 try {
308 $breakpoint = new \DateTimeImmutable(current_time('mysql'));
309 return $breakpoint->setTimestamp($breakpoint->getTimestamp() - self::STUCK_PROCESSING_SECONDS);
310 } catch (\Exception $e) {
311 return null;
312 }
313 }
314
315
316
317
318
319
320
321
322 private function registerFeatureDetection()
323 {
324
325 $updateOption = $this->container->callback(FeatureDetection::class, 'updateAjaxTestOption');
326
327 add_action('wp_ajax_' . FeatureDetection::ACTION_AJAX_TEST, $updateOption); // phpcs:ignore WPStaging.Security.AuthorizationChecked -- Public
328 add_action('wp_ajax_nopriv_' . FeatureDetection::ACTION_AJAX_TEST, $updateOption); // phpcs:ignore WPStaging.Security.AuthorizationChecked -- Public
329
330 $runAjaxFeatureTest = $this->container->callback(FeatureDetection::class, 'runAjaxFeatureTest');
331 add_action(FeatureDetection::ACTION_AJAX_SUPPORT_FEATURE_DETECTION, $runAjaxFeatureTest);
332
333
334 if (
335 is_admin()
336 && filter_input(INPUT_GET, FeatureDetection::AJAX_REQUEST_QUERY_VAR, FILTER_SANITIZE_NUMBER_INT)
337 ) {
338 $runAjaxFeatureTest();
339 wp_redirect(remove_query_arg(FeatureDetection::AJAX_REQUEST_QUERY_VAR));
340 die();
341 }
342 }
343 }
344