laravel / horizon

Dashboard and code-driven configuration for Laravel queues.
https://laravel.com/docs/horizon
MIT License
3.84k stars 645 forks source link

Singleton state overwritten before job process has finished #1359

Closed tklie closed 8 months ago

tklie commented 8 months ago

Horizon Version

5.21.4

Laravel Version

10.37.1

PHP Version

8.2.13

Redis Driver

Predis

Redis Version

5.0.7

Database Driver & Version

No response

Description

I am registering a singleton to keep track of a model id. This singleton is again resolved and used throughout services in my application to add model scopes based on this id.

First, the singleton is resolved in the handle() method of my job and the id is set. During the processing of the job, everything works as expected. I am then using an event subscriber to listen for the JobProcessedEvent and perform some post-job logic. In this subscriber, I also access the singleton. However, the singleton's state in the event subscriber sometimes does not match the state that was set by the job. I find this to be unexpected.

Based on my understanding, the raising of the JobProcessedEvent and the execution of the listener follow the processing of the job immediately and synchronously, before the worker picks up a new job from the queue. As such, the singleton's state should not be able to change between job and listener (I included the Sentry stack trace that lead to this understanding at the end). Am I mistaken here?

Steps To Reproduce

class MySingleton {
    private int|null $id = null;

    public function getId(): int
    {
        if (is_null($this->id)) {
            throw new RuntimeException('Singleton id has not been set');
        }

        return $this->id;
    }

    public function setId(int $id): self
    {
        $this->id = $id;

        return $this;
    }
}
class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(MySingleton::class);
    }
}
class MyJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public readonly MyModel $model)
    {
    }

    public function handle(): void
    {
        $singleton = app(MySingleton::class);

        $singleton->setId($this->model->id);

        // Some logic that resolves and accesses the singleton
        // to perform model scoping by the singleton's id.
    }
}
class MyListener
{
    public function subscribe(Dispatcher $events): void
    {
        $events->listen(JobFailed::class, [__CLASS__, 'handleJobCompleted']);
        $events->listen(JobProcessed::class, [__CLASS__, 'handleJobCompleted']);
    }

    public function handleJobCompleted(JobFailed|JobProcessed $event): void
    {
        $job = unserialize($event->job->payload()['data']['command']);

        $singleton = app(MySingleton::class);

        if ($job->model->id !== $singleton->getId()) {
            throw new RuntimeException('Singleton id does not match job')
        }

        // Some post-job logic that also needs to perform model scoping by
        // the singleton's id. Unexpectedly, here the singleton's id
        // does not match the job's model's id anymore.
    }
}
class EventServiceProvider extends ServiceProvider
{
    protected $subscribe = [
        MyListener::class,
    ];
}

Stacktrace

Error /app/Listeners/MyListener.php in App\Listeners\MyListener::handleJobCompleted
Singleton id does not match job

/app/Listeners/MyListener.php in App\Listeners\MyListener::handleJobCompleted at line 45
Called from: /vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php in Illuminate\Events\Dispatcher::Illuminate\Events\{closure}
/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php in Illuminate\Events\Dispatcher::invokeListeners at line 286
/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php in Illuminate\Events\Dispatcher::dispatch at line 266
/vendor/laravel/framework/src/Illuminate/Queue/Worker.php in Illuminate\Queue\Worker::raiseAfterJobEvent at line 666
/vendor/laravel/framework/src/Illuminate/Queue/Worker.php in Illuminate\Queue\Worker::process at line 441
/vendor/laravel/framework/src/Illuminate/Queue/Worker.php in Illuminate\Queue\Worker::runJob at line 389
/vendor/laravel/framework/src/Illuminate/Queue/Worker.php in Illuminate\Queue\Worker::daemon at line 176
/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php in Illuminate\Queue\Console\WorkCommand::runWorker at line 137
/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php in Illuminate\Queue\Console\WorkCommand::handle at line 120
/vendor/laravel/horizon/src/Console/WorkCommand.php in Laravel\Horizon\Console\WorkCommand::handle at line 51
/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php in Illuminate\Container\BoundMethod::Illuminate\Container\{closure} at line 36
/vendor/laravel/framework/src/Illuminate/Container/Util.php in Illuminate\Container\Util::unwrapIfClosure at line 41
/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php in Illuminate\Container\BoundMethod::callBoundMethod at line 93
/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php in Illuminate\Container\BoundMethod::call at line 35
/vendor/laravel/framework/src/Illuminate/Container/Container.php in Illuminate\Container\Container::call at line 662
/vendor/laravel/framework/src/Illuminate/Console/Command.php in Illuminate\Console\Command::execute at line 211
/vendor/symfony/console/Command/Command.php in Symfony\Component\Console\Command\Command::run at line 326
/vendor/laravel/framework/src/Illuminate/Console/Command.php in Illuminate\Console\Command::run at line 180
/vendor/symfony/console/Application.php in Symfony\Component\Console\Application::doRunCommand at line 1096
/vendor/symfony/console/Application.php in Symfony\Component\Console\Application::doRun at line 324
/vendor/symfony/console/Application.php in Symfony\Component\Console\Application::run at line 175
/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php in Illuminate\Foundation\Console\Kernel::handle at line 201
/artisan at line 35
crynobone commented 8 months ago

Hey there,

While this may be a legitimate issue, can you first try posting your problem or question on one of the support channels below? If this issue can be definitively identified as a bug, feel free to open up a new issue with a link to the original one and we'll gladly help you out.

Thanks!