AI coding agents love to run tests in parallel processes.
Running tests in parallel is great until multiple processes try to use the same local test database at once. This is mainly useful for feature tests that actually touch the database.
A small file lock can serialize access and stop those runs from stepping on each other.
1final class DatabaseLock
2{
3 private const string WaitingMessage = "Another Pest process is already using the shared test database. Waiting for the lock...\n";
4
5 /** @var null|resource */
6 private static $lockHandle;
7
8 public static function acquire(): void
9 {
10 if (self::$lockHandle !== null) {
11 return;
12 }
13
14 $lockFile = self::lockFile();
15 $lockHandle = fopen($lockFile, 'c+');
16
17 if ($lockHandle === false) {
18 throw new RuntimeException("Unable to open the Pest database lock file: $lockFile");
19 }
20
21 if (! flock($lockHandle, LOCK_EX | LOCK_NB)) {
22 fwrite(STDERR, self::WaitingMessage);
23 flock($lockHandle, LOCK_EX);
24 }
25
26 self::$lockHandle = $lockHandle;
27
28 register_shutdown_function(static fn () => self::release());
29 }
30
31 private static function release(): void
32 {
33 if (self::$lockHandle === null) {
34 return;
35 }
36
37 flock(self::$lockHandle, LOCK_UN);
38 fclose(self::$lockHandle);
39
40 self::$lockHandle = null;
41 }
42
43 private static function lockFile(): string
44 {
45 $workspaceRoot = dirname(__DIR__, 2);
46 $workspaceHash = md5($workspaceRoot);
47 $temporaryDirectory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR);
48
49 return "$temporaryDirectory/database-$workspaceHash.lock";
50 }
51}
This class creates a lock file in your temporary directory and releases it automatically when the process ends.
Add the acquiring of the lock in your base TestCase to make sure every test process waits its turn:
1public static function setUpBeforeClass(): void
2{
3 parent::setUpBeforeClass();
4
5 DatabaseLock::acquire();
6}
Instead of a pile of unrelated migration or transaction errors, you now get a clear message telling you the test run is waiting for the database lock.