PluginProbe
Media Cloud Sync / 1.3.0
Media Cloud Sync v1.3.0
1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 All 34 releases
media-cloud-sync / includes / base / bg-runner.php

bg-runner.php in Media Cloud Sync 1.3.0, at includes/base/bg-runner.php

424 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Dudlewebs\WPMCS;
3
4 defined('ABSPATH') || exit;
5
6 class BGRunner {
7 private static $instance = null;
8 private $meta_key = 'dw_bg_runner_meta';
9 private $transient_key = 'dw_bg_runner_state';
10 private $control_transient_key = 'dw_bg_runner_control'; // NEW: separate option for pause/stop requests
11 private $action_hook = 'dw_bg_runner_cron';
12 private $callback_map = []; // type => callback
13 private $state_cache = null; // cached state array
14 private static $lock_duration = 10 * 60; // static lock duration (seconds)
15
16
17 private function __construct() {
18 $this->meta_key = WPMCS_TOKEN . '_bg_runner_meta';
19 $this->control_transient_key = WPMCS_TOKEN . '_bg_runner_control'; // NEW: dynamic control key
20 $this->action_hook = WPMCS_TOKEN . '_bg_runner_cron';
21
22 add_filter('cron_schedules', [$this, 'add_cron_schedules']);
23 add_action($this->action_hook, [$this, 'run_all']);
24
25 // Ensure cron always exists
26 if (!wp_next_scheduled($this->action_hook)) {
27 wp_schedule_event(time(), 'every_minute', $this->action_hook);
28 }
29 }
30
31 public static function instance() {
32 if (!self::$instance) self::$instance = new self();
33 return self::$instance;
34 }
35
36 public function set_callback(string $type, callable $callback) {
37 $this->callback_map[$type] = $callback;
38 }
39
40 public function start(string $type, int $iterations) {
41 $state = $this->get_state();
42 $state[$type]['status'] = 'running';
43 $state[$type]['iterations_total'] = $iterations;
44 $state[$type]['iterations_done'] = 0;
45 $state[$type]['failed_count'] = 0;
46 $this->save_state($state);
47
48 // reset control flags
49 $control = $this->get_control();
50 $control[$type] = $this->default_control_state();
51 $this->save_control($control);
52
53 if (!wp_next_scheduled($this->action_hook)) {
54 wp_schedule_event(time(), 'every_minute', $this->action_hook);
55 }
56 }
57
58 public function pause(string $type) {
59 $control = $this->get_control();
60 $control[$type]['pause_requested'] = true;
61 $this->save_control($control);
62 }
63
64 public function stop(string $type) {
65 $control = $this->get_control();
66 $control[$type]['stop_requested'] = true;
67 $this->save_control($control);
68 }
69
70 public function resume(string $type) {
71 $control = $this->get_control();
72 $control[$type]['pause_requested'] = false;
73 $control[$type]['stop_requested'] = false;
74 $this->save_control($control);
75
76 $state = $this->get_state();
77 if (in_array($state[$type]['status'] ?? 'stopped', ['paused','stopped'], true)) {
78 $state[$type]['status'] = 'running';
79 $this->save_state($state);
80
81 if (!wp_next_scheduled($this->action_hook)) {
82 wp_schedule_event(time(), 'every_minute', $this->action_hook);
83 }
84 }
85 }
86
87 public function status(string $type) {
88 $state = $this->get_state();
89 return $this->format_status($type, $state);
90 }
91
92 public function all_statuses() {
93 $state = $this->get_state();
94 $statuses = [];
95 foreach ($state as $type => $s) {
96 $statuses[$type] = $this->format_status($type, $state);
97 }
98 return $statuses;
99 }
100
101 private function format_status(string $type, array &$state) {
102 $s = $state[$type] ?? $this->default_type_state();
103 $control = $this->get_control();
104 $c = $control[$type] ?? $this->default_control_state();
105
106 $total = $s['iterations_total'] ?? 0;
107 $done = $s['iterations_done'] ?? 0;
108 $failed_count = $s['failed_count'] ?? 0;
109 $remaining = max(0, $total - $done);
110 $percentage = $total > 0 ? round(($done / $total) * 100, 2) : 0;
111
112 $status = [
113 'total' => $total,
114 'processed' => $done,
115 'failed' => $failed_count,
116 'remaining' => $remaining,
117 'percentage' => $percentage,
118 'status' => $s['status'],
119 'last_run' => $s['last_run'] ?? 0,
120 'pause_requested' => $c['pause_requested'] ?? false,
121 'stop_requested' => $c['stop_requested'] ?? false,
122 ];
123
124 if (!empty($s['completed'])) {
125 $status['percentage'] = 100;
126 $status['remaining'] = 0;
127 $status['status'] = 'completed';
128
129 // reset type state after reporting completed
130 $state[$type] = $this->default_type_state();
131 $this->save_state($state);
132 }
133
134 if( $status['status'] === 'stopped' && $s['iterations_done'] < $s['iterations_total'] ) {
135 // reset type state after reporting completed
136 $state[$type] = $this->default_type_state();
137 $this->save_state($state);
138 }
139
140 return $status;
141 }
142
143 public function run_all() {
144 $state = $this->get_state();
145
146 foreach ($state as $type => $s) {
147 // refresh state for each type to pick up changes made during processing of other types
148 $latest_state = $this->get_state(true);
149
150 $s = $latest_state[$type] ?? $s;
151 if (($s['status'] ?? 'stopped') !== 'running') {
152 $latest_control = $this->get_control();
153 $c = $latest_control[$type] ?? $this->default_control_state();
154
155 if($c['stop_requested'] ?? false) {
156 $s['status'] = 'stopped';
157 $latest_state[$type] = $s;
158 $latest_control[$type]['stop_requested'] = false;
159 $this->save_state($latest_state);
160 $this->save_control($latest_control);
161 }
162 continue;
163 };
164 if (!isset($this->callback_map[$type])) continue;
165 if ($this->is_locked($s)) continue;
166
167 $this->lock($latest_state, $type);
168 $this->save_state($latest_state);
169
170 $this->process_iterations($type, $this->callback_map[$type]);
171
172 // Fetch state again to ensure we have the latest
173 $latest_state = $this->get_state(true);
174 $this->unlock($latest_state, $type);
175 $this->save_state($latest_state);
176 }
177
178 // Unschedule cron if all are stopped/completed
179 $all_idle = true;
180 foreach ($latest_state as $s) {
181 if (($s['status'] ?? 'stopped') === 'running') {
182 $all_idle = false;
183 break;
184 }
185 }
186 if ($all_idle && wp_next_scheduled($this->action_hook)) {
187 wp_clear_scheduled_hook($this->action_hook);
188 }
189 }
190
191 private function process_iterations(string $type, callable $callback) {
192 $s = $this->get_state_counts($type, true);
193
194 $max_exec = ini_get('max_execution_time') ? (int) ini_get('max_execution_time') : 30;
195 $max_mem = ini_get('memory_limit') ? $this->return_bytes(ini_get('memory_limit')) : 128*1024*1024;
196 $memory_safe = $max_mem * 0.65;
197 $time_safe = $max_exec * 0.65;
198 $start_time = time();
199
200 while ($s['iterations_done'] < $s['iterations_total']) {
201 // force refresh so pause/stop requests are seen immediately
202 $latest_control = get_transient($this->control_transient_key) ?: [];
203 $latest_c = $latest_control[$type] ?? $this->default_control_state();
204
205 // stop if paused or stopped
206 if (!empty($latest_c['pause_requested']) || !empty($latest_c['stop_requested'])) {
207 break;
208 }
209
210 if ((memory_get_usage(true) > $memory_safe) || (time() - $start_time > $time_safe)) {
211 break;
212 }
213
214 try {
215 $already_done = $s['iterations_done'];
216 $result = call_user_func($callback, $already_done, $s['failed_count'], $s['iterations_total']);
217 if ($result !== true) {
218 $s['failed_count']++;
219 }
220 } catch (\Exception $e) {
221 $s['failed_count']++;
222 }
223
224 $s['iterations_done']++;
225
226 // Update state after each iteration to ensure progress is saved
227 $this->update_state_counts($type, $s, true, false);
228
229 gc_collect_cycles();
230 }
231
232 // Get latest state again
233 $latest_state = $this->get_state(true);
234 $latest_s = $latest_state[$type];
235 $latest_control = $this->get_control();
236 $latest_c = $latest_control[$type] ?? $this->default_control_state();
237 $is_completed = ($latest_s['iterations_done'] ?? 0) >= ($latest_s['iterations_total'] ?? 0);
238
239 // Update state if completed, paused or stopped
240 if ($is_completed || ($latest_c['stop_requested'] ?? false) || ($latest_c['pause_requested'] ?? false)) {
241 $latest_s['status'] = $is_completed || ($latest_c['stop_requested'] ?? false) ? 'stopped' : 'paused';
242 $latest_s['completed'] = $is_completed;
243 $latest_state[$type] = $latest_s;
244 $this->save_state($latest_state);
245
246 $latest_control[$type]['pause_requested'] = false;
247 $latest_control[$type]['stop_requested'] = false;
248 $this->save_control($latest_control);
249 }
250 }
251
252 private function lock(array &$state, string $type) {
253 $state[$type]['lock_until'] = time() + self::$lock_duration;
254 }
255
256 private function unlock(array &$state, string $type) {
257 $state[$type]['lock_until'] = 0;
258 $state[$type]['last_run'] = time();
259 }
260
261 private function is_locked(array $s) {
262 return ($s['lock_until'] ?? 0) && time() < $s['lock_until'];
263 }
264
265 private function return_bytes($val) {
266 $val = trim($val);
267 $last = strtolower($val[strlen($val)-1]);
268 $num = (int)$val;
269 switch($last) {
270 case 'g': $num *= 1024*1024*1024; break;
271 case 'm': $num *= 1024*1024; break;
272 case 'k': $num *= 1024; break;
273 }
274 return $num;
275 }
276
277 public function add_cron_schedules($schedules) {
278 $schedules['every_minute'] = [
279 'interval' => 60,
280 'display' => 'Every Minute'
281 ];
282 return $schedules;
283 }
284
285
286 /**
287 * Get only the count-related fields of the state for a given type.
288 *
289 * @param string $type The type key in the state array.
290 * @param bool $force Reload state from transient/option instead of cache.
291 *
292 * @return array {
293 * @type int $iterations_total
294 * @type int $iterations_done
295 * @type int $failed_count
296 * }
297 */
298 private function get_state_counts($type, $force = false) {
299 $state = $this->get_state($force);
300
301 if (!isset($state[$type])) {
302 return [
303 'iterations_total' => 0,
304 'iterations_done' => 0,
305 'failed_count' => 0,
306 ];
307 }
308
309 return [
310 'iterations_total' => (int) ($state[$type]['iterations_total'] ?? 0),
311 'iterations_done' => (int) ($state[$type]['iterations_done'] ?? 0),
312 'failed_count' => (int) ($state[$type]['failed_count'] ?? 0),
313 ];
314 }
315
316
317 /**
318 * Update only the count-related fields for a given type.
319 *
320 * @param string $type The type key in the state array.
321 * @param array $counts {
322 * @type int $iterations_total
323 * @type int $iterations_done
324 * @type int $failed_count
325 * }
326 * @param bool $update_transient Whether to update transient.
327 * @param bool $update_options Whether to update options.
328 */
329 private function update_state_counts($type, array $counts, $update_transient = true, $update_options = true) {
330 $state = $this->get_state( true ); // full state array
331
332 if (!isset($state[$type])) {
333 $state[$type] = $this->default_type_state();
334 }
335
336 // update only the count fields
337 if (isset($counts['iterations_total'])) {
338 $state[$type]['iterations_total'] = (int) $counts['iterations_total'];
339 }
340 if (isset($counts['iterations_done'])) {
341 $state[$type]['iterations_done'] = (int) $counts['iterations_done'];
342 }
343 if (isset($counts['failed_count'])) {
344 $state[$type]['failed_count'] = (int) $counts['failed_count'];
345 }
346
347 $this->save_state($state, $update_transient, $update_options);
348 }
349
350
351 /**
352 * Get the state of the bg runner.
353 *
354 * If $force is true, the state will be reloaded from the transient or option.
355 * If $force is false and the state cache already exists, the cached state will be returned.
356 *
357 * If the state is not available from the transient, it will be loaded from the option.
358 * The state will then be cached and set as a transient for one week.
359 *
360 * @param bool $force Reload the state from the transient or option.
361 *
362 * @return array The state of the bg runner.
363 */
364 private function get_state($force = false) {
365 // if not forcing and cache already exists, return cached
366 if (!$force && $this->state_cache !== null) {
367 return $this->state_cache;
368 }
369
370 // try transient first
371 $state = get_transient($this->transient_key);
372
373 if ($state !== false) {
374 if ($force) {
375 // update cache with the fresh transient
376 $this->state_cache = $state;
377 }
378 return $state;
379 }
380
381 // fallback to option if transient missing
382 $state = get_option($this->meta_key, []);
383 $this->state_cache = $state;
384 set_transient($this->transient_key, $state, WEEK_IN_SECONDS);
385
386 return $state;
387 }
388
389 private function save_state($state, $update_transient = true, $update_options = true) {
390 if ($this->state_cache === null || $this->state_cache !== $state) {
391 $this->state_cache = $state;
392 if ($update_transient) set_transient($this->transient_key, $state, WEEK_IN_SECONDS);
393 if ($update_options) update_option($this->meta_key, $state, false);
394 }
395 }
396
397 private function default_type_state() {
398 return [
399 'lock_until' => 0,
400 'status' => 'stopped',
401 'iterations_total' => 0,
402 'iterations_done' => 0,
403 'failed_count' => 0,
404 'completed' => false,
405 'last_run' => 0,
406 ];
407 }
408
409 private function get_control() {
410 return get_transient($this->control_transient_key) ?: [];
411 }
412
413 private function save_control($control) {
414 set_transient($this->control_transient_key, $control, WEEK_IN_SECONDS);
415 }
416
417 private function default_control_state() {
418 return [
419 'pause_requested' => false,
420 'stop_requested' => false,
421 ];
422 }
423 }
424