1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-05-06 17:44:04 +00:00

Enhance tenant migration command to support parallel execution

- Added a `--parallel` option to the Migrate command for concurrent tenant migrations.
- Introduced a `parallel-batch-size` option to control the number of concurrent migrations.
- Implemented a new method `runParallel` to handle parallel migrations.
- Updated the `handle` method to initiate parallel migrations when the option is set.
- Added tests to verify the functionality of the parallel migration option.
This commit is contained in:
khaledSayed93 2026-03-28 23:09:42 +02:00
parent ab64f4599d
commit 3498a3f9c2
3 changed files with 219 additions and 10 deletions

View file

@ -10,12 +10,18 @@ use Illuminate\Database\Migrations\Migrator;
use Stancl\Tenancy\Concerns\DealsWithMigrations;
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
use Stancl\Tenancy\Concerns\HasATenantsOption;
use Stancl\Tenancy\Concerns\ParallelTenantMigrator;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Events\DatabaseMigrated;
use Stancl\Tenancy\Events\MigratingDatabase;
use Symfony\Component\Console\Input\InputOption;
class Migrate extends MigrateCommand
{
use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand;
use HasATenantsOption {
getOptions as private tenantsCommandOptions;
}
use DealsWithMigrations, ExtendsLaravelCommand, ParallelTenantMigrator;
protected $description = 'Run migrations for tenant(s)';
@ -31,12 +37,20 @@ class Migrate extends MigrateCommand
$this->specifyParameters();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
protected function getOptions()
{
return array_merge([
[
'parallel-batch-size',
null,
InputOption::VALUE_OPTIONAL,
'Maximum concurrent tenant migrations per batch when using --parallel',
'10',
],
], $this->tenantsCommandOptions());
}
public function handle(): int
{
foreach (config('tenancy.migration_parameters') as $parameter => $value) {
if (! $this->input->hasParameterOption($parameter)) {
@ -45,15 +59,73 @@ class Migrate extends MigrateCommand
}
if (! $this->confirmToProceed()) {
return;
return static::FAILURE;
}
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
if ($this->option('parallel')) {
return $this->runParallel();
}
$this->runMigrationForTenants($this->option('tenants'));
return static::SUCCESS;
}
private function runParallel(): int
{
if (! class_exists(\Illuminate\Support\Facades\Concurrency::class)) {
$this->error('Parallel tenant migrations require Laravel 11 or newer (Concurrency facade).');
return static::FAILURE;
}
$keys = $this->tenantKeys();
if ($keys === []) {
$this->info('No tenants to migrate.');
return static::SUCCESS;
}
$this->info('Running migrations in parallel');
$this->runParallelTenantBatches(
$keys,
max(1, (int) $this->option('parallel-batch-size')),
function (int $batchIndex, int $batchTotal, array $batch): void {
$n = count($batch);
$this->line(sprintf(
'Parallel batch %d/%d (%d tenant%s)',
$batchIndex + 1,
$batchTotal,
$n,
$n === 1 ? '' : 's'
));
}
);
return static::SUCCESS;
}
/**
* @return list<string|int>
*/
private function tenantKeys(): array
{
$keys = [];
foreach ($this->getTenants() as $tenant) {
$keys[] = $tenant instanceof Tenant ? $tenant->getTenantKey() : $tenant;
}
return $keys;
}
protected function runMigrationForTenants(?array $tenants = []): void
{
tenancy()->runForMultiple($tenants, function ($tenant) {
$this->line("Tenant: {$tenant->getTenantKey()}");
event(new MigratingDatabase($tenant));
// Migrate
parent::handle();
event(new DatabaseMigrated($tenant));

View file

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Concerns;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Concurrency;
use RuntimeException;
/**
* Parallel `tenants:migrate` via {@see Concurrency} and option forwarding for nested Artisan calls.
*/
trait ParallelTenantMigrator
{
/**
* Options for nested `tenants:migrate` (parallel workers). Omits `--parallel`.
*
* @return array<string, mixed>
*/
protected function optionsForNestedMigrateCall(): array
{
$options = [];
if ($database = $this->option('database')) {
$options['--database'] = $database;
}
if ($this->option('force')) {
$options['--force'] = true;
}
if ($paths = $this->option('path')) {
$options['--path'] = $paths;
}
if ($this->option('realpath')) {
$options['--realpath'] = true;
}
if ($schemaPath = $this->option('schema-path')) {
$options['--schema-path'] = $schemaPath;
}
if ($this->option('pretend')) {
$options['--pretend'] = true;
}
if ($this->option('seed')) {
$options['--seed'] = true;
}
if ($seeder = $this->option('seeder')) {
$options['--seeder'] = $seeder;
}
if ($this->option('step')) {
$options['--step'] = true;
}
if ($this->option('graceful')) {
$options['--graceful'] = true;
}
if ($this->option('no-interaction')) {
$options['--no-interaction'] = true;
}
return $options;
}
/**
* @param list<string|int> $keys
* @param callable(int $batchIndex, int $batchTotal, list<string|int> $batchKeys): void $beforeBatch
*/
protected function runParallelTenantBatches(array $keys, int $batchSize, callable $beforeBatch): void
{
$forward = $this->optionsForNestedMigrateCall();
$batches = array_chunk($keys, max(1, $batchSize));
$total = count($batches);
foreach ($batches as $i => $batch) {
$beforeBatch($i, $total, $batch);
$tasks = [];
foreach ($batch as $key) {
$tasks[] = static fn () => self::migrateOneTenantViaArtisan($key, $forward);
}
Concurrency::run($tasks);
}
}
/**
* @param array<string, mixed> $forwardOptions
*/
private static function migrateOneTenantViaArtisan(string|int $key, array $forwardOptions): void
{
$code = Artisan::call('tenants:migrate', array_merge($forwardOptions, [
'--tenants' => [$key],
'--force' => true,
]));
if ($code !== 0) {
throw new RuntimeException("Tenant migration failed for [{$key}] with exit code {$code}.");
}
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Schema;
@ -92,6 +93,34 @@ class CommandsTest extends TestCase
$this->assertTrue(Schema::hasTable('users'));
}
#[Test]
public function migrate_command_works_with_parallel_option()
{
if (! class_exists(Concurrency::class)) {
$this->markTestSkipped('Parallel tenant migrations require the Concurrency facade (Laravel 11+).');
}
config(['concurrency.default' => 'sync']);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
$this->assertFalse(Schema::hasTable('users'));
Artisan::call('tenants:migrate', [
'--parallel' => true,
]);
$this->assertFalse(Schema::hasTable('users'));
tenancy()->initialize($tenant1);
$this->assertTrue(Schema::hasTable('users'));
tenancy()->end();
tenancy()->initialize($tenant2);
$this->assertTrue(Schema::hasTable('users'));
}
#[Test]
public function rollback_command_works()
{