diff --git a/README.md b/README.md index 64a78fc..97c7b07 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,13 @@ Execute WordPress cron events in parallel. Docket CronWP is a command-line tool for executing WordPress cron events in parallel. +This tool is part of the [Docket Cache](https://docketcache.com) project. + ## Requirements - UNIX-like environment (OS X, Linux, FreeBSD, Cygwin, WSL) - PHP >= 7.2.5 - WordPress >= 5.4 -- PHP pnctl extension +- PHP [pnctl](https://www.php.net/manual/en/book.pcntl.php) extension ## Installation @@ -39,29 +41,38 @@ Disable the built in WordPress cron in `wp-config.php`: define( 'DISABLE_WP_CRON', true ); ``` +Updating to the latest version: + +```sh +sudo wget https://github.com/nawawi/docket-cronwp/raw/main/bin/docket-cronwp.phar -O /usr/local/bin/cronwp +sudo chmod +x /usr/local/bin/cronwp + +``` + ## Usage ``` $ cronwp -h -Docket CronWP v1.0.8. +Docket CronWP v1.0.9. Execute WordPress cron events in parallel. Usage: - cronwp [|] + docket-cronwp.phar [|] Path: Path to the WordPress files. Options: -p --path Path to the WordPress files. - -j --jobs Run number of events in parallel. + -j --jobs Run number of events in parallel (default: 3). -u --url Multisite target URL. -a --run-now Run all cron event. -t --dry-run Run without execute cron event. - -h --help Display this help message. -q --quiet Suppress output. -v --verbose Display additional output. + --debug Display debugging output. -V --version Display version. + -h --help Display this help message. ``` ## Example @@ -139,6 +150,36 @@ status : success .... ``` +Run with debugging output: + +```sh +cronwp /path-to/wordpress --jobs 3 --run-now --quiet --debug +``` + +Output: +``` +# 1625731052.7421 : Process-begin : 53132 : Processing 15 events, where every 3 events are run in parallel. +# 1625731052.7432 : Forked : 53133 : for event 'docketcache_gc' +# 1625731052.7434 : Callback-begin : 53133 : for event 'docketcache_gc'. +# 1625731052.7833 : Callback-done : 53133 : for event 'docketcache_gc'. +# 1625731052.8051 : Parent-closed : 53133 : for event 'docketcache_gc'. +# 1625731052.8063 : Forked : 53134 : for event 'docketcache_watchproc' +# 1625731052.8064 : Callback-begin : 53134 : for event 'docketcache_watchproc'. +# 1625731052.8501 : Callback-done : 53134 : for event 'docketcache_watchproc'. +# 1625731052.8698 : Parent-closed : 53134 : for event 'docketcache_watchproc'. +# 1625731052.8709 : Forked : 53135 : for event 'wp_privacy_delete_old_export_files' +# 1625731052.8710 : Callback-begin : 53135 : for event 'wp_privacy_delete_old_export_files'. +# 1625731052.8715 : Callback-done : 53135 : for event 'wp_privacy_delete_old_export_files'. +# 1625731052.8918 : Parent-closed : 53135 : for event 'wp_privacy_delete_old_export_files'. +# 1625731052.8923 : Wait-begin : 53132 : Waiting 3 events to finish. +# 1625731052.8923 : Child-closed : 53133 : for event 'docketcache_gc'. +# 1625731052.8926 : Child-closed : 53134 : for event 'docketcache_watchproc'. +# 1625731052.8927 : Child-closed : 53135 : for event 'wp_privacy_delete_old_export_files'. +# 1625731052.8927 : Wait-done : 53132 : Processing next 3 events. + +.... +``` + Run WordPress cron with 3 events execute in parallel using server cron. Edit `/etc/crontab` and insert command below: ``` diff --git a/bin/docket-cronwp.phar b/bin/docket-cronwp.phar index 4482049..ad0a262 100755 Binary files a/bin/docket-cronwp.phar and b/bin/docket-cronwp.phar differ diff --git a/docket-cronwp.php b/docket-cronwp.php index 02891a4..463d292 100644 --- a/docket-cronwp.php +++ b/docket-cronwp.php @@ -34,7 +34,7 @@ 'DOCKET_CRONWP', [ 'NAME' => 'Docket CronWP', - 'VERSION' => '1.0.8', + 'VERSION' => '1.0.9', 'ROOT' => __DIR__, 'SELF' => !empty($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : __FILE__, ] diff --git a/includes/src/Bepart.php b/includes/src/Bepart.php index 942ec83..997b156 100644 --- a/includes/src/Bepart.php +++ b/includes/src/Bepart.php @@ -43,18 +43,11 @@ private function strip_proto($url) private function rowh(string $name, string $pad = ' ', int $minlen = 15) { $len = $minlen - \strlen($name); - - return $name.str_repeat($pad, $len); - } - - private function result_export($data) - { - $output = ''; - foreach ($data as $name => $value) { - $output .= $this->rowh($name).': '.$value.\PHP_EOL; + if ($len < 0) { + $len = $minlen; } - return $output; + return $name.str_repeat($pad, $len); } private function output($text, $is_error = false) @@ -63,6 +56,16 @@ private function output($text, $is_error = false) fwrite($fd, $text); } + private function output_debug($title, $pid, $msg) + { + if (isset($this->args['debug']) && $this->args['debug']) { + $time = $this->rowh(sprintf('%.4F', microtime(true)), ' ', 16); + $title = $this->rowh($title); + $pid = $this->rowh($pid, ' ', 7); + $this->output('# '.$time.': '.$title.': '.$pid.': '.$msg.\PHP_EOL); + } + } + private function get_hash($string) { return substr(md5($string), 0, 12); diff --git a/includes/src/Console.php b/includes/src/Console.php index f6de9b9..04bc9dd 100644 --- a/includes/src/Console.php +++ b/includes/src/Console.php @@ -32,6 +32,7 @@ final class Console 'help' => false, 'version' => false, 'verbose' => false, + 'debug' => false, ]; public function __construct() @@ -83,14 +84,15 @@ private function print_usage() $text .= ' Path to the WordPress files.'.\PHP_EOL.\PHP_EOL; $text .= 'Options:'.\PHP_EOL; $text .= ' -p --path Path to the WordPress files.'.\PHP_EOL; - $text .= ' -j --jobs Run number of events in parallel.'.\PHP_EOL; + $text .= ' -j --jobs Run number of events in parallel (default: '.$this->args['job'].').'.\PHP_EOL; $text .= ' -u --url Multisite target URL.'.\PHP_EOL; $text .= ' -a --run-now Run all cron event.'.\PHP_EOL; $text .= ' -t --dry-run Run without execute cron event.'.\PHP_EOL; - $text .= ' -h --help Display this help message.'.\PHP_EOL; $text .= ' -q --quiet Suppress output.'.\PHP_EOL; $text .= ' -v --verbose Display additional output.'.\PHP_EOL; + $text .= ' --debug Display debugging output.'.\PHP_EOL; $text .= ' -V --version Display version.'.\PHP_EOL; + $text .= ' -h --help Display this help message.'.\PHP_EOL; $this->output($text); } @@ -135,15 +137,14 @@ private function register_args() case 'verbose': $this->args['verbose'] = $this->get_bool($key, false); break; + case 'debug': + $this->args['debug'] = $this->get_bool($key, false); + break; case 'j': case 'jobs': $job = (int) $value; $this->args['job'] = $job < 0 ? 1 : $job; break; - case 'n': - case 'network': - $this->args['network'] = $this->get_bool($key, false); - break; case 'u': case 'url': if (!$this->get_bool($key, false)) { @@ -283,11 +284,16 @@ function ($lockfile) use ($lock_file) { // ctrl+c pcntl_signal(\SIGINT, function ($signo) use ($lock_file) { - $this->output('Exiting.. cleanup lock files.'.\PHP_EOL); - if (is_file($lock_file) && is_writable($lock_file)) { - @unlink($lock_file); + static $done = false; + + if (!$done) { + $done = true; + $this->output('Exiting.. cleanup lock files.'.\PHP_EOL); + if (is_file($lock_file) && is_writable($lock_file)) { + @unlink($lock_file); + } + $this->cleanup(); } - $this->cleanup(); exit(0); }); @@ -346,17 +352,21 @@ function ($arr) use ($wp_get_schedules) { unset($crons, $cronhooks); + $mypid = getmypid(); + $this->output_debug('Process-begin', $mypid, 'Processing '.\count($collect).' events, where every '.$this->args['job'].' events are run in parallel.'); + if (!empty($collect)) { $cnt = 0; $max = $this->args['job']; - + $num = 0; + $all = \count($collect); foreach ($collect as $hook => $args) { ++$cnt; - + ++$num; $this->proc_fork( $hook, function () use ($hook, $args) { - $timer_start = microtime(true); + $timer_start = sprintf('%.4F', microtime(true)); $status = true; $error = ''; $content = ''; @@ -373,7 +383,7 @@ function () use ($hook, $args) { } } - $timer_stop = microtime(true); + $timer_stop = sprintf('%.4F', microtime(true)); $data = [ 'pid' => '', @@ -397,7 +407,18 @@ function () use ($hook, $args) { ); if ($cnt >= $max) { + $this->output_debug('Wait-begin', $mypid, 'Waiting '.$cnt.' events to finish.'); $this->proc_wait(); + + $nx = $all - $num; + if ($nx > $max) { + $nx = $cnt; + } + + if ($nx > 0) { + $this->output_debug('Wait-done', $mypid, 'Processing next '.$nx.' events.'); + } + $cnt = 0; } } @@ -428,6 +449,7 @@ function () use ($hook, $args) { delete_transient('doing_cron'); } + $this->output_debug('Process-done', $mypid, 'Exiting.'); exit(0); } } diff --git a/includes/src/Process.php b/includes/src/Process.php index 8ac2dd8..5b6798c 100644 --- a/includes/src/Process.php +++ b/includes/src/Process.php @@ -51,6 +51,16 @@ private function proc_get($key, $hook) return false; } + private function proc_output($data) + { + $output = ''; + foreach ($data as $name => $value) { + $output .= $this->rowh($name).': '.$value.\PHP_EOL; + } + + return $output; + } + private function proc_fork($name, $callback) { if (!\is_callable($callback)) { @@ -65,46 +75,58 @@ private function proc_fork($name, $callback) return false; } + if ($pid) { $this->pids[$name] = $pid; + $this->output_debug('Forked', $pid, "for event '".$name."'"); + // prevent zombie pcntl_wait($status); + $this->output_debug('Parent-closed', $pid, "for event '".$name."'."); + // we're parent, db already close, reconnect $this->wpdb_reconnect(); return true; } + $pid = getmypid(); + $this->output_debug('Callback-begin', $pid, "for event '".$name."'."); + \call_user_func($callback); + + $this->output_debug('Callback-done', $pid, "for event '".$name."'."); exit(0); } - private function proc_wait($cleanup = false) + private function proc_wait() { if (empty($this->pids)) { return false; } $pids = array_keys($this->pids); - foreach ($pids as $key) { - if (!isset($this->pids[$key])) { + foreach ($pids as $name) { + if (!isset($this->pids[$name])) { continue; } - $pid = $this->pids[$key]; - pcntl_waitpid($this->pids[$key], $status); - unset($this->pids[$key]); + $pid = $this->pids[$name]; + pcntl_waitpid($pid, $status); + unset($this->pids[$name]); + + $this->output_debug('Child-closed', $pid, "for event '".$name."'."); - $result = $this->proc_get($this->key, $key); + $result = $this->proc_get($this->key, $name); if (!empty($result) && \is_array($result)) { if (!$this->args['quiet']) { $time = ($result['timer_stop'] - $result['timer_start']); - $this->output('Executed the cron event \''.$key.'\' in '.number_format($time, 3).'s'.\PHP_EOL); + $this->output('Executed the cron event \''.$name.'\' in '.number_format($time, 3).'s.'.\PHP_EOL); if ($this->args['verbose']) { $result['pid'] = $pid; - $this->output($this->result_export($result).\PHP_EOL); + $this->output($this->proc_output($result).\PHP_EOL); } } }