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
← All changes | tests/JobsRunnerTest.php +195 -28 1.10.17 → 1.10.29 View file →
@@ -8,23 +8,34 @@
8 8 class JobsRunnerTest extends TestCase
9 9 {
10 10 private const ASYNC_ACTION = 'sync_basalam_run_jobs_async';
11 11 private const DISPATCH_LOCK = 'sync_basalam_jobs_runner_async_dispatch_lock';
12 + private const IDLE_PROBE_LOCK = 'sync_basalam_jobs_runner_idle_probe_lock';
12 13
13 14 private $originalRequest;
14 15 private $originalCookie;
16 + private $originalWpdb;
17 + private $hadOriginalWpdb;
18 + private $originalOptions;
19 + private $hadOriginalOptions;
15 20
16 21 protected function setUp(): void
17 22 {
18 23 $this->originalRequest = $_REQUEST;
19 24 $this->originalCookie = $_COOKIE;
25 + $this->hadOriginalWpdb = array_key_exists('wpdb', $GLOBALS);
26 + $this->originalWpdb = $GLOBALS['wpdb'] ?? null;
27 + $this->hadOriginalOptions = array_key_exists('sync_basalam_test_options', $GLOBALS);
28 + $this->originalOptions = $GLOBALS['sync_basalam_test_options'] ?? null;
20 29
21 30 $_REQUEST = [];
22 31 $_COOKIE = [];
32 + $GLOBALS['sync_basalam_test_options'] = [];
23 33
24 34 $GLOBALS['sync_basalam_jobs_runner_test_state'] = [
25 35 'actions' => [],
26 36 'did_actions' => [],
37 + 'current_filter' => '',
27 38 'doing_ajax' => false,
28 39 'transients' => [],
29 40 'transient_reads' => [],
30 41 'transient_writes' => [],
@@ -38,9 +49,20 @@
38 49 {
39 50 $_REQUEST = $this->originalRequest;
40 51 $_COOKIE = $this->originalCookie;
41 52
53 + if ($this->hadOriginalWpdb) {
54 + $GLOBALS['wpdb'] = $this->originalWpdb;
55 + } else {
56 + unset($GLOBALS['wpdb']);
57 + }
58 +
42 59 unset($GLOBALS['sync_basalam_jobs_runner_test_state']);
60 + if ($this->hadOriginalOptions) {
61 + $GLOBALS['sync_basalam_test_options'] = $this->originalOptions;
62 + } else {
63 + unset($GLOBALS['sync_basalam_test_options']);
64 + }
43 65 }
44 66
45 67 public function testRegistersDispatcherOnShutdownAndNeverOnInit(): void
46 68 {
@@ -48,9 +70,9 @@
48 70 $actions = $this->state()['actions'];
49 71
50 72 self::assertArrayHasKey('shutdown', $actions);
51 73 self::assertSame([$runner, 'maybeDispatchAsyncRequest'], $actions['shutdown'][0]['callback']);
52 - self::assertSame(PHP_INT_MAX, $actions['shutdown'][0]['priority']);
74 + self::assertSame(2, $actions['shutdown'][0]['priority']);
53 75 self::assertSame(1, $actions['shutdown'][0]['accepted_args']);
54 76 self::assertArrayNotHasKey('init', $actions);
55 77
56 78 self::assertSame(
@@ -64,29 +86,28 @@
64 86 $actions['wp_ajax_nopriv_' . self::ASYNC_ACTION][0]['callback']
65 87 );
66 88 }
67 89
68 - public function testDispatchLeaseIsWrittenBeforeQueueProbeAndDispatching(): void
90 + public function testDispatchLeaseIsWrittenOnlyWhenQueueHasWork(): void
69 91 {
70 92 $jobManager = new FakeJobManager([true]);
71 93 $runner = $this->newRunner($jobManager);
72 94
73 95 $_COOKIE = ['wordpress_test_cookie' => 'cookie-value'];
96 + $before = time();
74 97 $runner->maybeDispatchAsyncRequest();
75 98
76 99 self::assertSame(
77 - ['get_transient', 'set_transient', 'has_pending_jobs', 'remote_post'],
100 + ['get_transient', 'get_transient', 'has_pending_jobs', 'get_transient', 'set_transient', 'remote_post'],
78 101 $this->state()['events']
79 102 );
80 103 self::assertSame([120], $jobManager->timeouts);
81 - self::assertSame(
82 - [[
83 - 'name' => self::DISPATCH_LOCK,
84 - 'value' => 1,
85 - 'expiration' => 25,
86 - ]],
87 - $this->state()['transient_writes']
88 - );
104 + $writes = $this->state()['transient_writes'];
105 + self::assertCount(1, $writes);
106 + self::assertSame(self::DISPATCH_LOCK, $writes[0]['name']);
107 + self::assertSame(25, $writes[0]['expiration']);
108 + self::assertGreaterThanOrEqual($before + 25, $writes[0]['value']);
109 + self::assertLessThanOrEqual(time() + 25, $writes[0]['value']);
89 110
90 111 $requests = $this->state()['remote_requests'];
91 112 self::assertCount(1, $requests);
92 113 self::assertSame(
@@ -99,59 +120,175 @@
99 120 self::assertSame('test-nonce-for-' . self::ASYNC_ACTION, $requests[0]['args']['body']['nonce']);
100 121 self::assertSame($_COOKIE, $requests[0]['args']['cookies']);
101 122 }
102 123
103 - public function testEmptyQueueStillReservesDispatchLease(): void
124 + public function testShutdownRepairsDatabaseWithoutFinishingFastCgiResponse(): void
104 125 {
126 + $GLOBALS['sync_basalam_jobs_runner_test_state']['current_filter'] = 'shutdown';
127 + $GLOBALS['wpdb'] = new FakeWpdb();
128 +
105 129 $jobManager = new FakeJobManager([false]);
106 130 $runner = $this->newRunner($jobManager);
107 131
108 132 $runner->maybeDispatchAsyncRequest();
109 133
110 - self::assertSame(['get_transient', 'set_transient', 'has_pending_jobs'], $this->state()['events']);
111 - self::assertSame([120], $jobManager->timeouts);
112 134 self::assertSame(
113 - [[
114 - 'name' => self::DISPATCH_LOCK,
115 - 'value' => 1,
116 - 'expiration' => 25,
117 - ]],
118 - $this->state()['transient_writes']
135 + [
136 + 'db_flush',
137 + 'db_check_connection',
138 + 'get_transient',
139 + 'get_transient',
140 + 'has_pending_jobs',
141 + 'set_transient',
142 + ],
143 + $this->state()['events']
119 144 );
145 + self::assertSame([false], $GLOBALS['wpdb']->allowBailValues);
146 + }
147 +
148 + public function testEmptyQueueReservesOnlyShortIdleProbeLease(): void
149 + {
150 + $jobManager = new FakeJobManager([false]);
151 + $runner = $this->newRunner($jobManager);
152 +
153 + $before = time();
154 + $runner->maybeDispatchAsyncRequest();
155 +
156 + self::assertSame(['get_transient', 'get_transient', 'has_pending_jobs', 'set_transient'], $this->state()['events']);
157 + self::assertSame([120], $jobManager->timeouts);
158 + $writes = $this->state()['transient_writes'];
159 + self::assertCount(1, $writes);
160 + self::assertSame(self::IDLE_PROBE_LOCK, $writes[0]['name']);
161 + self::assertSame(5, $writes[0]['expiration']);
162 + self::assertGreaterThanOrEqual($before + 5, $writes[0]['value']);
163 + self::assertLessThanOrEqual(time() + 5, $writes[0]['value']);
120 164 self::assertSame([], $this->state()['remote_requests']);
121 165 }
122 166
123 - public function testEmptyQueueLeasePreventsASecondProbeInTheSameRequest(): void
167 + public function testNewJobBypassesIdleProbeLeaseAndDispatchesImmediately(): void
124 168 {
125 169 $jobManager = new FakeJobManager([false, true]);
126 170 $runner = $this->newRunner($jobManager);
127 171
128 - // The shutdown probe reserves the lease even when no work is found.
129 172 $runner->maybeDispatchAsyncRequest();
130 173 self::assertCount(1, $this->state()['transient_writes']);
131 174
132 - // A second callback in the same request must observe that lease.
175 + $GLOBALS['sync_basalam_jobs_runner_test_state']['current_filter'] = 'sync_basalam_job_created';
133 176 $runner->maybeDispatchAsyncRequest();
134 177
178 + self::assertSame([120, 120], $jobManager->timeouts);
179 + self::assertSame(self::DISPATCH_LOCK, $this->state()['transient_writes'][1]['name']);
180 + self::assertCount(1, $this->state()['remote_requests']);
181 + }
182 +
183 + public function testExistingLockSkipsQueueCheckAndDispatch(): void
184 + {
185 + $GLOBALS['sync_basalam_jobs_runner_test_state']['transients'][self::DISPATCH_LOCK] = time() + 25;
186 + $jobManager = new FakeJobManager([true]);
187 + $runner = $this->newRunner($jobManager);
188 +
189 + $runner->maybeDispatchAsyncRequest();
190 +
191 + self::assertSame(['get_transient', 'get_transient'], $this->state()['events']);
192 + self::assertSame([], $jobManager->timeouts);
193 + self::assertSame([], $this->state()['transient_writes']);
194 + self::assertSame([], $this->state()['remote_requests']);
195 + }
196 +
197 + /** @dataProvider inactiveLeaseCases */
198 + public function testExpiredOrOrphanedLeaseResumesDispatch(
199 + string $name,
200 + string $format,
201 + int $leaseOffset,
202 + ?int $timeoutOffset
203 + ): void {
204 + $this->setLease($name, $format, $leaseOffset, $timeoutOffset);
205 + $jobManager = new FakeJobManager([true]);
206 + $runner = $this->newRunner($jobManager);
207 +
208 + $runner->maybeDispatchAsyncRequest();
209 +
135 210 self::assertSame([120], $jobManager->timeouts);
136 - self::assertCount(1, $this->state()['transient_writes']);
137 - self::assertCount(0, $this->state()['remote_requests']);
211 + self::assertCount(1, $this->state()['remote_requests']);
212 + self::assertSame(self::DISPATCH_LOCK, $this->state()['transient_writes'][0]['name']);
213 + self::assertGreaterThan(time(), $this->state()['transient_writes'][0]['value']);
138 214 }
139 215
140 - public function testExistingLockSkipsQueueCheckAndDispatch(): void
216 + public function inactiveLeaseCases(): array
141 217 {
142 - $GLOBALS['sync_basalam_jobs_runner_test_state']['transients'][self::DISPATCH_LOCK] = 1;
218 + $cases = [];
219 + foreach ([self::DISPATCH_LOCK, self::IDLE_PROBE_LOCK] as $name) {
220 + $cases[$name . ': legacy without timeout'] = [$name, 'legacy', 0, null];
221 + $cases[$name . ': legacy string without timeout'] = [$name, 'legacy_string', 0, null];
222 + $cases[$name . ': legacy with expired timeout'] = [$name, 'legacy', 0, -1];
223 + $cases[$name . ': expired deadline without timeout'] = [$name, 'deadline', -1, null];
224 + $cases[$name . ': expired stored deadline without timeout'] = [$name, 'deadline_string', -1, null];
225 + $cases[$name . ': expired deadline with future timeout'] = [$name, 'deadline', -1, 999];
226 + $cases[$name . ': deadline expires at current second'] = [$name, 'deadline', 0, null];
227 + }
228 + return $cases;
229 + }
230 +
231 + /** @dataProvider activeLeaseCases */
232 + public function testActiveLeasePreservesDispatchThrottle(
233 + string $name,
234 + string $format,
235 + ?int $timeoutOffset,
236 + bool $externalCache
237 + ): void {
238 + $this->setLease($name, $format, 25, $timeoutOffset);
239 + $GLOBALS['sync_basalam_jobs_runner_test_state']['external_object_cache'] = $externalCache;
143 240 $jobManager = new FakeJobManager([true]);
144 241 $runner = $this->newRunner($jobManager);
145 242
146 243 $runner->maybeDispatchAsyncRequest();
147 244
148 - self::assertSame(['get_transient'], $this->state()['events']);
149 245 self::assertSame([], $jobManager->timeouts);
150 246 self::assertSame([], $this->state()['transient_writes']);
151 247 self::assertSame([], $this->state()['remote_requests']);
152 248 }
153 249
250 + public function activeLeaseCases(): array
251 + {
252 + $cases = [];
253 + foreach ([self::DISPATCH_LOCK, self::IDLE_PROBE_LOCK] as $name) {
254 + $cases[$name . ': legacy with valid timeout'] = [$name, 'legacy', 25, false];
255 + $cases[$name . ': legacy string with valid timeout'] = [$name, 'legacy_string', 25, false];
256 + $cases[$name . ': deadline without timeout'] = [$name, 'deadline', null, false];
257 + $cases[$name . ': stored deadline without timeout'] = [$name, 'deadline_string', null, false];
258 + $cases[$name . ': cached legacy without database timeout'] = [$name, 'legacy', null, true];
259 + }
260 + return $cases;
261 + }
262 +
263 + public function testLosingTimeoutDoesNotReleaseAnActiveDispatchLease(): void
264 + {
265 + $jobManager = new FakeJobManager([true, true]);
266 + $runner = $this->newRunner($jobManager);
267 + $runner->maybeDispatchAsyncRequest();
268 + unset($GLOBALS['sync_basalam_test_options']['_transient_timeout_' . self::DISPATCH_LOCK]);
269 +
270 + $runner->maybeDispatchAsyncRequest();
271 +
272 + self::assertSame([120], $jobManager->timeouts);
273 + self::assertCount(1, $this->state()['remote_requests']);
274 + }
275 +
276 + public function testDispatchRechecksLeaseAfterProbingQueue(): void
277 + {
278 + $jobManager = new FakeJobManager([true]);
279 + $jobManager->onProbe = static function (): void {
280 + $GLOBALS['sync_basalam_jobs_runner_test_state']['transients'][self::DISPATCH_LOCK] = time() + 25;
281 + };
282 + $runner = $this->newRunner($jobManager);
283 +
284 + $runner->maybeDispatchAsyncRequest();
285 +
286 + self::assertSame([120], $jobManager->timeouts);
287 + self::assertSame([], $this->state()['remote_requests']);
288 + self::assertSame([], $this->state()['transient_writes']);
289 + }
290 +
154 291 public function testSelfAsyncRequestReturnsBeforeQueueAndLockChecks(): void
155 292 {
156 293 $GLOBALS['sync_basalam_jobs_runner_test_state']['doing_ajax'] = true;
157 294 $_REQUEST['action'] = self::ASYNC_ACTION;
@@ -214,8 +351,18 @@
214 351 $httpBlockService ?? new FakeHttpBlockService(false)
215 352 );
216 353 }
217 354
355 + private function setLease(string $name, string $format, int $leaseOffset, ?int $timeoutOffset): void
356 + {
357 + $value = in_array($format, ['deadline', 'deadline_string'], true) ? time() + $leaseOffset : 1;
358 + if (in_array($format, ['legacy_string', 'deadline_string'], true)) $value = (string) $value;
359 + $GLOBALS['sync_basalam_jobs_runner_test_state']['transients'][$name] = $value;
360 + if ($timeoutOffset !== null) {
361 + $GLOBALS['sync_basalam_test_options']['_transient_timeout_' . $name] = (string) (time() + $timeoutOffset);
362 + }
363 + }
364 +
218 365 private function invokeRunAsyncBatch(JobsRunner $runner): int
219 366 {
220 367 $method = new \ReflectionMethod($runner, 'runAsyncBatch');
221 368 $method->setAccessible(true);
@@ -239,8 +386,9 @@
239 386
240 387 class FakeJobManager
241 388 {
242 389 public $timeouts = [];
390 + public $onProbe;
243 391
244 392 private $pendingResults;
245 393
246 394 public function __construct(array $pendingResults = [])
@@ -251,10 +399,29 @@
251 399 public function hasPendingOrStaleProcessingJobs(int $timeout): bool
252 400 {
253 401 $GLOBALS['sync_basalam_jobs_runner_test_state']['events'][] = 'has_pending_jobs';
254 402 $this->timeouts[] = $timeout;
403 + if ($this->onProbe) ($this->onProbe)();
255 404
256 405 return (bool) array_shift($this->pendingResults);
406 + }
407 +}
408 +
409 +class FakeWpdb
410 +{
411 + public $allowBailValues = [];
412 +
413 + public function flush(): void
414 + {
415 + $GLOBALS['sync_basalam_jobs_runner_test_state']['events'][] = 'db_flush';
416 + }
417 +
418 + public function check_connection($allowBail = true): bool
419 + {
420 + $GLOBALS['sync_basalam_jobs_runner_test_state']['events'][] = 'db_check_connection';
421 + $this->allowBailValues[] = $allowBail;
422 +
423 + return true;
257 424 }
258 425 }
259 426
260 427 class FakeHttpBlockService