tenancy
tenancy copied to clipboard
Run tenant aware query from central/tenants context
Describe the bug
- When we use $tenant->run() to perform query on another tenants from central/tenants context it should work as expected.
for example :
$tenant->run(function () { User::create(...); });
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
- Make env file to use
SESSION_DRIVER=database
- Run tenants specific query.
Expected behavior
- It should run the tenant specific query and return to central or whatever tenants we calling from.
Your setup
- Laravel version: 6.2.x
- stancl/tenancy version: 3.4.0
- Storage driver: DB
- Session Driver : DB
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
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
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:
I see. Seems to be related to the session driver then. I'll try to take a look at this soon
Yes. From my debugging:
- The runForMultiple function succeeds.
- The SessionManager correctly inits a connection using the 'tenant' db connection.
- But somewhere, in the DatabaseSessionHandler the PDO attribute of the Connection becomes null, causing the error.
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.
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
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.
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?
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.
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.
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?
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
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.
Hai, I face quite similar issue When I run
tenancy()->central(function(){})
It gives meERROR: Call to a member function prepare() on null.
why it is happen?
me also got this error, what the solution for this ?
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
The change sadly did not solve my Job calling tenancy()->central(function(){})
returning a
Call to a member function prepare() on null.
error.
Hmm okay, I'll take a separate look at this then. Thanks for testing!
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?
@stancl we are facing the same issue , do you have any updates regarding this issue ?
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.
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.
Any update on this guys?
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.
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
Awesome, that helps a lot. Will try to do this soon, thanks!
Any updates on this issue?
I can see what would cause the issue with running
$tenant->run()
ortenancy()->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
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.
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();
I haven't had a full look at this yet, will work on this soon, but to share some general thoughts: given these things:
- https://github.com/archtechx/tenancy/issues/547#issuecomment-1032589450
- https://github.com/archtechx/tenancy/issues/547#issuecomment-1148488387
- https://github.com/archtechx/tenancy/issues/547#issuecomment-1124193821
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:
-
$tenant->run()
— therefore temporarily switching to another tenant -
tenancy()->central()
— therefore temporarily interrupting tenancy
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:
- dive into the source code of the DB session driver, to see how it gets stored, and used later on at the end of the request
- try to understand why that class specifically uses the PDO connection(?) and works differently than the rest of Laravel
- find a way to reset this at the end of the request lifecycle, before the session gets written to the DB, perhaps using some event listener or hook on the DB session driver class