archtechx / tenancy

Automatic multi-tenancy for Laravel. No code changes needed.
https://tenancyforlaravel.com
MIT License
3.65k stars 429 forks source link

Run tenant aware query from central/tenants context #547

Closed bliveinhack closed 2 years ago

bliveinhack commented 3 years ago

Describe the bug

Above code works fine when we have session driver set to SESSION_DRIVER=files. But if we set session driver to SESSION_DRIVER=database . it fails and give Call to a member function prepare() on null.

Steps to reproduce

Expected behavior

Your setup

jandominik commented 3 years ago

Agree, have a same problem. I'm running command which is going through all Tenants tenancy()->runForMultiple(null, function (Tenant $tenant) { ... }); Problem happens when calling anything from database but only on second tenant and more. First tenant always pass.

Ad 1: Problem is happening even if I try to run Command using database via php artisan tenants:run my:command. Ad 2: Found that this problem is related to way of using database, because problem is happening only when using criterias of package https://packagist.org/packages/prettus/l5-repository. When I skip all criterias (which should not be anything else than a list of conditions), problem has gone. If it will be useful, I can write more detailed scenario how to reproduce.

Laravel: 7.28.4 stancl/tenancy: 3.2.1

stancl commented 3 years ago

If you could share an exception via Flare and showed the exact code that reproduces this (without Jetstream, events firing from the tenant call, ...) I can take a look at this

ngugijames commented 3 years ago

Here is sample code to replicate this error:

   /**
     * @return \Illuminate\Http\Response
     */
    public function getToolbox()
    { 
        $tenants = Tenant::get();

        $businesses = [];
        $locations = [];

        tenancy()->runForMultiple($tenants, function ($tenant) use (&$businesses, &$locations) {
            $businesses[$tenant->id] = $tenant->business_name;

            //for each tenant, get the business locations
            $locations[$tenant->id] = BusinessLocation::pluck('name', 'id')
                                                      ->toArray();
        });

        return view('core::toolkit.index', compact('businesses', 'locations'));
    }

Your setup Laravel version: 7.x stancl/tenancy version: 3.4.0 Storage driver: DB Session Driver : DB

My code has no custom middleware, events or configs. Tenants are identified by subdomain.

The error: Screen Shot 2021-03-15 at 17 23 22 Screen Shot 2021-03-15 at 17 23 41 Screen Shot 2021-03-15 at 17 25 15

stancl commented 3 years ago

I see. Seems to be related to the session driver then. I'll try to take a look at this soon

ngugijames commented 3 years ago

Yes. From my debugging:

  1. The runForMultiple function succeeds.
  2. The SessionManager correctly inits a connection using the 'tenant' db connection.
  3. But somewhere, in the DatabaseSessionHandler the PDO attribute of the Connection becomes null, causing the error.
timmywiff commented 3 years ago

I've also come across the issue where I get the error "Call to a member function prepare() on null".

Illuminate/Database/Connection.php:492: $statement = $this->getPdo()->prepare($query);

Laravel: v8.33.1 stancl/tenancy: v3.4.2 Session Driver : DB

From what I can see, the problem is caused by the purge() function in Illuminate/Database/DatabaseManager.php. Using disconnect() instead does not cause the error.

stancl commented 3 years ago

I think we need to use purge() to correctly scope data. Maybe the solution is using disconnect() for central and purge() for tenant connections? Would appreciate if anyone could test that

deviarte commented 3 years ago

We have been running into a similar issue.

Using the new Job Batching feature in Laravel 8. https://laravel.com/docs/8.x/queues#job-batching

In the docs, all the closure methods accept a model as a param. So most people will probably follow this example. The issue is that all models passed to those closures will be serialized using SerializesModels causing issues on a random set of jobs.

use Illuminate\Support\Facades\Bus;

//

$batchBus = Bus::batch([])
->allowFailures()
->finally(function() use ($post) {
    if (! $post->refresh()->batch_ended_at) {
        return;
    }

    dispatch(new MarkAsSent($post->id));
})
->dispatch();

Easiest way to work around it is to pass a model ID instead.

use Illuminate\Support\Facades\Bus;

//

$postId = $post->id;

$batchBus = Bus::batch([])
->allowFailures()
->finally(function() use ($postId) {
    $p = \App\Models\Post::find($postId);
    if (! $p->batch_ended_at) {
        return;
    }

    dispatch(new MarkAsSent($p->id));
})
->dispatch();

Spent some hours tracking down random job errors, hope this helps someone else.

stancl commented 3 years ago

Do you have the actual error that it showed? If you could show a Flare stack trace or something like that I could take a look at this.

Seems like it doesn't know how to properly serialize the model. Is this in the tenant context? With $post being a model in the tenant DB?

deviarte commented 3 years ago

Actually, i thought the above fixed it, but it still seems to happen once in a while. To answer the question, yes, $post is a model in the Tenant DB, the whole Job runs in a tenant-context. But again, that doesn't seem to have been the issue or at least, is not the root issue.

`` [2021-06-07 03:55:11] prod.ERROR: Call to a member function prepare() on null {"exception":"[object] (Error(code: 0): Call to a member function prepare() on null at /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php:472) [stacktrace]

0 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(678): Illuminate\Database\Connection->Illuminate\Database\{closure}()

1 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(645): Illuminate\Database\Connection->runQueryCallback()

2 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(479): Illuminate\Database\Connection->run()

3 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(431): Illuminate\Database\Connection->statement()

4 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2949): Illuminate\Database\Connection->insert()

5 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Bus/DatabaseBatchRepository.php(100): Illuminate\Database\Query\Builder->insert()

6 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Bus/PendingBatch.php(239): Illuminate\Bus\DatabaseBatchRepository->store()

7 /home/ubuntu/app/app/Jobs/Amplify/Campaigns/BatchSend.php(116): Illuminate\Bus\PendingBatch->dispatch()

8 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): App\Jobs\Amplify\Campaigns\BatchSend->handle()

9 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/Util.php(40): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()

10 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\Util::unwrapIfClosure()

11 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(37): Illuminate\Container\BoundMethod::callBoundMethod()

12 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/Container.php(614): Illuminate\Container\BoundMethod::call()

13 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(128): Illuminate\Container\Container->call()

14 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\Bus\Dispatcher->Illuminate\Bus\{closure}()

15 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

16 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(132): Illuminate\Pipeline\Pipeline->then()

17 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(120): Illuminate\Bus\Dispatcher->dispatchNow()

18 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\Queue\CallQueuedHandler->Illuminate\Queue\{closure}()

19 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()

20 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(122): Illuminate\Pipeline\Pipeline->then()

21 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(70): Illuminate\Queue\CallQueuedHandler->dispatchThroughMiddleware()

22 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(98): Illuminate\Queue\CallQueuedHandler->call()

23 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(414): Illuminate\Queue\Jobs\Job->fire()

24 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(364): Illuminate\Queue\Worker->process()

25 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(158): Illuminate\Queue\Worker->runJob()

26 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(117): Illuminate\Queue\Worker->daemon()

27 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(101): Illuminate\Queue\Console\WorkCommand->runWorker()

28 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Illuminate\Queue\Console\WorkCommand->handle()

29 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/Util.php(40): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()

30 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\Util::unwrapIfClosure()

31 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(37): Illuminate\Container\BoundMethod::callBoundMethod()

32 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/Container.php(614): Illuminate\Container\BoundMethod::call()

33 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Console/Command.php(136): Illuminate\Container\Container->call()

34 /home/ubuntu/app/vendor/symfony/console/Command/Command.php(288): Illuminate\Console\Command->execute()

35 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Console/Command.php(121): Symfony\Component\Console\Command\Command->run()

36 /home/ubuntu/app/vendor/symfony/console/Application.php(974): Illuminate\Console\Command->run()

37 /home/ubuntu/app/vendor/symfony/console/Application.php(291): Symfony\Component\Console\Application->doRunCommand()

38 /home/ubuntu/app/vendor/symfony/console/Application.php(167): Symfony\Component\Console\Application->doRun()

39 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Console/Application.php(92): Symfony\Component\Console\Application->run()

40 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(129): Illuminate\Console\Application->run()

41 /home/ubuntu/app/artisan(37): Illuminate\Foundation\Console\Kernel->handle()

42 {main}

"} ``

Also happens on the actual Job that gets 'batched', that job implements: Illuminate\Bus\Batchable

`` [2021-06-07 04:00:02] prod.ERROR: Call to a member function prepare() on null {"exception":"[object] (Error(code: 0): Call to a member function prepare() on null at /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php:345) [stacktrace]

0 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(678): Illuminate\Database\Connection->Illuminate\Database\{closure}()

1 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(645): Illuminate\Database\Connection->runQueryCallback()

2 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(353): Illuminate\Database\Connection->run()

3 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2351): Illuminate\Database\Connection->select()

4 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2339): Illuminate\Database\Query\Builder->runSelect()

5 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2905): Illuminate\Database\Query\Builder->Illuminate\Database\Query\{closure}()

6 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2340): Illuminate\Database\Query\Builder->onceWithColumns()

7 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Database/Concerns/BuildsQueries.php(254): Illuminate\Database\Query\Builder->get()

8 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Bus/DatabaseBatchRepository.php(82): Illuminate\Database\Query\Builder->first()

9 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Bus/Batchable.php(24): Illuminate\Bus\DatabaseBatchRepository->find()

10 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(184): App\Jobs\Amplify\Campaigns\SendEmailBatch->batch()

11 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(78): Illuminate\Queue\CallQueuedHandler->ensureSuccessfulBatchJobIsRecorded()

12 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(98): Illuminate\Queue\CallQueuedHandler->call()

13 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(414): Illuminate\Queue\Jobs\Job->fire()

14 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(364): Illuminate\Queue\Worker->process()

15 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(158): Illuminate\Queue\Worker->runJob()

16 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(117): Illuminate\Queue\Worker->daemon()

17 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(101): Illuminate\Queue\Console\WorkCommand->runWorker()

18 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Illuminate\Queue\Console\WorkCommand->handle()

19 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/Util.php(40): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()

20 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\Util::unwrapIfClosure()

21 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(37): Illuminate\Container\BoundMethod::callBoundMethod()

22 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Container/Container.php(614): Illuminate\Container\BoundMethod::call()

23 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Console/Command.php(136): Illuminate\Container\Container->call()

24 /home/ubuntu/app/vendor/symfony/console/Command/Command.php(288): Illuminate\Console\Command->execute()

25 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Console/Command.php(121): Symfony\Component\Console\Command\Command->run()

26 /home/ubuntu/app/vendor/symfony/console/Application.php(974): Illuminate\Console\Command->run()

27 /home/ubuntu/app/vendor/symfony/console/Application.php(291): Symfony\Component\Console\Application->doRunCommand()

28 /home/ubuntu/app/vendor/symfony/console/Application.php(167): Symfony\Component\Console\Application->doRun()

29 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Console/Application.php(92): Symfony\Component\Console\Application->run()

30 /home/ubuntu/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(129): Illuminate\Console\Application->run()

31 /home/ubuntu/app/artisan(37): Illuminate\Foundation\Console\Kernel->handle()

32 {main}

"} ``

On a side-note: The Illuminate\Bus\BusServiceProvider initiates Illuminate\Bus\DatabaseBatchRepository. In the source code you can read that you can override the default database connection and db table.

So in app/config/queue.php i added 'batching' => [ 'database' => 'tenants_tmpl', // the tenants connection template ],

The produced an error right away when trying to Batch the Jobs.

`` [previous exception] [object] (PDOException(code: 3D000): SQLSTATE[3D000]: Invalid catalog name: 1046 No database selected at /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Database/Connection.php:472) [stacktrace]

0 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(472): PDO->prepare('insert into `jo...')

1 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(678): Illuminate\Database\Connection->Illuminate\Database\{closure}('insert into `jo...', Array)

2 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(645): Illuminate\Database\Connection->runQueryCallback('insert into `jo...', Array, Object(Closure))

3 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(479): Illuminate\Database\Connection->run('insert into `jo...', Array, Object(Closure))

4 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(431): Illuminate\Database\Connection->statement('insert into `jo...', Array)

5 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2949): Illuminate\Database\Connection->insert('insert into `jo...', Array)

6 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Bus/DatabaseBatchRepository.php(100): Illuminate\Database\Query\Builder->insert(Array)

7 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Bus/PendingBatch.php(239): Illuminate\Bus\DatabaseBatchRepository->store(Object(Illuminate\Bus\PendingBatch))

8 /Users/code/some-app/app/Jobs/Amplify/Campaigns/BatchSend.php(112): Illuminate\Bus\PendingBatch->dispatch()

9 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): App\Jobs\Amplify\Campaigns\BatchSend->handle()

10 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/Util.php(40): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()

11 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\Util::unwrapIfClosure(Object(Closure))

12 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(37): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure))

13 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/Container.php(614): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL)

14 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(128): Illuminate\Container\Container->call(Array)

15 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\Bus\Dispatcher->Illuminate\Bus\{closure}(Object(App\Jobs\Amplify\Campaigns\BatchSend))

16 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(App\Jobs\Amplify\Campaigns\BatchSend))

17 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(132): Illuminate\Pipeline\Pipeline->then(Object(Closure))

18 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(120): Illuminate\Bus\Dispatcher->dispatchNow(Object(App\Jobs\Amplify\Campaigns\BatchSend), false)

19 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\Queue\CallQueuedHandler->Illuminate\Queue\{closure}(Object(App\Jobs\Amplify\Campaigns\BatchSend))

20 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(App\Jobs\Amplify\Campaigns\BatchSend))

21 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(122): Illuminate\Pipeline\Pipeline->then(Object(Closure))

22 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(70): Illuminate\Queue\CallQueuedHandler->dispatchThroughMiddleware(Object(Illuminate\Queue\Jobs\RedisJob), Object(App\Jobs\Amplify\Campaigns\BatchSend))

23 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(98): Illuminate\Queue\CallQueuedHandler->call(Object(Illuminate\Queue\Jobs\RedisJob), Array)

24 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(414): Illuminate\Queue\Jobs\Job->fire()

25 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(364): Illuminate\Queue\Worker->process('tenants', Object(Illuminate\Queue\Jobs\RedisJob), Object(Illuminate\Queue\WorkerOptions))

26 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(158): Illuminate\Queue\Worker->runJob(Object(Illuminate\Queue\Jobs\RedisJob), 'tenants', Object(Illuminate\Queue\WorkerOptions))

27 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(117): Illuminate\Queue\Worker->daemon('tenants', 'default', Object(Illuminate\Queue\WorkerOptions))

28 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(101): Illuminate\Queue\Console\WorkCommand->runWorker('tenants', 'default')

29 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Illuminate\Queue\Console\WorkCommand->handle()

30 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/Util.php(40): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()

31 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\Util::unwrapIfClosure(Object(Closure))

32 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(37): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure))

33 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Container/Container.php(614): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL)

34 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Console/Command.php(136): Illuminate\Container\Container->call(Array)

35 /Users/code/some-app/vendor/symfony/console/Command/Command.php(288): Illuminate\Console\Command->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))

36 /Users/code/some-app/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))

37 /Users/code/some-app/vendor/symfony/console/Application.php(974): Illuminate\Console\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

38 /Users/code/some-app/vendor/symfony/console/Application.php(291): Symfony\Component\Console\Application->doRunCommand(Object(Illuminate\Queue\Console\WorkCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

39 /Users/code/some-app/vendor/symfony/console/Application.php(167): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

40 /Users/code/some-app/vendor/laravel/framework/src/Illuminate/Console/Application.php(92): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

41 /Users/code/some-app/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))

42 /Users/code/some-app/artisan(37): Illuminate\Foundation\Console\Kernel->handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

43 {main}

"} ``

But then i remembered that the DB template for tenants would not work since that's not the one the Tenancy package creates or uses. (Stancl\Tenancy\Database\DatabaseManager line 43)

So i changed app/config/queue.php to 'batching' => [ 'database' => 'tenant', // the actual tenant connection (and the default connection when running in tenant-context) ],

No errors when doing some regular tests using the new Laravel Batch feature. But will keep an eye on it because the errors were random, it worked 95% of the time, just 5% of jobs failed with that PDO error. Will report back.

stancl commented 3 years ago

So i changed app/config/queue.php to 'batching' => [ 'database' => 'tenant', // the actual tenant connection (and the default connection when running in tenant-context) ],

That doesn't seem right, since even tenant jobs need to be dispatched to the central connection.

The real issue isn't where the jobs are being stored, it's how their payload is being serialized/unserialized. Specifically the model instances.

abanghendri commented 3 years ago

Hai, I face quite similar issue When I run tenancy()->central(function(){}) It gives me

ERROR: Call to a member function prepare() on null.

why it is happen?

uyab commented 3 years ago

Also happen to me. In my case, I have centralized users table called users_global where all users from all tenants stored. When some tenants user trying to update their email, I will run validation (check if its already exists in users_global).

https://flareapp.io/share/xPQzJKR7#F82

waterbuckit commented 2 years ago

Also got this - I found the easiest way to reproduce it is when multiple batches run at essentially the same time. I've got a scheduled task which gathers (an undefined number of) jobs to be run in a batch. Don't know if knowing this reproducibility step helps? For the record, none of my Jobs serialise models. And it only happens on the subsequent batches that are dispatched very soon after the first one. Wonder if dispatching the batch on the central connection and passing the tenant to the jobs and running their contents in the tenant context would help? I just don't really want to have to go through the many jobs I've already implemented doing this, not least the side effects it may have to other places that dispatch said jobs.

beattech commented 2 years ago

Hai, I face quite similar issue When I run tenancy()->central(function(){}) It gives me

ERROR: Call to a member function prepare() on null.

why it is happen?

me also got this error, what the solution for this ?

stancl commented 2 years ago

I fixed a queue-related issue here https://github.com/archtechx/tenancy/commit/73a4a3018cadca2ba0fb5f2130fca1718a2b3670 and I think it might fix the root cause of this as well.

If anyone would like to test that, you can run composer require stancl/tenancy:3.x-dev. I'll release it in 3.x in a couple of days, but would appreciate feedback before that

chillbram commented 2 years ago

The change sadly did not solve my Job calling tenancy()->central(function(){}) returning a

Call to a member function prepare() on null.

error.

stancl commented 2 years ago

Hmm okay, I'll take a separate look at this then. Thanks for testing!

stancl commented 2 years ago

I can see what would cause the issue with running $tenant->run() or tenancy()->central() from the context of another tenant, but I don't think there should be issues with using $tenant->run() inside central context.

The original issue makes it a bit unclear when exactly this happens. Can someone confirm if there's any issue with using $tenant->run() in central context?

Fedrosa commented 2 years ago

@stancl we are facing the same issue , do you have any updates regarding this issue ?

bbredewold commented 2 years ago

Can't find the problem. The second tenant that is created, doesn't get the Welcome e-mail, because of the PDO connection is null (ERROR: Call to a member function prepare() on null.)

We are listening to the TenantAdminCreated event, that's inside the JobPipeline.


    public function boot(): void
    {
        $this->bootEvents();

        $this->makeTenancyMiddlewareHighestPriority();

        Event::listen(TenantAdminCreated::class, function (TenantAdminCreated $event) {
            SendTenantWelcomeEmail::dispatch($event->tenant, $event->user->email)
                ->onQueue(QueueEnum::CENTRAL); // this is problematic :(
        });
    }

EDIT: Hmm, In my case it seems that the default PasswordBrokerManager in Laravel is a Singleton with the databaseconnection fixed to it... (So it's behaving like cache, since Horizon keeps running.) Maybe by using a custom broker I have a solution for my problem here.

afbeelding
loreberti89 commented 2 years ago

I have same error when I use Database Session driver. If I use tenancy()->central(function ($tenant)) then I have Call to a member function prepare() on null some one has solved? I have no problem with redis session, or file o other session driver.

thegr8awais commented 2 years ago

Any update on this guys?

stancl commented 2 years ago

I plan to dive into these bugs soon. Main thing that helps is just more context for reproduction and details about what exactly leads to this happening. Since it doesn't happen in all setups iirc.

thegr8awais commented 2 years ago

I have created a public repo for demo https://github.com/thegr8awais/tenancyforlaravel

These are things I think...

session_driver=database

multi-database

and then running code like this...

Route::middleware([
    'web',
    InitializeTenancyByDomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {

    Route::get('/', function () {
    $users = [];
    tenancy()->central(function()use(&$users){
        $users = User::all();
    });
    return $users;
    })->name('tenant.home');
});

https://github.com/thegr8awais/tenancyforlaravel/blob/0bb52a96255700d794a60f5f0dfaab583ad58d67/routes/tenant.php#L29 This will result in Call to a member function prepare() on null

error shared via flareapp

stancl commented 2 years ago

Awesome, that helps a lot. Will try to do this soon, thanks!

Riley19280 commented 2 years ago

Any updates on this issue?

vouala commented 2 years ago

I can see what would cause the issue with running $tenant->run() or tenancy()->central() from the context of another tenant, but I don't think there should be issues with using $tenant->run() inside central context.

The original issue makes it a bit unclear when exactly this happens. Can someone confirm if there's any issue with using $tenant->run() in central context?

Indeed, running on central works perfectly

stancl commented 2 years ago

Thank you for confirming that, this will make debugging easier for me. I hope to get to it very soon, since I'm getting the repo ready for v4.

buzkall commented 2 years ago

In case this helps to someone: the "Call to a member function prepare() on null" error was happening to me when running tenancy()->central() because I'm using the session_driver=database and it tries to update the session table when going back to the tenant. If I switch the session_driver to file, the error doesn't happen.

Another option, if you want to keep the database session driver, is to initialize a new db connection for your action:

$dbCentral = \DB::connection('mysql');
$dbCentral->table('TABLE_NAME')->insert([DATA]);
$dbCentral->disconnect();
stancl commented 2 years ago

I haven't had a full look at this yet, will work on this soon, but to share some general thoughts: given these things:

It seems that the issue is that the DB session driver stores the previous tenant's connection/PDO connection(?) which gets removed when we switch to another tenant. And then, I assume the DB session driver tries to store the session data at the end of the request — to the tenant's database, using the stored connection — but the connection doesn't exist anymore.

From the comments I linked, it seems that this happens only on tenant requests, and both when doing:

Both of these things get rid of the previous tenant's DB connection and re-establish it later again. And that works well for most of Laravel, since it only remembers to use the tenant connection (e.g. Eloquent does this — models mostly just need the connection name that they're using). But it seems that the DB session driver breaks when the old PDO connection gets removed.

So the solution would probably be to:

stancl commented 2 years ago

Fixed in v4 🚀 We now have a bootstrapper for database sessions.

gmdimitriz commented 1 year ago

there's a solution for version 3?

oulfr commented 1 year ago

For any one that need a fix in v3 when the database driver is used for session, add this line to your env:

      SESSION_DRIVER=database
      SESSION_CONNECTION=mysql --->this is the line

And make sure the table session existe in the central db

msitarzewski commented 1 year ago

For any one that need a fix in v3 when the database driver is used for session, add this line to your env:

      SESSION_DRIVER=database
      SESSION_CONNECTION=mysql --->this is the line

And make sure the table session existe in the central db

Is there anything else I need to do? This didn't solve my problem with 3.7 and Laravel 10. Thank you!

hballesteross commented 10 months ago

I tried everything posted here but I still have issues using redis queue and session storage with database or redis. I send a job to the queue from a tenant and the job uses tenancy()->central . The first job finishes ok. When I send a second one I get the prepare() on null error at the end. The job gets done but it gets marked as failed and the queue dies.

I know it's fixed in v4 but the announcement was made 16 month ago. Is there any workaround? Thanks in advance.

stancl commented 10 months ago

In v3 you can use a different session driver. If you need to use the DB session driver, you can use v4 early access (details on our Discord).

stancl commented 7 months ago

I'd be careful about the suggestion by @oulfr (I hid the comment) since it likely makes your application vulnerable to session forgery https://tenancyforlaravel.com/docs/v3/session-scoping/#session-scoping

deividfirewall commented 3 months ago

For any one that need a fix in v3 when the database driver is used for session, add this line to your env:

      SESSION_DRIVER=database
      SESSION_CONNECTION=mysql --->this is the line

And make sure the table session existe in the central db

Is there anything else I need to do? This didn't solve my problem with 3.7 and Laravel 10. Thank you!

Great!, This solution works for me, for the error: Call to a member function prepare() on null with this code line:

$currentTenant = tenant();
$sede = tenancy()->central(function ($currentTenant) {
            return Sede::where('tenant_id', $currentTenant->id)->first();
        });

my stack is: Laravel 11, MySql, Inertia Js, Vue 3 and tenancy package V3.8

willfer1049 commented 2 months ago

In my case I had the same problem, defining the SESSION_CONNECTION variable in the env with the connection to the central database worked for me, but all sessions are stored in the central database, and according to Stancl's recommendations this should not be done.

Solution: My definitive solution was to stop using the function to access the central database (I was using it in a middleware) and this was causing the problem, to access the central database I used the connection to the central database in the query and everything was solved.

That is, change this:

$domains = tenancy()->central(function () use ($tenant) {
         return DB::table("tenants")
                ->join("domains", "domains.tenant_id", "tenants.id")
                ->where("tenants.client_id", $tenant->client_id)
                ->whereNotNull("tenants.client_id")
                ->get(["domains.tenant_id", "domains.domain"]);
        });

For this:

$domains = DB::connection("mysql_main")->table("tenants")
            ->join("domains", "domains.tenant_id", "tenants.id")
            ->where("tenants.client_id", $tenant->client_id)
            ->whereNotNull("tenants.client_id")
            ->get(["domains.tenant_id", "domains.domain"]);
stancl commented 2 months ago

If all you need is to query the central database, you can specify the connection name as a workaround yeah. It doesn't solve the issue discussed here but works for that use case.