Skip to content

Commit 74d784a

Browse files
authored
Merge pull request #32 from techmahedy/techmahedy-4.x
(feat) Worker: run every job in its own `defer()` scope
2 parents 83742f4 + 441287a commit 74d784a

4 files changed

Lines changed: 107 additions & 0 deletions

File tree

‎src/QueueWorker.php‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,17 @@ protected function executeJobWithTimeout(JobInterface $job): void
420420
*/
421421
protected function executeJob(JobInterface $job): void
422422
{
423+
$app = app();
424+
425+
// Each job is its own execution: callbacks it registers with defer()
426+
// run when it finishes, after success or failure, and never leak
427+
// into the next job handled by this worker.
428+
if (is_object($app) && method_exists($app, 'runInDeferScope')) {
429+
$app->runInDeferScope(static fn() => $job->handle());
430+
431+
return;
432+
}
433+
423434
$job->handle();
424435
}
425436

‎tests/Support/claim_worker.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
* job and kills itself with SIGKILL without acknowledging it.
1111
*/
1212

13+
// Deprecation notices printed by third-party packages while the autoloader loads
14+
// (an old Mockery on PHP 8.5, for example) are noise here; real warnings still show.
15+
error_reporting(E_ALL & ~E_DEPRECATED);
16+
1317
require __DIR__ . '/../../vendor/autoload.php';
1418

1519
use Doppar\Queue\Drivers\DatabaseDriver;

‎tests/Support/slow_job_worker.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
* Usage: php slow_job_worker.php <base64 json spec>
88
*/
99

10+
// Deprecation notices printed by third-party packages while the autoloader loads
11+
// (an old Mockery on PHP 8.5, for example) are noise here; real warnings still show.
12+
error_reporting(E_ALL & ~E_DEPRECATED);
13+
1014
require __DIR__ . '/../../vendor/autoload.php';
1115

1216
use Doppar\Queue\QueueManager;

‎tests/Unit/QueueWorkerTest.php‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,4 +549,92 @@ public function extend(ReservedJob $job, int $seconds): bool
549549

550550
$this->assertStringContainsString('Could not renew job lease: redis timeout', $this->output);
551551
}
552+
553+
// ------------------------------------------------------------------
554+
// Deferred cleanup
555+
// ------------------------------------------------------------------
556+
557+
/**
558+
* Install a container that implements the framework's defer scope API.
559+
*
560+
* @return object{log: array<int, string>}
561+
*/
562+
private function installDeferAwareContainer(): object
563+
{
564+
$container = new class extends MockContainer {
565+
public array $log = [];
566+
567+
public int $scopes = 0;
568+
569+
private array $stack = [];
570+
571+
public function defer(callable $callback): static
572+
{
573+
if ($this->stack === []) {
574+
throw new \LogicException('defer() outside a scope');
575+
}
576+
577+
$this->stack[array_key_last($this->stack)][] = $callback;
578+
579+
return $this;
580+
}
581+
582+
public function runInDeferScope(callable $callback): mixed
583+
{
584+
$this->scopes++;
585+
$this->stack[] = [];
586+
587+
try {
588+
return $callback();
589+
} finally {
590+
foreach (array_reverse(array_pop($this->stack)) as $cleanup) {
591+
$cleanup();
592+
}
593+
}
594+
}
595+
};
596+
597+
Container::setInstance($container);
598+
$container->singleton(QueueManager::class, fn() => $this->manager);
599+
$container->alias(QueueManager::class, 'queue.worker');
600+
601+
return $container;
602+
}
603+
604+
public function testEachJobRunsInItsOwnDeferScope(): void
605+
{
606+
$container = $this->installDeferAwareContainer();
607+
608+
RecordingJob::$during = function (RecordingJob $job) use ($container) {
609+
$label = $job->label;
610+
$container->defer(function () use ($container, $label) { $container->log[] = "cleanup-{$label}"; });
611+
$container->log[] = "run-{$label}";
612+
};
613+
614+
$this->manager->push(new RecordingJob('a'));
615+
$this->manager->push(new RecordingJob('b'));
616+
617+
$this->runOne();
618+
$this->runOne();
619+
620+
$this->assertSame(2, $container->scopes);
621+
$this->assertSame(['run-a', 'cleanup-a', 'run-b', 'cleanup-b'], $container->log);
622+
}
623+
624+
public function testDeferredCallbacksRunWhenAJobFailsBeforeItIsReleased(): void
625+
{
626+
$container = $this->installDeferAwareContainer();
627+
628+
RecordingJob::$during = function () use ($container) {
629+
$container->defer(function () use ($container) { $container->log[] = 'cleanup'; });
630+
631+
throw new \RuntimeException('job failed');
632+
};
633+
634+
$this->manager->push(new RecordingJob('a'));
635+
$this->runOne();
636+
637+
$this->assertSame(['cleanup'], $container->log);
638+
$this->assertSame(1, $this->driver()->stats('default')['delayed'] + $this->driver()->countFailed());
639+
}
552640
}

0 commit comments

Comments
 (0)