Closed bliveinhack closed 2 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
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:
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]
"} ``
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]
"} ``
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]
"} ``
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).
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:
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 tenanttenancy()->central()
— therefore temporarily interrupting tenancyBoth 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:
Fixed in v4 🚀 We now have a bootstrapper for database sessions.
there's a solution for version 3?
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
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!
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.
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).
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
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
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"]);
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.
Describe the bug
$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 toSESSION_DRIVER=database
. it fails and giveCall to a member function prepare() on null.
Steps to reproduce
SESSION_DRIVER=database
Expected behavior
Your setup