PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.29
ووسلام – همگام سازی ووکامرس و باسلام v1.10.29
1.10.29 1.10.28 1.1.26 1.10.27 1.10.24 1.10.25 1.10.23 1.10.22 1.10.21 1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 All 62 releases
sync-basalam / tests / integration / async-worker-health.php

async-worker-health.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.29, at tests/integration/async-worker-health.php

223 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Local WordPress + real MySQL queue/advisory locks. One child is killed mid-job.
4 // docker exec -w /var/www/html/wp-content/plugins/sync-basalam \
5 // basalam-wordpress-local-wordpress-1 php tests/integration/async-worker-health.php
6 defined('DISABLE_WP_CRON') || define('DISABLE_WP_CRON', true);
7 require getenv('WORDPRESS_BOOTSTRAP') ?: dirname(__DIR__, 5) . '/wp-load.php';
8
9 use SyncBasalam\JobManager;
10 use SyncBasalam\Jobs\JobExecutor;
11 use SyncBasalam\Jobs\JobRegistry;
12 use SyncBasalam\Jobs\JobResult;
13 use SyncBasalam\Jobs\JobType;
14 use SyncBasalam\Jobs\LockManager;
15 use SyncBasalam\Jobs\Exceptions\NonRetryableException;
16 use SyncBasalam\JobsRunner;
17 use SyncBasalam\Services\AsyncWorkerHealth;
18 use SyncBasalam\Services\CheckHttpBlockService;
19
20 if (wp_get_environment_type() !== 'local') throw new RuntimeException('Local environment only.');
21
22 $installedRunner = syncBasalamContainer()->get(JobsRunner::class);
23 remove_action('shutdown', [$installedRunner, 'maybeDispatchAsyncRequest'], 2);
24 remove_action('sync_basalam_job_created', [$installedRunner, 'maybeDispatchAsyncRequest'], 10);
25 remove_action('admin_notices', [$installedRunner, 'renderWorkerHealthNotice']);
26
27 class WorkerHealthFixture implements JobType
28 {
29 public function getType(): string { return 'sync_basalam_health_integration'; }
30 public function getPriority(): int { return 0; }
31 public function canRun(): bool { return true; }
32 public function execute(array $payload)
33 {
34 if (($payload['mode'] ?? '') === 'hang') sleep(30);
35 if (($payload['mode'] ?? '') === 'reject') {
36 return JobResult::failed(new NonRetryableException('fixture API rejection (422)', 422));
37 }
38 return JobResult::success();
39 }
40 }
41
42 class FixtureQueue extends JobManager
43 {
44 // Leave other local-development queues alone.
45 public function hasPendingOrStaleProcessingJobs(int $timeout = 120): bool
46 {
47 return $this->getCountJobs(['job_type' => 'sync_basalam_health_integration', 'status' => ['pending', 'processing']]) > 0;
48 }
49 }
50
51 class LiteSpeedHealthFixture extends AsyncWorkerHealth
52 {
53 protected function isLiteSpeed(): bool { return true; }
54 }
55
56 $queue = new FixtureQueue();
57 $lock = new LockManager();
58 $health = new LiteSpeedHealthFixture();
59 $runner = new JobsRunner($queue, new JobExecutor($queue, $lock, new JobRegistry([new WorkerHealthFixture()])), new class {
60 public function process(): void {}
61 }, new CheckHttpBlockService(), $health);
62 remove_action('shutdown', [$runner, 'maybeDispatchAsyncRequest'], 2);
63 remove_action('sync_basalam_job_created', [$runner, 'maybeDispatchAsyncRequest'], 10);
64 $run = new ReflectionMethod($runner, 'runAsyncBatch');
65 $run->setAccessible(true);
66 add_filter('sync_basalam_jobs_runner_async_time_limit', static function () { return 0.15; });
67
68 if (($argv[1] ?? '') === 'worker') {
69 $_SERVER['noabort'] = '1';
70 $run->invoke($runner);
71 exit;
72 }
73
74 $table = $wpdb->prefix . 'sync_basalam_job_manager';
75 $snapshots = [];
76 foreach ([AsyncWorkerHealth::OPTION_KEY, 'sync_basalam_settings', 'sync_basalam_jobs_runner_last_run'] as $name) {
77 $snapshots[$name] = get_option($name, null);
78 }
79 $originalGet = $_GET;
80 $originalServer = $_SERVER;
81 $originalUser = get_current_user_id();
82 $jobs = [];
83 $assertions = 0;
84 $scenarios = [];
85 $child = null;
86 $assert = static function (bool $ok, string $message) use (&$assertions): void {
87 if (!$ok) throw new RuntimeException($message);
88 $assertions++;
89 };
90 $create = static function (string $mode) use ($queue, $wpdb, &$jobs): int {
91 $queue->createJob('sync_basalam_health_integration', 'pending', wp_json_encode(['mode' => $mode]));
92 $id = (int) $wpdb->insert_id;
93 $jobs[] = $id;
94 update_option('sync_basalam_jobs_runner_last_run', 0, false);
95 return $id;
96 };
97 $notice = static function () use ($runner): string {
98 ob_start();
99 $runner->renderWorkerHealthNotice();
100 return ob_get_clean();
101 };
102
103 try {
104 $assert($lock->acquireGlobalJobsLock(), 'Local runner must be idle before testing.');
105 $lock->releaseGlobalJobsLock();
106 $assert($queue->getCountJobs(['status' => 'processing']) === 0, 'Other local jobs must not be processing.');
107 $assert($queue->getCountJobs(['status' => 'pending']) === 0, 'Other local jobs must not be pending during HTTP tests.');
108
109 $response = wp_remote_post(admin_url('admin-ajax.php?action=sync_basalam_run_jobs_async'), [
110 'timeout' => 10,
111 'body' => ['action' => 'sync_basalam_run_jobs_async', 'nonce' => 'invalid-fixture-nonce'],
112 ]);
113 $assert(!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 403, 'Actual AJAX endpoint must reject an invalid nonce.');
114 $response = wp_remote_post(admin_url('admin-ajax.php?action=sync_basalam_run_jobs_async'), [
115 'timeout' => 10,
116 'body' => ['action' => 'sync_basalam_run_jobs_async', 'nonce' => wp_create_nonce('sync_basalam_run_jobs_async')],
117 ]);
118 $json = json_decode(wp_remote_retrieve_body($response), true);
119 $assert(!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200, 'Actual AJAX endpoint must accept a valid nonce.');
120 $assert(($json['success'] ?? false) && ($json['data']['processed'] ?? null) === 0, 'Empty AJAX worker must report zero processed jobs.');
121 $scenarios[] = 'actual HTTP AJAX nonce validation and empty response';
122 delete_option(AsyncWorkerHealth::OPTION_KEY);
123 $settings = get_option('sync_basalam_settings', []);
124 $settings['tasks_per_minute'] = 600;
125 $settings['tasks_per_minute_auto'] = false;
126 update_option('sync_basalam_settings', $settings);
127
128 $_SERVER['noabort'] = '1';
129 $id = $create('success');
130 $assert($run->invoke($runner) === 1, 'Protected real runner must execute a job.');
131 $assert($queue->getJob(['id' => $id]) === null, 'Successful job must be removed normally.');
132 $assert($health->getNotice() === null, 'Healthy worker must not alert.');
133 $scenarios[] = 'actual queue success and normal cleanup';
134
135 $id = $create('reject');
136 $run->invoke($runner);
137 $job = $queue->getJob(['id' => $id]);
138 $assert($job->status === 'failed' && strpos($job->error_message, '422') !== false, 'API rejection must be preserved.');
139 $assert($health->getNotice() === null, 'API rejection must not be a host alert.');
140 $scenarios[] = 'explicit API rejection remains separate';
141
142 unset($_SERVER['noabort']);
143 $create('success');
144 $run->invoke($runner);
145 $assert($health->getNotice()['class'] === 'notice-warning', 'Missing noabort must warn even when a quick job returns.');
146 $scenarios[] = 'missing protection persists after fast completion';
147
148 $_GET['page'] = 'sync_basalam';
149 $admins = get_users(['role' => 'administrator', 'number' => 1]);
150 $assert(!empty($admins), 'A local administrator must exist.');
151 wp_set_current_user($admins[0]->ID);
152 $html = $notice();
153 $assert(strpos($html, 'role="alert"') !== false && strpos($html, 'noabort') !== false, 'Authorized admin sees actionable RTL alert.');
154 $assert(strpos($html, '&lt;IfModule') !== false, 'Technical rule must be escaped.');
155 file_put_contents('/tmp/sync-basalam-health-warning.html', '<!doctype html><meta charset="utf-8"><body dir="rtl">' . $html);
156 wp_set_current_user(0);
157 $assert($notice() === '', 'Anonymous users cannot see admin notice.');
158 wp_set_current_user($admins[0]->ID);
159 $scenarios[] = 'real WordPress permissions and rendered notice';
160
161 $_SERVER['noabort'] = '1';
162 $id = $create('hang');
163 $child = proc_open([PHP_BINARY, __FILE__, 'worker'], [0 => ['pipe', 'r'], 1 => ['file', '/tmp/sync-basalam-health-child.log', 'w'], 2 => ['file', '/tmp/sync-basalam-health-child.log', 'a']], $pipes);
164 $assert(is_resource($child), 'Child worker must start.');
165 $deadline = microtime(true) + 8;
166 do {
167 usleep(100000);
168 $job = $queue->getJob(['id' => $id]);
169 } while ($job->status !== 'processing' && microtime(true) < $deadline);
170 $assert($job->status === 'processing', 'Child must reach execution under real MySQL lock.');
171 $queue->updateJob(['started_at' => time() - 121], ['id' => $id]);
172 $assert(!$lock->acquireGlobalJobsLock(), 'Concurrent request must not acquire live worker lock.');
173 $assert($run->invoke($runner) === 0, 'Concurrent runner must not duplicate execution.');
174 $assert(strpos($notice(), 'اجرای ه�
175 گا�
176 ‌سازی نی�
177 ه‌کاره �
178 توقف شده است.') === false, 'A slow live worker must not be called interrupted.');
179 $scenarios[] = 'real concurrent MySQL lock excludes slow active worker';
180
181 proc_terminate($child, 9);
182 proc_close($child);
183 $child = null;
184 $deadline = microtime(true) + 5;
185 do {
186 usleep(100000);
187 $free = $lock->acquireGlobalJobsLock();
188 } while (!$free && microtime(true) < $deadline);
189 $assert($free, 'Killed worker must release MySQL lock on disconnect.');
190 $lock->releaseGlobalJobsLock();
191 $html = $notice();
192 $assert(strpos($html, 'اجرای ه�
193 گا�
194 ‌سازی نی�
195 ه‌کاره �
196 توقف شده است.') !== false, 'Next admin request must discover interruption without relying on dead worker.');
197 $assert($health->getNotice()['job_id'] === $id, 'Alert must identify interrupted job.');
198 $scenarios[] = 'SIGKILL interruption detected independently';
199
200 $queue->updateJob(['retry_after' => time() + 60], ['id' => $id]);
201 $assert($run->invoke($runner) === 0, 'Batch without eligible jobs must not claim recovery.');
202 $assert($health->getNotice() !== null, 'Empty worker must not clear interruption warning.');
203 $queue->updateJob(['payload' => wp_json_encode(['mode' => 'success']), 'retry_after' => 0], ['id' => $id]);
204 update_option('sync_basalam_jobs_runner_last_run', 0, false);
205 $assert($run->invoke($runner) === 1, 'Stale job must be retried by normal runner.');
206 $assert($queue->getJob(['id' => $id]) === null, 'Retried job must complete normally.');
207 $assert($health->getNotice() === null, 'Protected real execution must clear notice.');
208 $scenarios[] = 'stale recovery followed by protected execution clears warning';
209
210 echo wp_json_encode(['success' => true, 'scenarios' => $scenarios, 'assertions' => $assertions], JSON_PRETTY_PRINT) . PHP_EOL;
211 } finally {
212 if (is_resource($child)) { proc_terminate($child, 9); proc_close($child); }
213 foreach ($jobs as $id) $queue->deleteJob(['id' => $id]);
214 foreach ($snapshots as $name => $value) {
215 if ($value === null) delete_option($name);
216 else update_option($name, $value, false);
217 }
218 $_GET = $originalGet;
219 $_SERVER = $originalServer;
220 wp_set_current_user($originalUser);
221 $lock->releaseGlobalJobsLock();
222 }
223