mirror of
https://github.com/archtechx/tenancy.git
synced 2026-06-20 22:54:05 +00:00
Test pulling pending tenants concurrently
This commit is contained in:
parent
652bc987ce
commit
0913614d5f
2 changed files with 112 additions and 0 deletions
34
tests/Etc/pull-worker.php
Normal file
34
tests/Etc/pull-worker.php
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker for the "concurrent pulls" tests.
|
||||||
|
* Separate OS process that boots the same test env and calls pullPendingFromPool().
|
||||||
|
* Since multiple processes run at once, they race for the pool.
|
||||||
|
*
|
||||||
|
* Used like `php pull-worker.php <startAtUnixFloat> <firstOrCreate:0|1>`
|
||||||
|
*
|
||||||
|
* Outputs the key of the pulled tenant, or "null" if nothing was pulled.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use Stancl\Tenancy\Tests\TestCase;
|
||||||
|
|
||||||
|
$startAt = (float) ($argv[1] ?? 0);
|
||||||
|
$firstOrCreate = ($argv[2] ?? '0') === '1';
|
||||||
|
|
||||||
|
// createApplication() replays the suite's central-MySQL config without running setUp(),
|
||||||
|
// so the pending tenants the parent test created survive into this process.
|
||||||
|
(new class('pull-worker') extends TestCase {})->createApplication();
|
||||||
|
|
||||||
|
// Wait so that every worker pulls at the same time
|
||||||
|
if ($startAt > 0.0) {
|
||||||
|
time_sleep_until($startAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = Tenant::pullPendingFromPool($firstOrCreate);
|
||||||
|
|
||||||
|
fwrite(STDOUT, $tenant?->getKey() ?? 'null');
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
@ -28,6 +29,7 @@ use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Events\TenancyEnded;
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
beforeEach($cleanup = function () {
|
beforeEach($cleanup = function () {
|
||||||
Tenant::$extraCustomColumns = [];
|
Tenant::$extraCustomColumns = [];
|
||||||
|
|
@ -126,6 +128,82 @@ test('a new tenant gets created while pulling a pending tenant if the pending po
|
||||||
expect(Tenant::withPending()->get()->count())->toBe(1); // All tenants
|
expect(Tenant::withPending()->get()->count())->toBe(1); // All tenants
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn $count separate PHP processes that all call pullPendingFromPool() at the same
|
||||||
|
* time and return the keys of pulled tenants to simulate concurrent pulls.
|
||||||
|
*
|
||||||
|
* @see tests/Etc/pull-worker.php
|
||||||
|
*/
|
||||||
|
function runConcurrentPulls(int $count, bool $firstOrCreate = false): array
|
||||||
|
{
|
||||||
|
$worker = __DIR__ . '/Etc/pull-worker.php';
|
||||||
|
|
||||||
|
// Shared start instant
|
||||||
|
$startAt = (string) (microtime(true) + 3.0);
|
||||||
|
|
||||||
|
/** @var Process[] $processes */
|
||||||
|
$processes = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$process = new Process(
|
||||||
|
['php', $worker, $startAt, $firstOrCreate ? '1' : '0']
|
||||||
|
);
|
||||||
|
$process->start();
|
||||||
|
$processes[] = $process;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pulledTenants = [];
|
||||||
|
|
||||||
|
foreach ($processes as $process) {
|
||||||
|
$process->wait();
|
||||||
|
|
||||||
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput());
|
||||||
|
|
||||||
|
$output = trim($process->getOutput());
|
||||||
|
|
||||||
|
if ($output !== 'null' && $output !== '') {
|
||||||
|
// If a tenant was pulled, add its key to the results
|
||||||
|
$pulledTenants[] = $output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pulledTenants;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('concurrent pulls each claim a distinct pending tenant', function (bool $firstOrCreate) {
|
||||||
|
Tenant::createPending();
|
||||||
|
Tenant::createPending();
|
||||||
|
Tenant::createPending();
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(3);
|
||||||
|
|
||||||
|
runConcurrentPulls(8, $firstOrCreate);
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(0);
|
||||||
|
expect(Tenant::withoutPending()->count())->toBe($firstOrCreate ? 8 : 3);
|
||||||
|
})->with([
|
||||||
|
'pull pending' => false,
|
||||||
|
'pull pending or create' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('a failed attribute write rolls back the claim and leaves the tenant pending', function () {
|
||||||
|
// The claim and the attribute write share one transaction, so if applying the attributes
|
||||||
|
// fails the claim must roll back and the tenant must stay in the pool.
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->string('slug')->nullable()->unique();
|
||||||
|
});
|
||||||
|
|
||||||
|
Tenant::$extraCustomColumns = ['slug'];
|
||||||
|
|
||||||
|
Tenant::create(['slug' => 'taken']);
|
||||||
|
Tenant::createPending();
|
||||||
|
|
||||||
|
expect(fn () => Tenant::pullPendingFromPool(false, ['slug' => 'taken']))
|
||||||
|
->toThrow(QueryException::class);
|
||||||
|
|
||||||
|
expect(Tenant::onlyPending()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('withoutPending chained with where clauses returns correct results', function () {
|
test('withoutPending chained with where clauses returns correct results', function () {
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
$pendingTenant = Tenant::createPending();
|
$pendingTenant = Tenant::createPending();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue