Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error dispatching queued jobs from artisan commands #76

Closed
esalazarv opened this issue Jul 10, 2020 · 15 comments
Closed

Error dispatching queued jobs from artisan commands #76

esalazarv opened this issue Jul 10, 2020 · 15 comments

Comments

@esalazarv
Copy link

Thanks for the great work, I have been using it for a short time and it has worked quite well except for putting a job in the queue when it is dispatched from an artisan command

Preconditions:

  • "php": "^7.4.0"
  • "laravel/framework": "^7.0",
  • Database for each tenant
  • jobs and jobs_failed tables are in each tenant database
  • Queue connection environment var QUEUE_CONNECTION=database
  • multitenancy config file has this value for queues 'queues_are_tenant_aware_by_default' => true
  • \Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class is registered in multitenancy file config

The command handler looks like:

class ImportUsers extends Command
{
    ... //removed to keep it short...
    public function handle(PeopleImporter $import)
    {
         ... //removed to keep it short
        try {
              DB::beginTransaction();
               user = User::updateOrCreate($data);
               if ($access->wasRecentlyCreated) {
		   // Push a queue job for publish a notification
		   dispatch(new PublishNewUser($access))->delay($this->delay);
		} else {
		  // Push a queue job for publish a notification
		   dispatch(new PublishUserChanges($access))->delay($this->delay);
		}
              DB::commit();
         } catch (\Throwable $exception) {
             DB::rollBack();
             Log::error($exception->getTraceAsString());
         }
         ... //removed to keep it short
    }
}

Using the tenants:artisan command wrapper, my command signature looks like:

php artisan tenants:artisan "app:import-users --file=files/imports/users.csv" --tenant=1

I have verified that the users are correctly registered in the correct database schema for the specified tenant, but I always get an error when trying to push the job to the queue.

The exception trace is:

Running command for tenant `Sandbox` (id: 1)...
---------------------------------------------------------

Starting import
===============

  0/10 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%

Error: Call to a member function prepare() on null in /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php:458
Stack trace:
#0 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php(664): Illuminate\Database\Connection->Illuminate\Database\{closure}('insert into `jo...', Array)
#1 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php(631): Illuminate\Database\Connection->runQueryCallback('insert into `jo...', Array, Object(Closure))
#2 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php(465): Illuminate\Database\Connection->run('insert into `jo...', Array, Object(Closure))
#3 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php(417): Illuminate\Database\Connection->statement('insert into `jo...', Array)
#4 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Query/Processors/Processor.php(32): Illuminate\Database\Connection->insert('insert into `jo...', Array)
#5 /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2771): Illuminate\Database\Query\Processors\Processor->processInsertGetId(Object(Illuminate\Database\Query\Builder), 'insert into `jo...', Array, NULL)
#6 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/DatabaseQueue.php(162): Illuminate\Database\Query\Builder->insertGetId(Array)
#7 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/DatabaseQueue.php(111): Illuminate\Queue\DatabaseQueue->pushToDatabase('publish-access-...', '{"uuid":"a7cd02...', Object(Carbon\Carbon))
#8 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/Queue.php(60): Illuminate\Queue\DatabaseQueue->later(Object(Carbon\Carbon), Object(App\Jobs\PublishAccessChanges), '', 'publish-access-...')
#9 /var/www/html/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(171): Illuminate\Queue\Queue->laterOn('publish-access-...', Object(Carbon\Carbon), Object(App\Jobs\PublishAccessChanges))
#10 /var/www/html/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(158): Illuminate\Bus\Dispatcher->pushCommandToQueue(Object(Illuminate\Queue\DatabaseQueue), Object(App\Jobs\PublishAccessChanges))
#11 /var/www/html/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(73): Illuminate\Bus\Dispatcher->dispatchToQueue(Object(App\Jobs\PublishAccessChanges))
#12 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Bus/PendingDispatch.php(134): Illuminate\Bus\Dispatcher->dispatch(Object(App\Jobs\PublishAccessChanges))
#13 /var/www/html/app/Imports/PeopleImport.php(104): Illuminate\Foundation\Bus\PendingDispatch->__destruct()
#14 /var/www/html/vendor/maatwebsite/excel/src/Sheet.php(275): App\Imports\PeopleImport->onRow(Object(Maatwebsite\Excel\Row))
#15 /var/www/html/vendor/maatwebsite/excel/src/Reader.php(111): Maatwebsite\Excel\Sheet->import(Object(App\Imports\PeopleImport), 2)
#16 /var/www/html/vendor/maatwebsite/excel/src/Transactions/NullTransactionHandler.php(14): Maatwebsite\Excel\Reader->Maatwebsite\Excel\{closure}()
#17 /var/www/html/vendor/maatwebsite/excel/src/Reader.php(115): Maatwebsite\Excel\Transactions\NullTransactionHandler->__invoke(Object(Closure))
#18 /var/www/html/vendor/maatwebsite/excel/src/Excel.php(146): Maatwebsite\Excel\Reader->read(Object(App\Imports\PeopleImport), 'files/imports/p...', 'Csv', 's3')
#19 /var/www/html/vendor/maatwebsite/excel/src/Concerns/Importable.php(37): Maatwebsite\Excel\Excel->import(Object(App\Imports\PeopleImport), 'files/imports/p...', 's3', 'Csv')
#20 /var/www/html/app/Console/Commands/ImportPeople.php(66): App\Imports\PeopleImport->import('files/imports/p...', 's3', 'Csv')
#21 [internal function]: App\Console\Commands\ImportPeople->handle(Object(App\Imports\PeopleImport))
#22 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(33): call_user_func_array(Array, Array)
#23 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Util.php(36): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()
#24 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(91): Illuminate\Container\Util::unwrapIfClosure(Object(Closure))
#25 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure))
#26 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php(592): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL)
#27 /var/www/html/vendor/laravel/framework/src/Illuminate/Console/Command.php(134): Illuminate\Container\Container->call(Array)
#28 /var/www/html/vendor/symfony/console/Command/Command.php(258): Illuminate\Console\Command->execute(Object(Symfony\Component\Console\Input\StringInput), Object(Illuminate\Console\OutputStyle))
#29 /var/www/html/vendor/laravel/framework/src/Illuminate/Console/Command.php(121): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\StringInput), Object(Illuminate\Console\OutputStyle))
#30 /var/www/html/vendor/symfony/console/Application.php(911): Illuminate\Console\Command->run(Object(Symfony\Component\Console\Input\StringInput), Object(Illuminate\Console\OutputStyle))
#31 /var/www/html/vendor/symfony/console/Application.php(264): Symfony\Component\Console\Application->doRunCommand(Object(App\Console\Commands\ImportPeople), Object(Symfony\Component\Console\Input\StringInput), Object(Illuminate\Console\OutputStyle))
#32 /var/www/html/vendor/symfony/console/Application.php(140): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\StringInput), Object(Illuminate\Console\OutputStyle))
#33 /var/www/html/vendor/laravel/framework/src/Illuminate/Console/Application.php(93): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\StringInput), Object(Illuminate\Console\OutputStyle))
#34 /var/www/html/vendor/laravel/framework/src/Illuminate/Console/Application.php(185): Illuminate\Console\Application->run(Object(Symfony\Component\Console\Input\StringInput), Object(Illuminate\Console\OutputStyle))
#35 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(263): Illuminate\Console\Application->call('zintech:import:...', Array, Object(Illuminate\Console\OutputStyle))
#36 /var/www/html/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php(261): Illuminate\Foundation\Console\Kernel->call('zintech:import:...', Array, Object(Illuminate\Console\OutputStyle))
#37 /var/www/html/vendor/spatie/laravel-multitenancy/src/Commands/TenantsArtisanCommand.php(56): Illuminate\Support\Facades\Facade::__callStatic('call', Array)
#38 /var/www/html/vendor/spatie/laravel-multitenancy/src/Commands/TenantsArtisanCommand.php(42): Spatie\Multitenancy\Commands\TenantsArtisanCommand->runArtisanCommandForTenant(Object(App\Models\Tenancy\Tenant), 'zintech:import:...')
#39 /var/www/html/vendor/laravel/framework/src/Illuminate/Support/Traits/EnumeratesValues.php(202): Spatie\Multitenancy\Commands\TenantsArtisanCommand->Spatie\Multitenancy\Commands\{closure}(Object(App\Models\Tenancy\Tenant), 0)
#40 /var/www/html/vendor/spatie/laravel-multitenancy/src/Commands/TenantsArtisanCommand.php(43): Illuminate\Support\LazyCollection->each(Object(Closure))
#41 [internal function]: Spatie\Multitenancy\Commands\TenantsArtisanCommand->handle()
#42 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(33): call_user_func_array(Array, Array)
#43 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Util.php(36): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()
#44 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(91): Illuminate\Container\Util::unwrapIfClosure(Object(Closure))
#45 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure))
#46 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php(592): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL)
#47 /var/www/html/vendor/laravel/framework/src/Illuminate/Console/Command.php(134): Illuminate\Container\Container->call(Array)
#48 /var/www/html/vendor/symfony/console/Command/Command.php(258): Illuminate\Console\Command->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))
#49 /var/www/html/vendor/laravel/framework/src/Illuminate/Console/Command.php(121): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))
#50 /var/www/html/vendor/symfony/console/Application.php(911): Illuminate\Console\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#51 /var/www/html/vendor/symfony/console/Application.php(264): Symfony\Component\Console\Application->doRunCommand(Object(Spatie\Multitenancy\Commands\TenantsArtisanCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#52 /var/www/html/vendor/symfony/console/Application.php(140): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#53 /var/www/html/vendor/laravel/framework/src/Illuminate/Console/Application.php(93): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#54 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(129): Illuminate\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#55 /var/www/html/artisan(37): Illuminate\Foundation\Console\Kernel->handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#56 {main}# php artisan tenants:artisan "app:import-users --file=files/imports/users.csv" --tenant=1

The result is the same if exec the command without database transactions

Do you have any idea how I can solve it?

@masterix21
Copy link
Collaborator

It seems that somewhere the app loses the connection with the database. Can you supply a more accurate example?

You wrote php class ImportUsers... but the error starts from php App\Console\Commands\ImportPeople....

Finally, does your job implements the Spatie\Multitenancy\Jobs\TenantAware interface?

@esalazarv
Copy link
Author

esalazarv commented Jul 10, 2020

a) It seems that somewhere the app loses the connection with the database. Can you supply a more accurate example?

b) You wrote php class ImportUsers... but the error starts from php App\Console\Commands\ImportPeople....

c) Finally, does your job implements the Spatie\Multitenancy\Jobs\TenantAware interface?

Hi @masterix21

c) : Yes, all jobs implements Spatie\Multitenancy\Jobs\TenantAware interface
b): sorry i mixed some of the outputs by exemplifying, but the output is the same for all similar jobs
a): You can reproduce the issue with these steps:

Create a basic comand

php artisan make:command ImportUsers

Use this code for the command:

<?php

namespace App\Console\Commands;

use App\Jobs\PublishNewUser;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class ImportUsers extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:import-users';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        try {
            DB::beginTransaction();
            $faker = \Faker\Factory::create();
            $user = User::create([
                'name' => $faker->name,
                'email' => $faker->email,
            ]);
            dump($user->toArray());
            dispatch(new PublishNewUser($user))->delay(now()->addSeconds(10));
            DB::commit();
        } catch (\Throwable $exception) {
            DB::rollBack();
            Log::error($exception->getTraceAsString());
        }
    }
}

Create a basic Job

php artisan make:job PublishNewUser

Use this code for the Job:

<?php

namespace App\Jobs;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Spatie\Multitenancy\Jobs\TenantAware;

class PublishNewUser implements ShouldQueue, TenantAware
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * @var User
     */
    protected $user;

    /**
     * Create a new job instance.
     *
     * @param User $user
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Log::info($this->user->toJson());
    }
}

Run command:

php artisan tenants:artisan app:import-users --tenant=1

The console output:

Running command for tenant `Sandbox` (id: 1)...
---------------------------------------------------------
array:5 [
  "name" => "Sharon Ernser"
  "email" => "darian.bahringer@friesen.net"
  "updated_at" => "2020-07-10T16:33:33.000000Z"
  "created_at" => "2020-07-10T16:33:33.000000Z"
  "id" => 21
]
All done!

Then new user record is now in database, but job is not in jobs table because for some reason PDO connection is null at the moment to push the job to the queue, the logs file has a full trace.

If you remove try catch in the command handler, the output is:

Running command for tenant `Sandbox` (id: 1)...
---------------------------------------------------------
array:5 [
  "name" => "Lloyd Stehr"
  "email" => "maureen.conn@hotmail.com"
  "updated_at" => "2020-07-10T16:58:21.000000Z"
  "created_at" => "2020-07-10T16:58:21.000000Z"
  "id" => 22
]

   Error 

  Call to a member function prepare() on null

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:458
    454|             if ($this->pretending()) {
    455|                 return true;
    456|             }
    457| 
  > 458|             $statement = $this->getPdo()->prepare($query);
    459| 
    460|             $this->bindValues($statement, $this->prepareBindings($bindings));
    461| 
    462|             $this->recordsHaveBeenModified();

      +12 vendor frames 
  13  app/Console/Commands/ImportUsers.php:52
      Illuminate\Foundation\Bus\PendingDispatch::__destruct()

      +34 vendor frames 
  48  artisan:37
      Illuminate\Foundation\Console\Kernel::handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

@masterix21
Copy link
Collaborator

masterix21 commented Jul 10, 2020

Using tinker, can you post here the output of config('queue.connections.'. config('queue.default'))?

Thanks

@esalazarv
Copy link
Author

Using tinker, can you post here the output of config('queue.connections.'. config('queue.default'))?

Thanks

For php artisan tinker:

# php artisan tinker
Psy Shell v0.10.4 (PHP 7.4.7 — cli) by Justin Hileman
>>> 
>>> config('queue.connections.'. config('queue.default'));
=> [
     "driver" => "database",
     "table" => "jobs",
     "queue" => "default",
     "retry_after" => 90,
   ]
>>> 

Using tinker for a tenant

# php artisan tenants:artisan tinker --tenant=1

Running command for tenant `Sandbox` (id: 1)...
---------------------------------------------------------
Psy Shell v0.10.4 (PHP 7.4.7 — cli) by Justin Hileman
>>> config('queue.connections.'. config('queue.default'));
=> [
     "driver" => "database",
     "table" => "jobs",
     "queue" => "default",
     "retry_after" => 90,
   ]
>>>

@masterix21
Copy link
Collaborator

masterix21 commented Jul 10, 2020

Ok. Your jobs table is in landlord database? If so, change your database queue config following:

"driver" => "database",
"connection" => "landlord", // or your landlord connection name
"table" => "jobs",
"queue" => "default",
"retry_after" => 90,

@esalazarv
Copy link
Author

Ok. Your jobs table is in landlord database? If so, change your database queue config following:

"driver" => "database",
"connection" => "landlord", // or your landlord connection name
"table" => "jobs",
"queue" => "default",
"retry_after" => 90,

No, each tenant has its own database and has its own jobs table, maybe with a SwicthQueueConnectionTask for solve this?

@masterix21
Copy link
Collaborator

Isn't better to create only one jobs table in the landlord database? I think that's more powerful: so, you can use Telescope to track all your jobs with no pain, for example.

Using that approach, you need to start a "queue:listen" for each tenant. I don't know how many tenants you have, but for me, it's a bit a wrong way.

@esalazarv
Copy link
Author

Isn't better to create only one jobs table in the landlord database? I think that's more powerful: so, you can use Telescope to track all your jobs with no pain, for example.

Using that approach, you need to start a "queue:listen" for each tenant. I don't know how many tenants you have, but for me, it's a bit a wrong way.

I put the job tables in each database because I was planning to keep most of the data isolated and to be able to explicitly handle job queues by tenant, and that the landlord had their own job table too, but I didn't know the recommended approach is that everything is managed in the landlord

Let me try that approach, because with a SwicthQueueConnectionTask I get the same results.

@esalazarv
Copy link
Author

@masterix21
I have put the job tables in the landlord database, and changed the queue settings for the driver database as you suggested and the command has created the jobs correctly.
However I still think it is an issue, because it only happens when I dispatch a job from an artisan command, if a job is dispatched from a route or a controller, the job is correctly registered in the tenant database and it is not necessary to centralize the jobs in the landlord. It seems that the connection is being lost during the console life cycle when the package is enabled.

For now I think it is a solution but it would be nice to be able to completely isolate it.

Thank you so much for your support.

@masterix21
Copy link
Collaborator

masterix21 commented Jul 10, 2020

@esalazarv in the big applications - when isolation could be needed - it's better to avoid the database queue: you can use Redis or other (scalable?) queue solutions. Queues are differents than other data; you can change your queue logic any time without losing a single job.

@esalazarv
Copy link
Author

@esalazarv in the big applications - when isolation could be needed - it's better to avoid the database queue: you can use Redis or other (scalable?) queue solutions. Queues are differents than other data; you can change your queue logic any time without losing a single job.

No, im not using Redis for now, only database for this little project, but you are right the next step for a large app should be change to Redis or another service for now i don't have many tenants that is why for now the solution that you helped me works well in my project, hopefully soon I will have to change to Redis, it would mean that everything is going very well with the project.

Thank you very much for your help.

@masterix21
Copy link
Collaborator

@esalazarv, can be closed the issue? It is solved, or is there something else?

@esalazarv
Copy link
Author

@esalazarv, can be closed the issue? It is solved, or is there something else?

Hi @masterix21, The alternative of putting all the jobs in the landlord database works for me, but I don't know if you would consider an issue when dispatching a job from the artisan command, if not then yes, the issue can be closed.

Thanks.

@ChrisCook1906
Copy link

I think my issue (#82) is related. Scheduled jobs (App/Console/Kernel) don't seem to be tenant aware when executed from the scheduler or commands (schedule:run).

Chris

@freekmurze
Copy link
Member

Closing this one, as it seems to have been answered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants