| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- <?php
- /** Two independent MySQL connections, deterministically interleaved at the enterprise lock. */
- require dirname(__DIR__) . '/vendor/autoload.php';
- \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
- \support\App::loadAllConfig(['route']);
- date_default_timezone_set('Asia/Shanghai');
- use app\miniapi\auth\Principal;
- use app\miniapi\exception\ApiException;
- use app\miniapi\model\MiniAudit;
- use app\miniapi\model\MiniCommand;
- use app\miniapi\model\MiniGrant;
- use app\miniapi\model\MiniUser;
- use app\miniapi\service\Access;
- use app\miniapi\service\EnterpriseCommands;
- use app\miniapi\service\WaterPresenter;
- use Illuminate\Container\Container;
- use Illuminate\Database\ConnectionResolver;
- use Illuminate\Database\Connectors\ConnectionFactory;
- use plugin\saiadmin\app\model\water\WaterAccount;
- use plugin\saiadmin\app\model\water\WaterDelivery;
- use plugin\saiadmin\app\model\water\WaterEnterprise;
- use plugin\saiadmin\app\model\water\WaterLedger;
- use plugin\saiadmin\app\model\water\WaterSku;
- use plugin\saiadmin\app\service\WaterBusiness;
- use support\Model;
- $checks = 0;
- function check(bool $ok, string $message): void
- {
- global $checks;
- $checks++;
- if (!$ok) {
- throw new RuntimeException($message);
- }
- }
- $db = (new MiniUser())->getConnection();
- $originalResolver = Model::getConnectionResolver();
- $peer = (new ConnectionFactory(new Container()))->make($db->getConfig(), 'mini_concurrency_peer');
- $peerResolver = new ConnectionResolver(['mini_concurrency_peer' => $peer]);
- $peerResolver->setDefaultConnection('mini_concurrency_peer');
- $enterprise = $sku = $user = 0;
- $armed = false;
- $tag = 'MINI-RACE-' . bin2hex(random_bytes(8));
- $now = date('Y-m-d H:i:s');
- try {
- check($peer->getPdo() !== $db->getPdo(), 'test requires independent connections');
- // Commit only isolated fixtures so the second connection can see them.
- $db->transaction(function () use (&$enterprise, &$sku, &$user, $tag, $now) {
- $enterprise = WaterEnterprise::query()->insertGetId([
- 'name' => $tag, 'contact' => 'Concurrency test', 'phone' => '13812345678',
- 'address' => $tag . '-original', 'created_at' => $now, 'updated_at' => $now,
- ]);
- $sku = WaterSku::query()->insertGetId([
- 'name' => $tag, 'liters' => 18.9, 'available' => 10, 'created_at' => $now, 'updated_at' => $now,
- ]);
- WaterAccount::query()->insert([
- 'enterprise_id' => $enterprise, 'sku_id' => $sku, 'entitlement' => 10, 'updated_at' => $now,
- ]);
- $user = MiniUser::query()->insertGetId([
- 'appid' => $tag, 'openid' => $tag, 'created_at' => $now, 'updated_at' => $now,
- ]);
- MiniGrant::query()->insert([
- 'user_id' => $user, 'role' => 'enterprise_admin', 'resource_id' => $enterprise,
- 'created_by' => 0, 'created_at' => $now, 'updated_at' => $now,
- ]);
- });
- $revision = WaterPresenter::revision(WaterEnterprise::query()->findOrFail($enterprise));
- $interleaved = false;
- $armed = true;
- $db->beforeExecuting(function (string $sql) use (&$armed, &$interleaved, $db, $peerResolver, $originalResolver, $enterprise, $sku) {
- if (!$armed || !str_contains($sql, 'sa_water_enterprise') || !str_contains(strtolower($sql), 'for update')) {
- return;
- }
- $armed = false;
- check($db->transactionLevel() > 0, 'address transaction already started');
- check(!WaterDelivery::query()->where('enterprise_id', $enterprise)->exists(), 'address snapshot predates booking');
- try {
- Model::setConnectionResolver($peerResolver);
- (new WaterBusiness())->createDelivery(['enterprise_id' => $enterprise, 'sku_id' => $sku, 'quantity' => 2], 0);
- } finally {
- Model::setConnectionResolver($originalResolver);
- }
- check(!WaterDelivery::query()->where('enterprise_id', $enterprise)->exists(), 'plain read still misses committed booking under REPEATABLE READ');
- $interleaved = true;
- });
- try {
- (new EnterpriseCommands(new Access(new Principal((string) $user))))->address($enterprise, [
- 'request_key' => $tag . '-address', 'revision' => $revision,
- 'address' => $tag . '-moved', 'contact' => 'Concurrency test', 'phone' => '13812345678',
- ]);
- throw new RuntimeException('Address change must reject the concurrent booking');
- } catch (ApiException $error) {
- check($error->getCode() === 40902, 'current read rejects active task');
- }
- check($interleaved, 'booking committed before enterprise lock');
- check(WaterEnterprise::query()->findOrFail($enterprise)->address === $tag . '-original', 'address preserved');
- check(WaterDelivery::query()->where('enterprise_id', $enterprise)->count() === 1, 'concurrent booking remains committed');
- check(!MiniCommand::query()->where('user_id', $user)->exists(), 'rejected command not persisted');
- } finally {
- $armed = false;
- Model::setConnectionResolver($originalResolver);
- while ($db->transactionLevel() > 0) {
- $db->rollBack();
- }
- $peer->disconnect();
- // Exact fixture IDs only; existing enterprises, inventory and users are never modified.
- $db->transaction(function () use ($enterprise, $sku, $user) {
- if ($enterprise) {
- WaterLedger::query()->where('enterprise_id', $enterprise)->delete();
- WaterDelivery::query()->where('enterprise_id', $enterprise)->delete();
- WaterAccount::query()->where('enterprise_id', $enterprise)->delete();
- WaterEnterprise::query()->where('id', $enterprise)->delete();
- }
- if ($sku) {
- WaterSku::query()->where('id', $sku)->delete();
- }
- if ($user) {
- MiniAudit::query()->where('actor_type', 'mini')->where('actor_id', $user)->delete();
- MiniCommand::query()->where('user_id', $user)->delete();
- MiniGrant::query()->where('user_id', $user)->delete();
- MiniUser::query()->where('id', $user)->delete();
- }
- });
- }
- check(!WaterEnterprise::query()->where('id', $enterprise)->exists(), 'enterprise fixture cleaned');
- check(!WaterSku::query()->where('id', $sku)->exists(), 'inventory fixture cleaned');
- check(!MiniUser::query()->where('id', $user)->exists(), 'identity fixture cleaned');
- echo "PASS: $checks two-connection address checks; isolated fixtures cleaned.\n";
|