Skip to content

Commit 6a0cc27

Browse files
staabmclxmstaab
andauthored
reduce the risk of possible concurrency issues in ReflectionCache (#97)
Co-authored-by: Markus Staab <[email protected]>
1 parent 5613361 commit 6a0cc27

File tree

1 file changed

+56
-33
lines changed

1 file changed

+56
-33
lines changed

src/QueryReflection/ReflectionCache.php

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,13 @@ final class ReflectionCache
2424
private $records = [];
2525

2626
/**
27-
* @var bool
27+
* @var array<string, array{error?: ?Error, result?: array<QueryReflector::FETCH_TYPE*, ?Type>}>
2828
*/
29-
private $changed = false;
29+
private $changes = [];
3030

3131
private function __construct(string $cacheFile)
3232
{
3333
$this->cacheFile = $cacheFile;
34-
35-
if (!is_file($cacheFile)) {
36-
// enforce file creation
37-
$this->changed = true;
38-
}
3934
}
4035

4136
public static function create(string $cacheFile): self
@@ -49,35 +44,69 @@ public static function load(string $cacheFile): self
4944
throw new DbaException(sprintf('Cache file "%s" is not readable', $cacheFile));
5045
}
5146

52-
$cache = require $cacheFile;
53-
5447
$reflectionCache = new self($cacheFile);
55-
if (\is_array($cache) && \array_key_exists('schemaVersion', $cache) && self::SCHEMA_VERSION === $cache['schemaVersion']) {
56-
$reflectionCache->records = $cache['records'];
48+
$cachedRecords = $reflectionCache->readCache();
49+
if (null !== $cachedRecords) {
50+
$reflectionCache->records = $cachedRecords;
5751
}
5852

5953
return $reflectionCache;
6054
}
6155

56+
/**
57+
* @return array<string, array{error?: ?Error, result?: array<QueryReflector::FETCH_TYPE*, ?Type>}>|null
58+
*/
59+
private function readCache(): ?array
60+
{
61+
clearstatcache(true, $this->cacheFile);
62+
$cache = require $this->cacheFile;
63+
64+
if (\is_array($cache) && \array_key_exists('schemaVersion', $cache) && self::SCHEMA_VERSION === $cache['schemaVersion']) {
65+
return $cache['records'];
66+
}
67+
68+
return null;
69+
}
70+
6271
public function persist(): void
6372
{
64-
if (!$this->changed) {
73+
if (0 === \count($this->changes)) {
6574
return;
6675
}
6776

68-
$records = $this->records;
77+
// freshly read the cache as it might have changed in the meantime
78+
$cachedRecords = $this->readCache();
79+
80+
// actually we should lock even earlier, but we could no longer read the cache-file with require()
81+
$handle = fopen($this->cacheFile, 'w+');
82+
if (false === $handle) {
83+
throw new DbaException(sprintf('Could not open cache file "%s" for writing', $this->cacheFile));
84+
}
85+
flock($handle, LOCK_EX);
86+
87+
// re-apply all changes to the current cache-state
88+
if (null === $cachedRecords) {
89+
$newRecords = $this->changes;
90+
} else {
91+
$newRecords = array_merge($cachedRecords, $this->changes);
92+
}
6993

7094
// sort records to prevent unnecessary cache invalidation caused by different order of queries
71-
uksort($records, function ($queryA, $queryB) {
95+
uksort($newRecords, function ($queryA, $queryB) {
7296
return $queryA <=> $queryB;
7397
});
7498

75-
if (false === file_put_contents($this->cacheFile, '<?php return '.var_export([
76-
'schemaVersion' => self::SCHEMA_VERSION,
77-
'records' => $records,
78-
], true).';', LOCK_EX)) {
99+
$cacheContent = '<?php return '.var_export([
100+
'schemaVersion' => self::SCHEMA_VERSION,
101+
'records' => $newRecords,
102+
], true).';';
103+
104+
if (false === fwrite($handle, $cacheContent)) {
79105
throw new DbaException(sprintf('Unable to write cache file "%s"', $this->cacheFile));
80106
}
107+
108+
// frees the lock implictly
109+
fclose($handle);
81110
}
82111

83112
public function hasValidationError(string $queryString): bool
@@ -108,14 +137,11 @@ public function getValidationError(string $queryString): ?Error
108137
public function putValidationError(string $queryString, ?Error $error): void
109138
{
110139
if (!\array_key_exists($queryString, $this->records)) {
111-
$this->records[$queryString] = [];
112-
$this->changed = true;
140+
$this->changes[$queryString] = $this->records[$queryString] = [];
113141
}
114142

115-
$cacheEntry = &$this->records[$queryString];
116-
if (!\array_key_exists('error', $cacheEntry) || $cacheEntry['error'] !== $error) {
117-
$cacheEntry['error'] = $error;
118-
$this->changed = true;
143+
if (!\array_key_exists('error', $this->records[$queryString]) || $this->records[$queryString]['error'] !== $error) {
144+
$this->changes[$queryString]['error'] = $this->records[$queryString]['error'] = $error;
119145
}
120146
}
121147

@@ -163,19 +189,16 @@ public function getResultType(string $queryString, int $fetchType): ?Type
163189
public function putResultType(string $queryString, int $fetchType, ?Type $resultType): void
164190
{
165191
if (!\array_key_exists($queryString, $this->records)) {
166-
$this->records[$queryString] = [];
167-
$this->changed = true;
192+
$this->changes[$queryString] = $this->records[$queryString] = [];
168193
}
169194

170-
$cacheEntry = &$this->records[$queryString];
171-
if (!\array_key_exists('result', $cacheEntry)) {
172-
$cacheEntry['result'] = [];
173-
$this->changed = true;
195+
if (!\array_key_exists('result', $this->records[$queryString])) {
196+
$this->changes[$queryString]['result'] = $this->records[$queryString]['result'] = [];
174197
}
175198

176-
if (!\array_key_exists($fetchType, $cacheEntry['result']) || $cacheEntry['result'][$fetchType] !== $resultType) {
177-
$cacheEntry['result'][$fetchType] = $resultType;
178-
$this->changed = true;
199+
// @phpstan-ignore-next-line
200+
if (!\array_key_exists($fetchType, $this->records[$queryString]['result']) || $this->records[$queryString]['result'][$fetchType] !== $resultType) {
201+
$this->changes[$queryString]['result'][$fetchType] = $this->records[$queryString]['result'][$fetchType] = $resultType;
179202
}
180203
}
181204
}

0 commit comments

Comments
 (0)