Skip to content

Commit 93c3c51

Browse files
staabmclxmstaab
andauthored
use shared read/write lock in ReflectionCache to prevent race conditions (#160)
Co-authored-by: Markus Staab <[email protected]>
1 parent 132e704 commit 93c3c51

File tree

1 file changed

+57
-38
lines changed

1 file changed

+57
-38
lines changed

src/QueryReflection/ReflectionCache.php

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,26 @@ final class ReflectionCache
2828
*/
2929
private $changes = [];
3030

31+
/**
32+
* @var resource
33+
*/
34+
private static $lockHandle;
35+
3136
private function __construct(string $cacheFile)
3237
{
3338
$this->cacheFile = $cacheFile;
39+
40+
if (null === self::$lockHandle) {
41+
// prevent parallel phpstan-worker-process from writing into the cache file at the same time
42+
// XXX we use a single system-wide lock file, which might get problematic if multiple users run phpstan on the same machine at the same time
43+
$lockFile = sys_get_temp_dir().'/staabm-phpstan-dba-cache.lock';
44+
$lockHandle = fopen($lockFile, 'w+');
45+
if (false === $lockHandle) {
46+
throw new DbaException(sprintf('Could not open lock file "%s".', $this->cacheFile));
47+
}
48+
49+
self::$lockHandle = $lockHandle;
50+
}
3451
}
3552

3653
public static function create(string $cacheFile): self
@@ -41,7 +58,7 @@ public static function create(string $cacheFile): self
4158
public static function load(string $cacheFile): self
4259
{
4360
$reflectionCache = new self($cacheFile);
44-
$cachedRecords = $reflectionCache->readCache();
61+
$cachedRecords = $reflectionCache->readCache(true);
4562
if (null !== $cachedRecords) {
4663
$reflectionCache->records = $cachedRecords;
4764
}
@@ -52,15 +69,24 @@ public static function load(string $cacheFile): self
5269
/**
5370
* @return array<string, array{error?: ?Error, result?: array<QueryReflector::FETCH_TYPE*, ?Type>}>|null
5471
*/
55-
private function readCache(): ?array
72+
private function readCache(bool $useReadLock): ?array
5673
{
5774
if (!is_file($this->cacheFile)) {
5875
if (false === file_put_contents($this->cacheFile, '')) {
5976
throw new DbaException(sprintf('Cache file "%s" is not readable and creating a new one did not succeed.', $this->cacheFile));
6077
}
6178
}
62-
clearstatcache(true, $this->cacheFile);
63-
$cache = require $this->cacheFile;
79+
80+
try {
81+
if ($useReadLock) {
82+
flock(self::$lockHandle, \LOCK_SH);
83+
}
84+
$cache = require $this->cacheFile;
85+
} finally {
86+
if ($useReadLock) {
87+
flock(self::$lockHandle, \LOCK_UN);
88+
}
89+
}
6490

6591
if (\is_array($cache) && \array_key_exists('schemaVersion', $cache) && self::SCHEMA_VERSION === $cache['schemaVersion']) {
6692
return $cache['records'];
@@ -75,47 +101,40 @@ public function persist(): void
75101
return;
76102
}
77103

78-
// prevent parallel phpstan-worker-process from writing into the cache file at the same time
79-
// XXX we use a single system-wide lock file, which might get problematic if multiple users run phpstan on the same machine at the same time
80-
$lockFile = sys_get_temp_dir().'/staabm-phpstan-dba-cache.lock';
81-
$lockHandle = fopen($lockFile, 'w+');
82-
if (false === $lockHandle) {
83-
throw new DbaException(sprintf('Could not open cache file "%s" for writing', $this->cacheFile));
84-
}
85-
flock($lockHandle, LOCK_EX);
104+
try {
105+
flock(self::$lockHandle, LOCK_EX);
86106

87-
// freshly read the cache as it might have changed in the meantime
88-
$cachedRecords = $this->readCache();
107+
// freshly read the cache as it might have changed in the meantime
108+
$cachedRecords = $this->readCache(false);
89109

90-
$handle = fopen($this->cacheFile, 'w+');
91-
if (false === $handle) {
92-
throw new DbaException(sprintf('Could not open cache file "%s" for writing', $this->cacheFile));
93-
}
110+
// re-apply all changes to the current cache-state
111+
if (null === $cachedRecords) {
112+
$newRecords = $this->changes;
113+
} else {
114+
$newRecords = array_merge($cachedRecords, $this->changes);
115+
}
94116

95-
// re-apply all changes to the current cache-state
96-
if (null === $cachedRecords) {
97-
$newRecords = $this->changes;
98-
} else {
99-
$newRecords = array_merge($cachedRecords, $this->changes);
100-
}
117+
// sort records to prevent unnecessary cache invalidation caused by different order of queries
118+
uksort($newRecords, function ($queryA, $queryB) {
119+
return $queryA <=> $queryB;
120+
});
101121

102-
// sort records to prevent unnecessary cache invalidation caused by different order of queries
103-
uksort($newRecords, function ($queryA, $queryB) {
104-
return $queryA <=> $queryB;
105-
});
122+
$cacheContent = '<?php return '.var_export([
123+
'schemaVersion' => self::SCHEMA_VERSION,
124+
'records' => $newRecords,
125+
], true).';';
106126

107-
$cacheContent = '<?php return '.var_export([
108-
'schemaVersion' => self::SCHEMA_VERSION,
109-
'records' => $newRecords,
110-
], true).';';
127+
if (false === file_put_contents($this->cacheFile, $cacheContent)) {
128+
throw new DbaException(sprintf('Unable to write cache file "%s"', $this->cacheFile));
129+
}
111130

112-
if (false === fwrite($handle, $cacheContent)) {
113-
throw new DbaException(sprintf('Unable to write cache file "%s"', $this->cacheFile));
131+
clearstatcache(true, $this->cacheFile);
132+
if (\function_exists('opcache_invalidate')) {
133+
opcache_invalidate($this->cacheFile, true);
134+
}
135+
} finally {
136+
flock(self::$lockHandle, \LOCK_UN);
114137
}
115-
116-
fclose($handle);
117-
// will free the lock implictly
118-
fclose($lockHandle);
119138
}
120139

121140
public function hasValidationError(string $queryString): bool

0 commit comments

Comments
 (0)