therezor / laravel-transactional-jobs

Run Laravel job inside transaction (after transaction is committed, or cancel if rolled back)
MIT License
45 stars 8 forks source link

Jobs with transactions within them cause this package to infinitely loop #3

Closed DaveWishesHe closed 5 years ago

DaveWishesHe commented 5 years ago

Laravel 5.7

If the job being dispatched also contains a transaction, this gets stuck in a loop and eventually errors with Maximum function nesting level of '512' reached, aborting!

Example:

MyService.php

<?php

namespace App\Services;

use App\Jobs\DummyJobWithTransaction;

class MyService
{
    public function dispatchDummyJob()
    {
        \Log::info('About to enter TX');

        \DB::transaction(function () {

            \Log::info('About to dispatch DummyJobWithTransaction');

            dispatch(new DummyJobWithTransaction());

        });
    }
}

DummyJobWithTransaction.php

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class DummyJobWithTransaction implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $afterTransactions = true;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        \Log::info('Dummy Dispatched.');
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        \Log::info('Dummy Handling...');

        \DB::transaction(function () {

            \Log::info('We\'re in the Job\'s TX');
            sleep(10);

        });

        \Log::info('Dummy Handled.');
    }
}

In tinker:

$s = app()->make(App\Services\MyService::class)
$s->dispatchDummyJob();

Output in logs:

==> storage/logs/laravel.log <==
[2019-08-05 11:29:13] local.INFO: About to enter TX  
[2019-08-05 11:29:13] local.INFO: About to dispatch DummyJobWithTransaction  
[2019-08-05 11:29:13] local.INFO: Dummy Dispatched.  
[2019-08-05 11:29:13] local.INFO: Dummy Handling...  
[2019-08-05 11:29:13] local.INFO: We're in the Job's TX  
[2019-08-05 11:29:23] local.INFO: Dummy Handling...  
[2019-08-05 11:29:23] local.INFO: We're in the Job's TX  
[2019-08-05 11:29:33] local.INFO: Dummy Handling...  
[2019-08-05 11:29:33] local.INFO: We're in the Job's TX  
[2019-08-05 11:29:43] local.INFO: Dummy Handling...  
[2019-08-05 11:29:43] local.INFO: We're in the Job's TX 
(and so on...)

Presumably the issue is related to the fact that Laravel's DB is a singleton, so as soon as a fresh transaction opens, the TransactionalDispatcher starts waiting for the commit.

therezor commented 5 years ago

Are you using sync driver for jobs?

DaveWishesHe commented 5 years ago

On my local environment, yes. I haven't put the code up anywhere to test on an alternate driver

DaveWishesHe commented 5 years ago

I've had an experiment, and replacing TransactionalDispatcher.php line 35 like this seems to resolve the issue, but I'm unsure as to knock on impact of overriding Laravel's singleton pattern.

- $this->db = $container->make('db');
+ $this->db = clone $container->make('db');
therezor commented 5 years ago

I think the issue is because you are using sync queue. It won't happened on real workers cause every worker run single instance of db. But this case should be handled also. Please try redis as queue provider. Cloning db object is not a good idea at all

DaveWishesHe commented 5 years ago

Yeah, works fine on the redis queue driver, will just crack on with the "it'll work up on the other environments" for now, thanks :)

therezor commented 5 years ago

Fixed in 0.4.0