| @@ -41,18 +41,18 @@ | ||
| 41 | 41 | |
| 42 | 42 | unset($GLOBALS['sync_basalam_jobs_runner_test_state']); |
| 43 | 43 | } |
| 44 | 44 | |
| 45 | - public function testRegistersDispatcherOnInitAndNeverOnShutdown(): void | |
| 45 | + public function testRegistersDispatcherOnShutdownAndNeverOnInit(): void | |
| 46 | 46 | { |
| 47 | 47 | $runner = $this->newRunner(); |
| 48 | 48 | $actions = $this->state()['actions']; |
| 49 | 49 | |
| 50 | - self::assertArrayHasKey('init', $actions); | |
| 51 | - self::assertSame([$runner, 'maybeDispatchAsyncRequest'], $actions['init'][0]['callback']); | |
| 52 | - self::assertSame(10, $actions['init'][0]['priority']); | |
| 53 | - self::assertSame(0, $actions['init'][0]['accepted_args']); | |
| 54 | - self::assertArrayNotHasKey('shutdown', $actions); | |
| 50 | + self::assertArrayHasKey('shutdown', $actions); | |
| 51 | + self::assertSame([$runner, 'maybeDispatchAsyncRequest'], $actions['shutdown'][0]['callback']); | |
| 52 | + self::assertSame(PHP_INT_MAX, $actions['shutdown'][0]['priority']); | |
| 53 | + self::assertSame(1, $actions['shutdown'][0]['accepted_args']); | |
| 54 | + self::assertArrayNotHasKey('init', $actions); | |
| 55 | 55 | |
| 56 | 56 | self::assertSame( |
| 57 | 57 | [$runner, 'maybeDispatchAsyncRequest'], |
| 58 | 58 | $actions['sync_basalam_job_created'][0]['callback'] |
| @@ -64,9 +64,9 @@ | ||
| 64 | 64 | $actions['wp_ajax_nopriv_' . self::ASYNC_ACTION][0]['callback'] |
| 65 | 65 | ); |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | - public function testEligibleQueueIsConfirmedBeforeWritingLockAndDispatching(): void | |
| 68 | + public function testDispatchLeaseIsWrittenBeforeQueueProbeAndDispatching(): void | |
| 69 | 69 | { |
| 70 | 70 | $jobManager = new FakeJobManager([true]); |
| 71 | 71 | $runner = $this->newRunner($jobManager); |
| 72 | 72 | |
| @@ -73,9 +73,9 @@ | ||
| 73 | 73 | $_COOKIE = ['wordpress_test_cookie' => 'cookie-value']; |
| 74 | 74 | $runner->maybeDispatchAsyncRequest(); |
| 75 | 75 | |
| 76 | 76 | self::assertSame( |
| 77 | - ['get_transient', 'has_pending_jobs', 'set_transient', 'remote_post'], | |
| 77 | + ['get_transient', 'set_transient', 'has_pending_jobs', 'remote_post'], | |
| 78 | 78 | $this->state()['events'] |
| 79 | 79 | ); |
| 80 | 80 | self::assertSame([120], $jobManager->timeouts); |
| 81 | 81 | self::assertSame( |
| @@ -81,9 +81,9 @@ | ||
| 81 | 81 | self::assertSame( |
| 82 | 82 | [[ |
| 83 | 83 | 'name' => self::DISPATCH_LOCK, |
| 84 | 84 | 'value' => 1, |
| 85 | - 'expiration' => 1, | |
| 85 | + 'expiration' => 25, | |
| 86 | 86 | ]], |
| 87 | 87 | $this->state()['transient_writes'] |
| 88 | 88 | ); |
| 89 | 89 | |
| @@ -99,9 +99,9 @@ | ||
| 99 | 99 | self::assertSame('test-nonce-for-' . self::ASYNC_ACTION, $requests[0]['args']['body']['nonce']); |
| 100 | 100 | self::assertSame($_COOKIE, $requests[0]['args']['cookies']); |
| 101 | 101 | } |
| 102 | 102 | |
| 103 | - public function testEmptyQueueDoesNotWriteLockOrDispatch(): void | |
| 103 | + public function testEmptyQueueStillReservesDispatchLease(): void | |
| 104 | 104 | { |
| 105 | 105 | $jobManager = new FakeJobManager([false]); |
| 106 | 106 | $runner = $this->newRunner($jobManager); |
| 107 | 107 | |
| @@ -106,29 +106,36 @@ | ||
| 106 | 106 | $runner = $this->newRunner($jobManager); |
| 107 | 107 | |
| 108 | 108 | $runner->maybeDispatchAsyncRequest(); |
| 109 | 109 | |
| 110 | - self::assertSame(['get_transient', 'has_pending_jobs'], $this->state()['events']); | |
| 110 | + self::assertSame(['get_transient', 'set_transient', 'has_pending_jobs'], $this->state()['events']); | |
| 111 | 111 | self::assertSame([120], $jobManager->timeouts); |
| 112 | - self::assertSame([], $this->state()['transient_writes']); | |
| 112 | + self::assertSame( | |
| 113 | + [[ | |
| 114 | + 'name' => self::DISPATCH_LOCK, | |
| 115 | + 'value' => 1, | |
| 116 | + 'expiration' => 25, | |
| 117 | + ]], | |
| 118 | + $this->state()['transient_writes'] | |
| 119 | + ); | |
| 113 | 120 | self::assertSame([], $this->state()['remote_requests']); |
| 114 | 121 | } |
| 115 | 122 | |
| 116 | - public function testJobCreatedAfterAnEmptyInitCanStillDispatch(): void | |
| 123 | + public function testEmptyQueueLeasePreventsASecondProbeInTheSameRequest(): void | |
| 117 | 124 | { |
| 118 | 125 | $jobManager = new FakeJobManager([false, true]); |
| 119 | 126 | $runner = $this->newRunner($jobManager); |
| 120 | 127 | |
| 121 | - // The init probe sees no work and must not reserve the dispatch lock. | |
| 128 | + // The shutdown probe reserves the lease even when no work is found. | |
| 122 | 129 | $runner->maybeDispatchAsyncRequest(); |
| 123 | - self::assertSame([], $this->state()['transient_writes']); | |
| 130 | + self::assertCount(1, $this->state()['transient_writes']); | |
| 124 | 131 | |
| 125 | - // A job created later in the same request must still wake the runner. | |
| 132 | + // A second callback in the same request must observe that lease. | |
| 126 | 133 | $runner->maybeDispatchAsyncRequest(); |
| 127 | 134 | |
| 128 | - self::assertSame([120, 120], $jobManager->timeouts); | |
| 135 | + self::assertSame([120], $jobManager->timeouts); | |
| 129 | 136 | self::assertCount(1, $this->state()['transient_writes']); |
| 130 | - self::assertCount(1, $this->state()['remote_requests']); | |
| 137 | + self::assertCount(0, $this->state()['remote_requests']); | |
| 131 | 138 | } |
| 132 | 139 | |
| 133 | 140 | public function testExistingLockSkipsQueueCheckAndDispatch(): void |
| 134 | 141 | { |
| @@ -170,34 +177,53 @@ | ||
| 170 | 177 | self::assertSame(1, $httpBlockService->calls); |
| 171 | 178 | $this->assertNoQueueLockOrDispatchActivity($jobManager); |
| 172 | 179 | } |
| 173 | 180 | |
| 174 | - public function testShutdownGuardPreventsEveryDispatchSideEffect(): void | |
| 181 | + public function testAsyncBatchExitsImmediatelyWhenAnotherRunnerOwnsTheGlobalLock(): void | |
| 175 | 182 | { |
| 176 | - $GLOBALS['sync_basalam_jobs_runner_test_state']['did_actions']['shutdown'] = 1; | |
| 183 | + $jobManager = new FakeJobManager([true]); | |
| 184 | + $jobExecutor = new FakeJobExecutor(false); | |
| 185 | + $runner = $this->newRunner($jobManager, null, $jobExecutor); | |
| 177 | 186 | |
| 178 | - $jobManager = new FakeJobManager([true]); | |
| 179 | - $httpBlockService = new FakeHttpBlockService(false); | |
| 180 | - $runner = $this->newRunner($jobManager, $httpBlockService); | |
| 187 | + self::assertSame(0, $this->invokeRunAsyncBatch($runner)); | |
| 188 | + self::assertSame(1, $jobExecutor->acquireCalls); | |
| 189 | + self::assertSame(0, $jobExecutor->releaseCalls); | |
| 190 | + self::assertSame([], $jobManager->timeouts); | |
| 191 | + } | |
| 181 | 192 | |
| 182 | - $runner->maybeDispatchAsyncRequest(); | |
| 193 | + public function testAsyncBatchReleasesTheGlobalLockWhenTheQueueIsEmpty(): void | |
| 194 | + { | |
| 195 | + $jobManager = new FakeJobManager([false]); | |
| 196 | + $jobExecutor = new FakeJobExecutor(true); | |
| 197 | + $runner = $this->newRunner($jobManager, null, $jobExecutor); | |
| 183 | 198 | |
| 184 | - self::assertSame(0, $httpBlockService->calls); | |
| 185 | - $this->assertNoQueueLockOrDispatchActivity($jobManager); | |
| 199 | + self::assertSame(0, $this->invokeRunAsyncBatch($runner)); | |
| 200 | + self::assertSame(1, $jobExecutor->acquireCalls); | |
| 201 | + self::assertSame(1, $jobExecutor->releaseCalls); | |
| 202 | + self::assertSame([120], $jobManager->timeouts); | |
| 186 | 203 | } |
| 187 | 204 | |
| 188 | 205 | private function newRunner( |
| 189 | 206 | ?FakeJobManager $jobManager = null, |
| 190 | - ?FakeHttpBlockService $httpBlockService = null | |
| 207 | + ?FakeHttpBlockService $httpBlockService = null, | |
| 208 | + $jobExecutor = null | |
| 191 | 209 | ): JobsRunner { |
| 192 | 210 | return new JobsRunner( |
| 193 | 211 | $jobManager ?? new FakeJobManager(), |
| 212 | + $jobExecutor ?? new FakeJobExecutor(), | |
| 194 | 213 | new \stdClass(), |
| 195 | - new \stdClass(), | |
| 196 | 214 | $httpBlockService ?? new FakeHttpBlockService(false) |
| 197 | 215 | ); |
| 198 | 216 | } |
| 199 | 217 | |
| 218 | + private function invokeRunAsyncBatch(JobsRunner $runner): int | |
| 219 | + { | |
| 220 | + $method = new \ReflectionMethod($runner, 'runAsyncBatch'); | |
| 221 | + $method->setAccessible(true); | |
| 222 | + | |
| 223 | + return $method->invoke($runner); | |
| 224 | + } | |
| 225 | + | |
| 200 | 226 | private function assertNoQueueLockOrDispatchActivity(FakeJobManager $jobManager): void |
| 201 | 227 | { |
| 202 | 228 | self::assertSame([], $jobManager->timeouts); |
| 203 | 229 | self::assertSame([], $this->state()['transient_reads']); |
| @@ -246,6 +272,33 @@ | ||
| 246 | 272 | { |
| 247 | 273 | $this->calls++; |
| 248 | 274 | |
| 249 | 275 | return $this->blocked; |
| 276 | + } | |
| 277 | +} | |
| 278 | + | |
| 279 | +class FakeJobExecutor | |
| 280 | +{ | |
| 281 | + public $acquireCalls = 0; | |
| 282 | + public $releaseCalls = 0; | |
| 283 | + | |
| 284 | + private $canAcquire; | |
| 285 | + | |
| 286 | + public function __construct(bool $canAcquire = true) | |
| 287 | + { | |
| 288 | + $this->canAcquire = $canAcquire; | |
| 289 | + } | |
| 290 | + | |
| 291 | + public function acquireGlobalJobsLock(int $timeout = 0): bool | |
| 292 | + { | |
| 293 | + $this->acquireCalls++; | |
| 294 | + | |
| 295 | + return $this->canAcquire; | |
| 296 | + } | |
| 297 | + | |
| 298 | + public function releaseGlobalJobsLock(): bool | |
| 299 | + { | |
| 300 | + $this->releaseCalls++; | |
| 301 | + | |
| 302 | + return true; | |
| 250 | 303 | } |
| 251 | 304 | } |