PluginProbe
WP-Stateless – Google Cloud Storage / 3.1.0
WP-Stateless – Google Cloud Storage v3.1.0
4.4.3 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.3.0 2.3.1 2.3.2 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 All 62 releases
wp-stateless / lib / classes / sync / class-background-sync.php

class-background-sync.php in WP-Stateless – Google Cloud Storage 3.1.0, at lib/classes/sync/class-background-sync.php

463 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace wpCloud\StatelessMedia\Sync;
4
5 // Require lib classes if not yet available
6 if (!class_exists('UDX_WP_Async_Request')) {
7 require_once ud_get_stateless_media()->path('lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-async-request.php', 'dir');
8 }
9
10 if (!class_exists('UDX_WP_Background_Process')) {
11 require_once ud_get_stateless_media()->path('lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-background-process.php', 'dir');
12 }
13
14 use UDX_WP_Background_Process, JsonSerializable;
15
16 /**
17 * Generic background process
18 */
19 abstract class BackgroundSync extends UDX_WP_Background_Process implements ISync, JsonSerializable {
20
21 /**
22 * Cron Healthcheck interval
23 */
24 public $cron_interval;
25
26 /**
27 * Flag to allow sorting
28 */
29 protected $allow_sorting = false;
30
31 /**
32 * Flag to allow setting the limit
33 */
34 protected $allow_limit = false;
35
36 /**
37 * Storage for emergency memory
38 */
39 private $emergency_memory = null;
40
41 /**
42 * Storage for currently processed item
43 */
44 protected $currently_processing_item = null;
45
46 /**
47 * Extend the construct
48 */
49 public function __construct() {
50 // Support different threads for multisite installations
51 $blog_id = get_current_blog_id();
52 $this->action = "{$this->action}_{$blog_id}";
53
54 add_filter('wp_stateless_sync_types', function ($classes) {
55 $classes[$c = get_called_class()] = $c;
56 return $classes;
57 });
58
59 $this->cron_interval = $this->get_healthcheck_cron_interval();
60
61 // Reserve 1MB of RAM for the fallback action
62 $this->emergency_memory = new \SplFixedArray(65536);
63
64 // Register the fallback action to be executed on shutdown
65 register_shutdown_function(function () {
66 // Free up reserved memory
67 $this->emergency_memory = null;
68
69 // Check if we should execute the fallback action
70 if (is_null($err = error_get_last())) return;
71 if ($err['type'] != E_ERROR) return;
72 if (strstr($err['message'], 'memory') === false || strstr($err['message'], 'exhausted') === false) return;
73 if (!$this->is_running()) return;
74 if (!$this->currently_processing_item) return;
75
76 // If we are here, then we shutdown because of `memory exhausted` error
77
78 // Remove already processed and problem items from the current batch
79 $current_batch = $this->get_batch();
80 if ($current_batch && $current_batch->data && is_array($current_batch->data)) {
81 foreach ($current_batch->data as $key => $item) {
82 unset($current_batch->data[$key]);
83 if ($item == $this->currently_processing_item) {
84 $this->log(sprintf(__('Item skipped: %s. Waiting for process to resume.', ud_get_stateless_media()->domain), $this->currently_processing_item));
85 call_user_func([get_class(), 'task'], $this->currently_processing_item);
86 break;
87 }
88 }
89 $current_batch->data = array_values($current_batch->data);
90 }
91
92 // Update current batch directly to the option
93 // because it needs to be updated even if it is empty
94 update_site_option($current_batch->key, $current_batch->data);
95
96 // Add notice
97 $this->save_process_meta([
98 'notice' => sprintf(
99 __("Not enough memory to process the following item '%s' %s: %s. Item skipped. Please, try to increase memory limit or use uploading by chunks: <a target=\"_blank\" href=\"https://wp-stateless.github.io/docs/constants/#wp_stateless_media_upload_chunk_size\">How to use WP_STATELESS_MEDIA_UPLOAD_CHUNK_SIZE setting.</a>", ud_get_stateless_media()->domain),
100 is_numeric($this->currently_processing_item) ? get_the_title($this->currently_processing_item) : $this->currently_processing_item,
101 is_numeric($this->currently_processing_item) ? "(ID: {$this->currently_processing_item})" : '',
102 $err['message']
103 )
104 ]);
105
106 wp_die();
107 });
108
109 parent::__construct();
110 }
111
112 /**
113 * Maybe process queue (extended)
114 *
115 * Checks whether data exists within the queue and that
116 * the process is not already running.
117 */
118 public function maybe_handle() {
119 // Don't lock up other requests while processing
120 session_write_close();
121
122 if ($this->is_process_running()) {
123 // Background process already running.
124 wp_die();
125 }
126
127 if ($this->is_queue_empty()) {
128 // No data to process.
129 wp_die();
130 }
131
132 $this->handle();
133
134 wp_die();
135 }
136
137 /**
138 * Determine sync healthcheck interval
139 */
140 protected function get_healthcheck_cron_interval() {
141 return (defined('WP_STATELESS_SYNC_HEALTHCHECK_INTERVAL') && is_int(WP_STATELESS_SYNC_HEALTHCHECK_INTERVAL)) ? WP_STATELESS_SYNC_HEALTHCHECK_INTERVAL : 1;
142 }
143
144 /**
145 * Get option key for STOPPED option
146 */
147 protected function get_stopped_option_key() {
148 return "{$this->action}_stopped";
149 }
150
151 /**
152 * Determine maximum batch size
153 *
154 * @return int Default is 50
155 */
156 public function get_max_batch_size() {
157 return (defined('WP_STATELESS_SYNC_MAX_BATCH_SIZE') && is_int(WP_STATELESS_SYNC_MAX_BATCH_SIZE)) ? WP_STATELESS_SYNC_MAX_BATCH_SIZE : 50;
158 }
159
160 /**
161 * Get all batches
162 *
163 * @param int $limit 0
164 * @return array
165 */
166 public function get_batches($limit = 0) {
167 global $wpdb;
168
169 if (empty($limit) || !is_int($limit)) {
170 $limit = 0;
171 }
172
173 $table = $wpdb->options;
174 $column = 'option_name';
175 $key_column = 'option_id';
176 $value_column = 'option_value';
177
178 if (is_multisite()) {
179 $table = $wpdb->sitemeta;
180 $column = 'meta_key';
181 $key_column = 'meta_id';
182 $value_column = 'meta_value';
183 }
184
185 $key = $wpdb->esc_like($this->identifier) . '_batch_%';
186
187 $sql = "
188 SELECT *
189 FROM {$table}
190 WHERE {$column} LIKE %s
191 ORDER BY {$key_column} ASC
192 ";
193
194 if (!empty($limit)) {
195 $sql .= " LIMIT {$limit}";
196 }
197
198 $items = $wpdb->get_results($wpdb->prepare($sql, $key));
199
200 $batches = [];
201
202 if (!empty($items)) {
203 $batches = array_map(
204 function ($item) use ($column, $value_column) {
205 $batch = new \stdClass();
206 $batch->key = $item->$column;
207 $batch->data = maybe_unserialize($item->$value_column);
208
209 return $batch;
210 },
211 $items
212 );
213 }
214
215 return $batches;
216 }
217
218 /**
219 * Get one top batch
220 */
221 protected function get_batch() {
222 return array_reduce(
223 $this->get_batches(1),
224 function ($_, $batch) {
225 return $batch;
226 },
227 []
228 );
229 }
230
231 /**
232 * Delete all batches
233 *
234 * @return self
235 */
236 public function delete_all() {
237 $batches = $this->get_batches();
238
239 foreach ($batches as $batch) {
240 $this->delete($batch->key);
241 }
242
243 $this->clear_queue_size();
244 return $this;
245 }
246
247 /**
248 * Stop processing
249 */
250 public function stop() {
251 $this->delete_all();
252 update_site_option($this->get_stopped_option_key(), true);
253 $this->clear_process_meta();
254 $this->log("Stopped");
255 }
256
257 /**
258 * Determine if process is stopped.
259 *
260 * @return bool
261 */
262 public function is_stopped() {
263 $network_id = get_current_network_id();
264 wp_cache_delete("$network_id:notoptions", 'site-options');
265 return boolval(get_site_option($this->get_stopped_option_key()));
266 }
267
268 /**
269 * Update the whole queue size
270 *
271 * @param int $size
272 * @return self
273 */
274 public function update_queue_size($size) {
275 $size = intval($size) + $this->get_queue_size();
276 update_site_option("{$this->action}_queue_size", $size);
277 return $this;
278 }
279
280 /**
281 * Get current queue size
282 */
283 public function get_queue_size() {
284 return intval(get_site_option("{$this->action}_queue_size", 0));
285 }
286
287 /**
288 * Clear the queue size
289 *
290 * @return self
291 */
292 public function clear_queue_size() {
293 delete_site_option("{$this->action}_queue_size");
294 return $this;
295 }
296
297 /**
298 * Clear process meta
299 *
300 * @return self
301 */
302 public function clear_process_meta() {
303 // Clear limits for future starts
304 delete_site_option("{$this->action}_meta");
305 return $this;
306 }
307
308 /**
309 * Save process meta data
310 *
311 * @param array $meta
312 */
313 public function save_process_meta($meta = []) {
314 if (!empty($meta)) {
315 $existing_meta = get_site_option("{$this->action}_meta", []);
316 foreach ($meta as $key => $value) {
317 $existing_meta[$key] = $value;
318 }
319 update_site_option("{$this->action}_meta", $existing_meta);
320 }
321 }
322
323 /**
324 * Get process meta data. All or by the key.
325 *
326 * @param string|bool $key
327 * @return array|string|null
328 */
329 public function get_process_meta($name = false) {
330 $meta = get_site_option("{$this->action}_meta", []);
331 if (false === $name) {
332 return $meta;
333 }
334 return isset($meta[$name]) ? $meta[$name] : null;
335 }
336
337 /**
338 * Extending save queue method
339 *
340 * @return $this
341 */
342 public function save() {
343 $batch_size = is_array($this->data) ? count($this->data) : 1;
344 $this->update_queue_size($batch_size);
345 parent::save();
346 $this->data = [];
347 return $this;
348 }
349
350 /**
351 * Extending complete process method
352 */
353 protected function complete() {
354 parent::complete();
355 $this->clear_process_meta();
356 $this->clear_queue_size();
357 delete_site_option($this->get_stopped_option_key());
358 }
359
360 /**
361 * Remember currently processing item
362 */
363 protected function before_task($item) {
364 $this->currently_processing_item = $item;
365 }
366
367 /**
368 * Common task that should be executed in the end of each subclass task
369 */
370 protected function task($_) {
371 $processedCount = intval($this->get_process_meta('processed'));
372 $this->save_process_meta([
373 'processed' => ++$processedCount,
374 'last_at' => current_time('timestamp')
375 ]);
376 }
377
378 /**
379 * Default name
380 *
381 * @return string
382 */
383 public function get_name() {
384 return __('Background Sync', ud_get_stateless_media()->domain);
385 }
386
387 /**
388 * Default helper window is set to false
389 *
390 * @return HelperWindow|bool
391 */
392 public function get_helper_window() {
393 return false;
394 }
395
396 /**
397 * Process specific notice
398 *
399 * @return array|bool
400 */
401 public function get_process_notice() {
402 $notice = $this->get_process_meta('notice');
403 if (empty($notice)) return [];
404 return [$notice];
405 }
406
407 /**
408 * Is running?
409 */
410 public function is_running() {
411 return !$this->is_queue_empty() || $this->is_process_running();
412 }
413
414 /**
415 * Convert to json
416 *
417 * @return array
418 */
419 public function jsonSerialize() {
420 return [
421 'id' => get_called_class(),
422 'name' => $this->get_name(),
423 'helper' => $this->get_helper_window(),
424 'is_running' => $this->is_running(),
425 'limit' => ($limit = $this->get_process_meta('limit')) ? $limit : 0,
426 'order' => ($order = $this->get_process_meta('order')) ? $order : 'desc',
427 'total_items' => $this->get_total_items(),
428 'queued_items' => $this->get_queue_size(),
429 'processed_items' => ($processed = $this->get_process_meta('processed')) ? $processed : 0,
430 'allow_limit' => $this->allow_limit,
431 'allow_sorting' => $this->allow_sorting,
432 'notice' => $this->get_process_notice()
433 ];
434 }
435
436 /**
437 * Log background process event
438 *
439 * @param string $message
440 * @return bool TRUE on success or FALSE on failure
441 */
442 public function log($message) {
443 $message = strip_tags(sprintf('Background Sync - %s: %s', $this->get_name(), $message));
444
445 if (is_multisite()) {
446 $blog_id = get_current_blog_id();
447 $message = sprintf('[Blog %s] %s', $blog_id, $message);
448 }
449
450 if (!defined('WP_STATELESS_SYNC_LOG')) {
451 return error_log($message);
452 }
453
454 return error_log(date('c') . ": $message\n", 3, WP_STATELESS_SYNC_LOG);
455 }
456
457 /**
458 * Start process.
459 * Should be implemented by subclasses.
460 */
461 abstract public function start();
462 }
463