--- web/core/tests/Drupal/Tests/BrowserTestBase.php.orig 2024-01-09 22:05:30.693134453 -0500 +++ web/core/tests/Drupal/Tests/BrowserTestBase.php 2024-01-09 21:35:05.841690252 -0500 @@ -544,13 +544,28 @@ public function installDrupal() { if (getenv('BROWSERTEST_CACHE_DB')) { $this->initDumpFile(); - if (file_exists($this->dumpFile)) { + + $lock_file = $this->dumpFile . '.lock'; + + $this->runWithLockFile($lock_file, function () use (&$install_from_dump) { + if (file_exists($this->dumpFile)) { + $install_from_dump = TRUE; + } + else { + $this->installDrupalFromProfile(); + $this->dumpDatabase(); + + $install_from_dump = FALSE; + } + }); + + // The check for the dump file happens inside the critical section to + // ensure it blocks while a dump is being created, but the installation + // happens outside the critical section so that multiple tests can use the + // same dump file concurrently. + if ($install_from_dump) { $this->installDrupalFromDump(); } - else { - $this->installDrupalFromProfile(); - $this->dumpDatabase(); - } } else { $this->installDrupalFromProfile(); @@ -571,9 +586,58 @@ } sort($modules); array_unique($modules); - $cache_dir = getenv('BROWSERTEST_CACHE_DIR') ?: sys_get_temp_dir() . '/test_dumps/' . \Drupal::VERSION; - is_dir($cache_dir) || mkdir($cache_dir, 0777, TRUE); - $this->dumpFile = $cache_dir . '/_' . md5(implode('-', $modules)) . '.sql'; + $temp_dir = sys_get_temp_dir(); + $cache_dir = getenv('BROWSERTEST_CACHE_DIR') ?: $temp_dir . '/test_dumps/' . \Drupal::VERSION; + $this->dumpFile = $cache_dir . '/' . md5(implode('-', $modules)) . '.sql'; + + // Create a folder to contain the test database dumps, if it does not + // already exist. Only one test is allowed to create this at a time. + $lock_file = sprintf("%s/test_dumps-%s.lock", $temp_dir, \Drupal::VERSION); + $this->runWithLockFile($lock_file, function () use ($cache_dir) { + if (!is_dir($cache_dir)) { + mkdir($cache_dir, 0777, TRUE); + } + }); + } + + /** + * Acquires an exclusive lock on the specified file and runs the given code. + * + * This is used to ensure that only one test is executing the same code at the + * same time on the same runner. The lock is released after the critical code + * returns. + * + * @param string $lock_file_path + * The path to the lock file. + * @param callable $critical_code + * The code to invoke only once the lock is obtained. + */ + private function runWithLockFile(string $lock_file_path, callable $critical_code) { + $locked = FALSE; + $lock_handle = fopen($lock_file_path, 'w'); + try { + if (($lock_handle !== FALSE) && flock($lock_handle, LOCK_EX)) { + $locked = TRUE; + + $critical_code(); + } + else { + $this->fail('Failed to acquire lock: ' . $lock_file_path); + } + } + finally { + // Delete the lock file before unlocking it to ensure we don't delete a + // lock created by another process. + if (file_exists($lock_file_path)) { + @unlink($lock_file_path); + } + if ($locked) { + @flock($lock_handle, LOCK_UN); + } + if ($lock_handle !== FALSE) { + @fclose($lock_handle); + } + } } /**