レースコンディションは同時に実行される複数のプロセスが共有リソースにアクセスし、その結果が期待しない状態になる問題です。例えば、状態管理システムでほぼ同時に状態をを完了にするリクエストが送られ二重に完了状態へ移行する処理がなされる、といった具合です。これをちゃんと防げているか自動テストで確かめたい時があります。この記事ではPHPのSwooleを使ってレースコンディションのテストを行う方法について紹介します。
Swoole は PHP の拡張です。PHPのdebian系のDockerイメージならば次のように書くことで Swoole 拡張を追加して有効化できます。
RUN apt-get update && apt-get install -y libbrotli-dev && \
pecl install swoole && \
docker-php-ext-enable swoole
Swooleは PHP の公式ドキュメントにも使用方法が載っています。それに基づくとレースコンディションのテストを行うためのPHPコードは次のように作れます。
<?php
namespace Tests\Process;
use App\Models\Eloquents\Account\Account;
use Carbon\Carbon;
use Illuminate\Database\QueryException;
use Tests\TestCase;
class ProcessCompleteTest extends TestCase
{
/**
* @test
* @return void
*/
public function 二重完了問題のテスト(): void
{
// 問題が起きるデータベースを再現します
// ここでレースコンディションになりうるデータを用意します。
// この時点では process_logs テーブルにログが2つだけあります。
// 完了リクエストをほぼ同時に2つ送信した後、完了ログが1つだけ増えればテスト成功です。
$start = microtime(true);
\Artisan::call('db:wipe');
\DB::unprepared(file_get_contents(__DIR__.'/base.sql'));
\Artisan::call('migrate');
$end = microtime(true);
dump('データベースの再現にかかった時間: '.($end - $start).'秒');
// 操作可能なアカウントでログインする
\Auth::login(User::query()->where('type', 'ADMIN')->inRandomOrder()->first());
// リクエスト前の状態を取得
Carbon::setTestNow('2024-07-30 10:38:00.000');
\Swoole\Runtime::enableCoroutine(); // コルーチンを有効にします
$result = [];
// 並列実行を行います
\Co\run(function () use (&$result) {
$co1 = go(function () use (&$result) {
// 並行してPOSTリクエストを送ります(1つ目)
$result[0] = $this->json('POST', '/api/process/1234/complete', [
'processIdList' => ['01j40meehhv9d9awj6xhxc6bgs'],
]);
});
$co2 = go(function () use (&$result) {
// 並行してPOSTリクエストを送ります(2つ目)
$result[1] = $this->json('POST', '/api/process/1234/complete', [
'processIdList' => ['01j40meehhv9d9awj6xhxc6bgs'],
]);
});
\co::join([$co1, $co2]); // 両方のコルーチンが完了するのを待ちます
});
// ここで dump($result) すれば両レスポンスの内容が見れます。
// レースコンディションが発生せず、保存が正常に行われたことを検証します
$this->assertDatabaseHas('process_logs', [
'action' => 'TO_COMPLETE'
]);
// 元々 INIT, START の2つのログがあり、適切に完了ログが追加されていれば3つになります
// 適切に完了処理がなされたことを確認します
$this->assertDatabaseCount('process_logs', 3);
}
}
こんな感じで Swoole を使うとリクエストを並行で処理し、レースコンディションのテストができます。例は元とした実例の都合で Laravel で書いてますが Swoole はPHP拡張であり、より広範囲で使えます。
余談ですが Swoole を使うと IDE が未定義クラス、未定義メソッドなどで警告をこれでもかと出してきます。次のIDEヘルパーをプロジェクトに追加することで適切にドキュメントとコードを紐づけられます。