How to use parallel testing in multi tenant laravel application? - laravel

I am using Stancl Tenancy for a multi tenant Laravel 8 app. I have this testing well in a single core, however I want to take advantage of the new feature to run tests in parallel.
I think to do this sensibly I should create a new tenant for each database that is created by the parallelization. I've been trying to do this using the ParallelTesting::setUpTestDatabase callback, but am getting some odd behaviour.
When I go on to run tests in parallel, only database 5 of 7 (and consistently this one) has a tenant created for it:
AppServiceProvider
ParallelTesting::setUpTestDatabase(function ($database, $token) {
$tenant = Tenant::create(['id'=>"testTenant$token",'name'=>"PHPUnit Process $token"]);
});
Without the call to create a model, the databases are being setup correctly:
ParallelTesting::setUpTestDatabase(function ($database, $token) {
//$tenant = Tenant::create(['id'=>"testTenant$token",'name'=>"PHPUnit Process $token"]);
Log::info("Setup called by $token My database connection is ".DB::connection()->getDatabaseName());
});
[2021-10-21 14:45:32] testing.INFO: Setup called by 4 My database connection is project_test_4
[2021-10-21 14:45:32] testing.INFO: Setup called by 3 My database connection is project_test_3
[2021-10-21 14:45:32] testing.INFO: Setup called by 7 My database connection is project_test_7
[2021-10-21 14:45:32] testing.INFO: Setup called by 6 My database connection is project_test_6
[2021-10-21 14:45:34] testing.INFO: Setup called by 5 My database connection is project_test_5
With the line to create the model uncommented, nothing is logged and only the last connection 5 gets a Tenant created for it. Subsequent tests then appear to use the correct connection as 4/5 of them fail for TenantNotIdentified (because they have no tenant or domain).
Can anyone spot the problem?

The setupTestDatabase callback is not helpful for this as the database has not yet been migrated. Multi tenancy can work with parallel testing (after a lot of work on this!) if:
DatabaseTransactions is used rather than RefreshDatabbase
You manually handle restarting database transactions for your tenant tests
You do some of the cleanup manually
Register a new service provider
class ParallelTestingMultiTenantProvider extends ServiceProvider
{
/**
* Register services.
*
* #return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
ParallelTesting::setUpTestCase(function ($token, $testCase) {
$tenant = Tenant::firstOrCreate(['id' => "test_{$token}_tenant", 'name' => "testTenant$token"]);
$tenant->domains()->firstOrCreate(['domain'=>"testtenant$token.site.test"]);
});
ParallelTesting::tearDownProcess(function ($token) {
try {
DB::disconnect();
config(['database.connections.mysql.database'=>"site_test_{$token}"]);
DB::reconnect();
if($tenant = Tenant::find("test_{$token}_tenant")){
$tenant->delete();
}
}
catch(\Exception $e){
if($e->getCode() !== 1049){ //Intercepts database does not exist errors - where tests do not use Database traits
throw $e;
}
}
});
}
}
Add to setup() in your base test classes
class TenantTestCase extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
if($this->isTestingInParallel()){
$token = ParallelTesting::token();
tenancy()->initialize(Tenant::find("test_{$token}_tenant"));
DB::beginTransaction(); //Needed as otherwise database transactions don't work on the tenant database
URL::forceRootUrl("https://testtenant$token.site.test");
}
else {
//Test setup for when not running in parallel
}
}
protected function tearDown(): void
{
if($this->isTestingInParallel()){
DB::rollBack();
}
else {
//Tear down if not testing in parallel
}
parent::tearDown();
}
protected function isTestingInParallel() : bool
{
return (bool)ParallelTesting::token();
}
}
This allowed me to reduce time for a whole test suite run from 1m 30 to 16s, whilst retaining the ability to run tests not in parallel for red green using if statements as above.

Related

How to include Laravel model boot "deleting" method into DB transaction with the main model delete?

I have a model Agent and it has many agent accounts.
public function agentAccounts(): Relation
{
return $this->hasMany(AgentAccount::class);
}
I want to delete them in one transaction but using boot method
public static function boot()
{
parent::boot();
self::deleting([self::class, 'onDeleting']);
}
I understand, that when I create a Db transaction inside "onDeleting" funcion like this
public static function onDeleting(self $model): void
{
DB::transaction(function () use ($model) {
$agentAccounts = $model->agentAccounts;
foreach ($agentAccounts as $agentAccount) {
/* #var $agentAccount AgentAccount */
$agentAccount->delete();
}
}, 5);
}
The db transaction does not include the deletion of the agent itself.
It precedes the agent deletion db transaction.
In my case agent deletion can fail due to some SQL level restrictions not related to agentAccounts
and If I use the exampel above I can end up with all agentAccounts deleted but the Agent - preserved.
I don't want that to happen.
I want them either get deleted together, or be preserved together.
How can I do it?
I believe DB events are run in a blocking mode, so it's enough when calling the delete() on a model, to do it inside of a transaction, like that
DB::transaction(function () use($user) { $user->delete(); });

Reset Cache TTL in each Access

I was wondering if there is any way to update cache TTL from the last time it has accessed?
currently, I have a method to login to adobe connect with API call and API session is valid for 4 days from the last call.
but my cache driver only keeps session in the cache for 4 days from the moment that is added. but I want to keep it for 4 days since the last time it has accessed!
is there any way to update Cache TTL?
I'm sure forgetting and reinserting key is not best practice.
/**
* Login Client Based on information that introduced in environment/config file
*
* #param Client $client
*
* #return void
*/
private function loginClient(Client $client)
{
$config = $this->app["config"]->get("adobeConnect");
$session = Cache::store($config["session-cache"]["driver"])->remember(
$config['session-cache']['key'],
$config['session-cache']['expire'],
function () use ($config, $client) {
$client->login($config["user-name"], $config["password"]);
return $client->getSession();
});
$client->setSession($session);
}
You could listen for the event CacheHit, test for the key pattern, and reset the cache for that key with a new TTL.
To do that, you should create a new listener and add it to the EventServiceProvider:
protected $listen = [
'Illuminate\Cache\Events\CacheHit' => [
'App\Listeners\UpdateAdobeCache',
]
];
And the listener:
class UpdateAdobeCache {
public function handle(CacheHit $event)
{
if ($event->key === 'the_cache_key') { // you could also test for a match with regexp
Cache::store($config["session-cache"]["driver"])->put($event->key, $event->value, $newTTL);
}
}
}

Change SMTP user according to the user in Laravel 5

I want to assign different SMTP hosts to different authenticated users so that the privileged users can send mails faster through a dedicated SMTP server.
I can change the host in the service provider like:
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->extend('swift.transport', function ($transportManager, $app) {
$app->make('config')->set('mail.host', 'just.testing.com');
return new TransportManager($app);
});
}
}
However since I need the authenticated user I created a listener listening to "Authenticated" event and moved the code there like:
class ChangeSmtpServer
{
public function handle($event)
{
app()->extend('swift.transport', function ($transportManager, $app) use ($event) {
$app->make('config')->set('mail.host', $event->user->smtp_server);
return new TransportManager($app);
});
}
}
The host is not changed this time... So inside the service provider I can overwrite the setting but not inside the listener.
Any ideas why?
Your code works on my setup just fine. Actually it should still work if you keep it in AppServiceProvider because Laravel will only resolve bindings when they are relevant. So the code pertaining to Mail driver configuration will not be run until you actually try to send a Mail. By that point your user will already be authenticated. However...
This will only work when you send your mail synchronously. When you want to send from a Queue worker, there won't be any authenticated user and the Authenticated event will never be called. You need a way to keep track of which user is sending the e-mail.
Here is my solution:
Add a sender argument to your Mail class constructor (the one in App\Mail) that takes in the User object that's sending the e-mail.
public $sender;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct(User $sender)
{
$this->sender = $sender;
}
Then add this method that configures your SwiftMailer instance
private function usingSendersSmtp()
{
$mailTransport = app()->make('mailer')
->getSwiftMailer()
->getTransport();
if ($mailTransport instanceof \Swift_SmtpTransport) {
/** #var \Swift_SmtpTransport $mailTransport */
$mailTransport->setHost($this->sender->smtp_host);
$mailTransport->setUsername($this->sender->smtp_username);
$mailTransport->setPassword($this->sender->smtp_password);
// Port and authentication can also be configured... You get the picture
}
return $this;
}
And finally call it inside your build method:
public function build()
{
return $this->usingSendersSmtp()
->view('test');
}
When sending the mail, instantiate your class like new YourMailClass(auth()->user()) and then send it or queue it with the Mail facade to whomever you like. It also might be a good idea to create an abstract class that inherits Illuminate\Mail\Mailable and move these extra stuff over there so you won't have to duplicate this in every other mail class. Hope this helps!

Expire for Laravel Jobs

I parse some HTML pages and API endpoints, for example, every 5 minutes to track changes. For this purpose, I have created ParseJob where I do parsing and save changes to a database. ParseJob implements interface ShouldQueue and I have changed queue driver to Redis. In order to run the ParseJob on a regular basis, I have created ParseCommand and added it to schedule:
class ParseCommand extends Command
{
protected $signature = 'application:my-parse-command';
public function handle()
{
$this->dispatch(new ParseJob());
}
}
class Kernel extends ConsoleKernel
{
protected $commands = [
Commands\ParseCommand::class
];
protected function schedule(Schedule $schedule)
{
$schedule->command('application:my-parse-command')
->everyFiveMinutes();
}
}
And the queue worker is started as a daemon to process the queue. So, every 5 minutes ParseJob is pushed to the queue and the queue worker is processing the job.
Sometimes queue worker process crashes, freezes or for other reasons is not working. But jobs every 5 minutes are pushed into the queue. After an hour of downtime, I have 12 jobs in the queue but they are for that time irrelevant because I do not need to parse 12 times at a certain time, I want just one parse job.
So I want to set TTL for a job that works like expire command in Redis. How to do that? Or maybe you have an alternative solution?
As far a i know it is not possible to set explicitly a Job expiration into Laravel queues. A solution could be setting an expires_at property within your ParseJob and check before executing:
class ParseCommand extends Command
{
protected $signature = 'application:my-parse-command';
public function handle()
{
$this->dispatch(new ParseJob(Carbon::now()->addMinutes(5)));
}
}
then in your Job class
class ParseJob {
protected $expires_at;
public function __construct(Carbon $expires_at) {
$this->expires_at = $expires_at;
}
public function handle()
{
if(!Carbon::now()->gt($this->expires_at)) {
// Parse data
}
}
}
At larvel 8/9 in job
MyJObClass
public function retryUntil(): Carbon
{
return now()->addMinutes(10);
}
Laravel :: Queues #time-based-attempts

Is the database class in Laravel use the "Singleton Pattern"?

I am new to Laravel. Does the Laravel create a database connection every time for each query of the program or use the same database object throughout the program using the "Singleton" Pattern? Does the strategy have an impact on performance, especially for Enterprise Level Applications?
Short answer
No, it's not a singleton, but a factory pattern. However the same connection will be reused if possible and you don't manually request it to reconnect. There is no performance hit.
Long answer
At the beginning of ever request lifecycle, in app/bootstrap/start.php an instance of Illuminate\Foundation\Application gets created. This servers as IoC container.
Shortly after creating the application all service providers will be loaded. The service providers are defined in app/config/app.php
'providers' => array(
// ...
'Illuminate\Database\DatabaseServiceProvider',
// ...
),
Let's have a look at the Illuminate\Database\DatabaseServiceProvider shall we? The important part is the register function
$this->app->bindShared('db', function($app)
{
return new DatabaseManager($app, $app['db.factory']);
});
An instance of DatabaseManager gets bound to db. This instance will stay the same over the whole request and will be used for every database request.
Example from "reverse" direction
Say you call
DB::table('users')->get();
First, the DB facade will resolve to the instance of DatabaseManager that gets bound in the DatabaseServiceProvider using bindShared('db')
protected static function getFacadeAccessor() { return 'db'; }
Then the table('users') call gets forwarded because the method doesn't exist in Database Manager
public function __call($method, $parameters)
{
return call_user_func_array(array($this->connection(), $method), $parameters);
}
It is called on the return value of $this->connection()
public function connection($name = null)
{
list($name, $type) = $this->parseConnectionName($name);
// If we haven't created this connection, we'll create it based on the config
// provided in the application. Once we've created the connections we will
// set the "fetch mode" for PDO which determines the query return types.
if ( ! isset($this->connections[$name]))
{
$connection = $this->makeConnection($name);
$this->setPdoForType($connection, $type);
$this->connections[$name] = $this->prepare($connection);
}
return $this->connections[$name];
}
With if (!isset($this->connections[$name])) it will check if the connection already has been established and will only make a new connection if not.
Then it returns the connection and table('users')->get() will be executed.

Resources