قفل کردن دیتابیس تست برای اجرای موازی
خلاصهٔ کاملتر
اگه از ابزارهایی مثل Pest یا PhpUnit استفاده میکنی و تستهات رو بهصورت موازی اجرا میکنی، احتمالاً با خطاهای عجیب migration یا transaction مواجه شدی. مشکل اینجاست که چند پروسه همزمان میخوان از یه دیتابیس تست مشترک استفاده کنن و روی هم تأثیر میذارن.
راهحل پیشنهادی خیلی سادهست: یه file lock. با یه کلاس کوچیک PHP میشه دسترسی به دیتابیس رو سریالی کرد؛ یعنی هر پروسه صبر میکنه تا پروسه قبلی کارش تموم بشه و بعد وارد بشه.
کلاس DatabaseLock یه فایل قفل توی پوشه temp سیستم میسازه و با تابع flock PHP قفل انحصاری میگیره. اگه یه پروسه دیگهای قبلاً قفل رو گرفته باشه، پروسه جدید یه پیام واضح نشون میده که داره منتظر میمونه، بهجای اینکه با یه سری خطای گنگ مواجه بشی:
final class DatabaseLock {
private const string WaitingMessage = "Another Pest process is already using the shared test database. Waiting for the lock...\n";
/** @var null|resource */
private static $lockHandle;
public static function acquire(): void {
if (self::$lockHandle !== null) {
return;
}
$lockFile = self::lockFile();
$lockHandle = fopen($lockFile, 'c+');
if ($lockHandle === false) {
throw new RuntimeException("Unable to open the Pest database lock file: $lockFile");
}
if (! flock($lockHandle, LOCK_EX | LOCK_NB)) {
fwrite(STDERR, self::WaitingMessage);
flock($lockHandle, LOCK_EX);
}
self::$lockHandle = $lockHandle;
register_shutdown_function(static fn () => self::release());
}
}نکته جالب اینه که قفل با register_shutdown_function بهصورت خودکار آزاد میشه؛ پس لازم نیست نگران جا موندن قفل باشی.
اسم فایل قفل هم بر اساس hash مسیر root پروژه ساخته میشه، یعنی اگه چند پروژه مختلف داشته باشی، قفلهاشون با هم قاطی نمیشن.
برای اینکه این مکانیزم روی همه تستها اعمال بشه، کافیه در کلاس پایه TestCase متد setUpBeforeClass رو override کنی:
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
DatabaseLock::acquire();
}با این کار هر پروسه تست قبل از شروع صبر میکنه تا دیتابیس آزاد بشه، و دیگه اون خطاهای عجیب migration یا transaction از بین میرن.
این روش مخصوصاً برای feature testهایی مفیده که واقعاً با دیتابیس تعامل دارن، نه unit testهایی که دیتابیس رو mock میکنن.
نکات کلیدی:
- اجرای موازی تستها روی دیتابیس مشترک باعث تداخل و خطاهای گنگ میشه
- یه file lock ساده با flock میتونه دسترسی رو ترتیببندی کنه
- کلاس DatabaseLock فایل قفل رو توی temp سیستم میسازه و با پایان پروسه آزادش میکنه
- هش مسیر پروژه مانع تداخل قفلهای پروژههای مختلف میشه
- فقط باید acquire رو در setUpBeforeClass کلاس پایه TestCase صدا بزنی




