Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion languages/wp-queue-ru_RU.l10n.php

Large diffs are not rendered by default.

Binary file modified languages/wp-queue-ru_RU.mo
Binary file not shown.
1,305 changes: 763 additions & 542 deletions languages/wp-queue-ru_RU.po

Large diffs are not rendered by default.

862 changes: 524 additions & 338 deletions languages/wp-queue.pot

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Tags: queue, cron, background-processing, jobs, scheduler
Requires at least: 6.0
Tested up to: 6.9
Requires PHP: 8.3
Stable tag: 1.1.0
Stable tag: 1.2.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Expand Down Expand Up @@ -147,6 +147,12 @@ Yes, WP Queue works with WordPress multisite installations.

== Changelog ==

= 1.2.0 =
* Added runtime modes: cron_loopback, daemon, auto
* Added loopback dispatch for immediate queue processing after dispatch()
* Added --daemon flag to `wp queue work` CLI command
* WP-Cron scheduling and loopback handler are now gated by runtime mode

= 1.1.0 =
* Added Redis queue driver (compatible with redis-cache plugin)
* Added Memcached queue driver
Expand All @@ -167,6 +173,9 @@ Yes, WP Queue works with WordPress multisite installations.

== Upgrade Notice ==

= 1.2.0 =
New runtime modes. Use `define('WP_QUEUE_RUNTIME_MODE', 'daemon')` with a separate worker process, or keep the default `cron_loopback` mode for shared hosting.

= 1.1.0 =
New Redis and Memcached drivers for better performance. Set WP_QUEUE_DRIVER to 'auto' for automatic selection.

Expand Down
166 changes: 151 additions & 15 deletions src/Admin/AdminPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace WPQueue\Admin;

use WPQueue\Loopback\LoopbackDispatcher;
use WPQueue\Runtime\RuntimeMode;
use WPQueue\WPQueue;

/**
Expand All @@ -24,7 +26,6 @@ class AdminPage

public function __construct()
{
$this->initTabs();
add_action('init', [$this, 'initTabs'], 5);
add_action('admin_menu', [$this, 'addMenuPage']);
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
Expand Down Expand Up @@ -54,6 +55,7 @@ public function initTabs(): void
],
'system' => [
'status' => ['title' => __('System Status', 'wp-queue'), 'icon' => 'dashicons-heart'],
'daemon' => ['title' => __('Daemon worker', 'wp-queue'), 'icon' => 'dashicons-admin-generic'],
'tools' => ['title' => __('Tools', 'wp-queue'), 'icon' => 'dashicons-admin-tools'],
'help' => ['title' => __('Help', 'wp-queue'), 'icon' => 'dashicons-editor-help'],
],
Expand Down Expand Up @@ -1029,7 +1031,7 @@ protected function renderQueuesDrivers(): void
<p>
<strong><?php echo esc_html__('⚠️ Warning:', 'wp-queue'); ?></strong>
<?php echo esc_html(sprintf(
__('Driver "%s" is configured in wp-config.php, but not available. Falling back to "%s".', 'wp-queue'),
__('Storage backend "%s" is configured in wp-config.php, but not available. Falling back to "%s".', 'wp-queue'),
$configuredDriver,
$currentDriver,
)); ?>
Expand All @@ -1041,7 +1043,7 @@ protected function renderQueuesDrivers(): void
<?php } else { ?>
<div class="notice notice-info" style="margin: 0 0 20px;">
<p>
<strong><?php echo esc_html__('Current driver:', 'wp-queue'); ?></strong>
<strong><?php echo esc_html__('Current storage backend:', 'wp-queue'); ?></strong>
<?php echo esc_html(ucfirst($currentDriver)); ?>
</p>
</div>
Expand All @@ -1050,7 +1052,7 @@ protected function renderQueuesDrivers(): void
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width: 150px;"><?php echo esc_html__('Driver', 'wp-queue'); ?></th>
<th style="width: 150px;"><?php echo esc_html__('Storage backend', 'wp-queue'); ?></th>
<th style="width: 120px;"><?php echo esc_html__('Status', 'wp-queue'); ?></th>
<th><?php echo esc_html__('Description', 'wp-queue'); ?></th>
<th style="width: 100px;"><?php echo esc_html__('Active', 'wp-queue'); ?></th>
Expand Down Expand Up @@ -1097,7 +1099,7 @@ protected function renderQueuesDrivers(): void
</table>

<div class="wp-queue-help" style="margin-top: 20px;">
<h3><?php echo esc_html__('How to change the driver?', 'wp-queue'); ?></h3>
<h3><?php echo esc_html__('How to change the storage backend?', 'wp-queue'); ?></h3>
<p><?php echo esc_html__('Add to wp-config.php:', 'wp-queue'); ?></p>
<pre><code>define('WP_QUEUE_DRIVER', 'database'); // or 'redis', 'memcached', 'sync', 'auto'</code></pre>

Expand Down Expand Up @@ -1853,20 +1855,56 @@ protected function renderSystemStatus(): void
</tbody>
</table>

<?php
$resolvedMode = RuntimeMode::resolveMode();
$loopbackEnabled = LoopbackDispatcher::isEnabled();
$loopbackStatus = $loopbackInfo['status'] ?? 'unknown';
$loopbackOk = $loopbackStatus === 'ok';

if ($resolvedMode === RuntimeMode::MODE_DAEMON) {
$handlerLabel = __('Daemon process', 'wp-queue');
$handlerClass = 'status-completed';
$handlerDescription = __('A daemon worker is expected to process queues. WP-Cron and loopback are disabled.', 'wp-queue');
} elseif ($loopbackEnabled && $loopbackOk) {
$handlerLabel = __('WP-Cron + Loopback', 'wp-queue');
$handlerClass = 'status-completed';
$handlerDescription = __('Loopback is enabled and working. Jobs start immediately after dispatch.', 'wp-queue');
} elseif ($loopbackEnabled) {
$handlerLabel = __('WP-Cron + Loopback', 'wp-queue');
$handlerClass = 'status-pending';
$handlerDescription = __('Loopback is inactive. Jobs will be processed only by WP-Cron, which can delay execution by up to one minute. Enable loopback or run a daemon worker for instant processing.', 'wp-queue');
} else {
$handlerLabel = __('WP-Cron only', 'wp-queue');
$handlerClass = 'status-pending';
$handlerDescription = __('Loopback is disabled. Jobs will be processed only by WP-Cron, which can delay execution by up to one minute. Run a daemon worker for instant processing.', 'wp-queue');
}

$wpCronDisabled = $report['wp_cron_disabled'] ?? false;
?>
<!-- Статус компонентов -->
<h2><?php echo esc_html__('Component Status', 'wp-queue'); ?></h2>
<table class="wp-list-table widefat fixed striped">
<tbody>
<tr>
<th scope="row" style="width:200px;"><?php echo esc_html__('Queue Driver', 'wp-queue'); ?></th>
<th scope="row" style="width:200px;"><?php echo esc_html__('Queue Storage backend', 'wp-queue'); ?></th>
<td>
<span class="status-badge status-completed"><?php echo esc_html(ucfirst($driver)); ?></span>
</td>
</tr>
<tr>
<th scope="row"><?php echo esc_html__('Queue handler', 'wp-queue'); ?></th>
<td>
<span class="status-badge <?php echo esc_attr($handlerClass); ?>"><?php echo esc_html($handlerLabel); ?></span>
<span class="description"><?php echo esc_html($handlerDescription); ?></span>
<?php if (! $wpCronDisabled && $resolvedMode !== RuntimeMode::MODE_DAEMON) { ?>
<span class="description"><?php echo esc_html__('WP-Cron checks the queue every minute as a fallback.', 'wp-queue'); ?></span>
<?php } ?>
</td>
</tr>
<tr>
<th scope="row"><?php echo esc_html__('WP-Cron', 'wp-queue'); ?></th>
<td>
<?php if ($cronInfo['disabled'] ?? false) { ?>
<?php if ($wpCronDisabled) { ?>
<span class="status-badge status-failed"><?php echo esc_html__('Disabled', 'wp-queue'); ?></span>
<span class="description"><?php echo esc_html__('Configure system cron', 'wp-queue'); ?></span>
<?php } else { ?>
Expand All @@ -1878,16 +1916,19 @@ protected function renderSystemStatus(): void
<th scope="row"><?php echo esc_html__('Loopback', 'wp-queue'); ?></th>
<td>
<?php
$loopbackStatus = $loopbackInfo['status'] ?? 'unknown';
$statusClass = match ($loopbackStatus) {
'ok' => 'completed',
'warning' => 'pending',
default => 'failed',
$lbBadgeClass = match ($loopbackStatus) {
'ok' => 'status-completed',
'warning' => 'status-pending',
default => 'status-failed',
};
$lbStatusLabel = match ($loopbackStatus) {
'ok' => __('OK', 'wp-queue'),
'warning' => __('Warning', 'wp-queue'),
'error' => __('Error', 'wp-queue'),
default => __('Unknown', 'wp-queue'),
};
?>
<span class="status-badge status-<?php echo esc_attr($statusClass); ?>">
<?php echo esc_html(ucfirst($loopbackStatus)); ?>
</span>
<span class="status-badge <?php echo esc_attr($lbBadgeClass); ?>"><?php echo esc_html($lbStatusLabel); ?></span>
<?php if ($loopbackStatus !== 'ok' && isset($loopbackInfo['message'])) { ?>
<span class="description"><?php echo esc_html($loopbackInfo['message']); ?></span>
<?php } ?>
Expand Down Expand Up @@ -2235,6 +2276,101 @@ protected function renderSchedulerPaused(): void
</div>
<?php
}

/**
* Инструкция по запуску демон-воркера
*/
protected function renderSystemDaemon(): void
{
?>
<div class="wp-queue-content-wrapper">
<div class="wp-queue-section-header">
<h1><?php echo esc_html__('Daemon worker', 'wp-queue'); ?></h1>
<p class="description"><?php echo esc_html__('How to run WP Queue as a long-running background process.', 'wp-queue'); ?></p>
</div>

<div class="wp-queue-help">
<h2><?php echo esc_html__('When to use a daemon', 'wp-queue'); ?></h2>
<p><?php echo esc_html__('Use a daemon worker on VPS, Docker or dedicated servers where you can keep a process running. It polls the queue continuously and starts jobs immediately. Without a daemon, WP Queue relies on WP-Cron and/or loopback, which can delay jobs by up to a minute.', 'wp-queue'); ?></p>

<h2><?php echo esc_html__('1. Enable daemon runtime mode', 'wp-queue'); ?></h2>
<p><?php echo esc_html__('Add this to wp-config.php:', 'wp-queue'); ?></p>
<pre><code>define('WP_QUEUE_RUNTIME_MODE', 'daemon');</code></pre>
<p class="description"><?php echo esc_html__('In this mode WP-Cron scheduling and loopback requests are disabled automatically.', 'wp-queue'); ?></p>

<h2><?php echo esc_html__('2. Run the worker', 'wp-queue'); ?></h2>

<h3><?php echo esc_html__('With WP-CLI', 'wp-queue'); ?></h3>
<p><?php echo esc_html__('Start the worker from the WordPress root:', 'wp-queue'); ?></p>
<pre><code>wp queue work --daemon --memory=256</code></pre>
<p class="description"><?php echo esc_html__('--memory limits the PHP memory for the worker process. Adjust the value to your needs.', 'wp-queue'); ?></p>

<h3><?php echo esc_html__('Without WP-CLI', 'wp-queue'); ?></h3>
<p><?php echo esc_html__('If WP-CLI is not available, create a PHP script that boots WordPress and runs the worker loop:', 'wp-queue'); ?></p>
<pre><code>&lt;?php
define('WP_QUEUE_DAEMON', true);
define('WP_QUEUE_RUNTIME_MODE', 'daemon');

require '/var/www/html/wordpress/wp-load.php';

$queue = $argv[1] ?? 'default';
$memory = 256;

ini_set('memory_limit', "{$memory}M");
set_time_limit(0);

WPQueue\WPQueue::worker()->setMemoryLimit($memory);
WPQueue\WPQueue::worker()->daemon($queue);</code></pre>
<p class="description"><?php echo esc_html__('Save the script, make it executable and run it in the background:', 'wp-queue'); ?></p>
<pre><code>chmod +x /var/www/html/wp-queue-daemon.php
nohup php /var/www/html/wp-queue-daemon.php > /var/log/wp-queue-daemon.log 2>&1 &amp;</code></pre>

<h2><?php echo esc_html__('3. Keep it running', 'wp-queue'); ?></h2>
<p><?php echo esc_html__('The command above must stay alive. In production use one of the options below.', 'wp-queue'); ?></p>

<h3><?php echo esc_html__('Option A: systemd service', 'wp-queue'); ?></h3>
<pre><code>[Unit]
Description=WP Queue daemon worker
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/html
ExecStart=/usr/local/bin/wp queue work --daemon --memory=256
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target</code></pre>
<p class="description"><?php echo esc_html__('Save as /etc/systemd/system/wp-queue-worker.service, then run systemctl enable --now wp-queue-worker.', 'wp-queue'); ?></p>

<h3><?php echo esc_html__('Option B: Supervisor', 'wp-queue'); ?></h3>
<pre><code>[program:wp-queue-worker]
command=/usr/local/bin/wp queue work --daemon --memory=256
directory=/var/www/html
user=www-data
autostart=true
autorestart=true
stderr_logfile=/var/log/wp-queue-worker.err.log
stdout_logfile=/var/log/wp-queue-worker.out.log</code></pre>

<h3><?php echo esc_html__('Option C: Docker Compose', 'wp-queue'); ?></h3>
<pre><code>worker:
image: your-wordpress-image
command: ["wp", "queue", "work", "--daemon", "--memory=256"]
restart: unless-stopped</code></pre>

<h3><?php echo esc_html__('Option D: plain PHP script', 'wp-queue'); ?></h3>
<p><?php echo esc_html__('Use the script from step 2 (Without WP-CLI) inside screen, tmux or a simple cron-based restart loop. Remember that the script must stay alive to act as a daemon.', 'wp-queue'); ?></p>

<h2><?php echo esc_html__('4. Check status', 'wp-queue'); ?></h2>
<pre><code>wp queue status</code></pre>
<p><?php echo esc_html__('If the worker is not running, queues will not be processed in daemon mode. Make sure the process is started and monitored.', 'wp-queue'); ?></p>
</div>
</div>
<?php
}
}

if (! function_exists('class_basename')) {
Expand Down
41 changes: 32 additions & 9 deletions src/CLI/QueueCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,30 @@ public function status(array $args, array $assocArgs): void
* ---
*
* [--daemon]
* : Run the worker continuously as a daemon.
* : Run as a daemon worker that continuously polls the queue.
* In daemon mode loopback requests and WP-Cron scheduling are skipped
* on the current process so the worker does not fight with itself.
*
* ## EXAMPLES
*
* wp queue work
* wp queue work emails --limit=50
* wp queue work imports --memory=512
* wp queue work --daemon --memory=256
* wp queue work --daemon
* wp queue work imports --daemon --memory=512
*
* @when after_wp_load
*/
public function work(array $args, array $assocArgs): void
{
$queue = $args[0] ?? 'default';

if (! empty($assocArgs['daemon'])) {
$this->runDaemon($queue, $assocArgs);

return;
}

$limit = (int) ($assocArgs['limit'] ?? 100);

// Get default memory limit from WordPress
Expand All @@ -132,13 +142,6 @@ public function work(array $args, array $assocArgs): void
$worker = WPQueue::worker();
$worker->setMemoryLimit($memory);

if (isset($assocArgs['daemon'])) {
WP_CLI::log("Starting daemon worker for queue: {$queue}");
$worker->daemon($queue);

return;
}

$processed = 0;

while ($processed < $limit) {
Expand All @@ -152,6 +155,26 @@ public function work(array $args, array $assocArgs): void
WP_CLI::success("Processed {$processed} jobs from queue: {$queue}");
}

/**
* Run a daemon worker that continuously polls the queue.
*/
protected function runDaemon(string $queue, array $assocArgs): void
{
// Mark this process as the daemon so loopback/cron do not trigger
// additional workers inside the same process.
if (! defined('WP_QUEUE_DAEMON')) {
define('WP_QUEUE_DAEMON', true);
}

$memory = (int) ($assocArgs['memory'] ?? 256);

WP_CLI::log("Starting daemon worker for queue: {$queue} (memory limit: {$memory}MB)");

$worker = WPQueue::worker();
$worker->setMemoryLimit($memory);
$worker->daemon($queue);
}

/**
* Clear all jobs from a queue.
*
Expand Down
Loading