diff options
Diffstat (limited to 'vendor/symfony/cache/LockRegistry.php')
-rw-r--r-- | vendor/symfony/cache/LockRegistry.php | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/vendor/symfony/cache/LockRegistry.php b/vendor/symfony/cache/LockRegistry.php new file mode 100644 index 0000000..c5c5fde --- /dev/null +++ b/vendor/symfony/cache/LockRegistry.php | |||
@@ -0,0 +1,166 @@ | |||
1 | <?php | ||
2 | |||
3 | /* | ||
4 | * This file is part of the Symfony package. | ||
5 | * | ||
6 | * (c) Fabien Potencier <fabien@symfony.com> | ||
7 | * | ||
8 | * For the full copyright and license information, please view the LICENSE | ||
9 | * file that was distributed with this source code. | ||
10 | */ | ||
11 | |||
12 | namespace Symfony\Component\Cache; | ||
13 | |||
14 | use Psr\Log\LoggerInterface; | ||
15 | use Symfony\Contracts\Cache\CacheInterface; | ||
16 | use Symfony\Contracts\Cache\ItemInterface; | ||
17 | |||
18 | /** | ||
19 | * LockRegistry is used internally by existing adapters to protect against cache stampede. | ||
20 | * | ||
21 | * It does so by wrapping the computation of items in a pool of locks. | ||
22 | * Foreach each apps, there can be at most 20 concurrent processes that | ||
23 | * compute items at the same time and only one per cache-key. | ||
24 | * | ||
25 | * @author Nicolas Grekas <p@tchwork.com> | ||
26 | */ | ||
27 | final class LockRegistry | ||
28 | { | ||
29 | private static array $openedFiles = []; | ||
30 | private static ?array $lockedFiles = null; | ||
31 | private static \Exception $signalingException; | ||
32 | private static \Closure $signalingCallback; | ||
33 | |||
34 | /** | ||
35 | * The number of items in this list controls the max number of concurrent processes. | ||
36 | */ | ||
37 | private static array $files = [ | ||
38 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractAdapter.php', | ||
39 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractTagAwareAdapter.php', | ||
40 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AdapterInterface.php', | ||
41 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php', | ||
42 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php', | ||
43 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php', | ||
44 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseBucketAdapter.php', | ||
45 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseCollectionAdapter.php', | ||
46 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineDbalAdapter.php', | ||
47 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php', | ||
48 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php', | ||
49 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'MemcachedAdapter.php', | ||
50 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'NullAdapter.php', | ||
51 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ParameterNormalizer.php', | ||
52 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PdoAdapter.php', | ||
53 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpArrayAdapter.php', | ||
54 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpFilesAdapter.php', | ||
55 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ProxyAdapter.php', | ||
56 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'Psr16Adapter.php', | ||
57 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisAdapter.php', | ||
58 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisTagAwareAdapter.php', | ||
59 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapter.php', | ||
60 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapterInterface.php', | ||
61 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableAdapter.php', | ||
62 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableTagAwareAdapter.php', | ||
63 | ]; | ||
64 | |||
65 | /** | ||
66 | * Defines a set of existing files that will be used as keys to acquire locks. | ||
67 | * | ||
68 | * @return array The previously defined set of files | ||
69 | */ | ||
70 | public static function setFiles(array $files): array | ||
71 | { | ||
72 | $previousFiles = self::$files; | ||
73 | self::$files = $files; | ||
74 | |||
75 | foreach (self::$openedFiles as $file) { | ||
76 | if ($file) { | ||
77 | flock($file, \LOCK_UN); | ||
78 | fclose($file); | ||
79 | } | ||
80 | } | ||
81 | self::$openedFiles = self::$lockedFiles = []; | ||
82 | |||
83 | return $previousFiles; | ||
84 | } | ||
85 | |||
86 | public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, ?\Closure $setMetadata = null, ?LoggerInterface $logger = null): mixed | ||
87 | { | ||
88 | if ('\\' === \DIRECTORY_SEPARATOR && null === self::$lockedFiles) { | ||
89 | // disable locking on Windows by default | ||
90 | self::$files = self::$lockedFiles = []; | ||
91 | } | ||
92 | |||
93 | $key = self::$files ? abs(crc32($item->getKey())) % \count(self::$files) : -1; | ||
94 | |||
95 | if ($key < 0 || self::$lockedFiles || !$lock = self::open($key)) { | ||
96 | return $callback($item, $save); | ||
97 | } | ||
98 | |||
99 | self::$signalingException ??= unserialize("O:9:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}"); | ||
100 | self::$signalingCallback ??= fn () => throw self::$signalingException; | ||
101 | |||
102 | while (true) { | ||
103 | try { | ||
104 | // race to get the lock in non-blocking mode | ||
105 | $locked = flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock); | ||
106 | |||
107 | if ($locked || !$wouldBlock) { | ||
108 | $logger?->info(sprintf('Lock %s, now computing item "{key}"', $locked ? 'acquired' : 'not supported'), ['key' => $item->getKey()]); | ||
109 | self::$lockedFiles[$key] = true; | ||
110 | |||
111 | $value = $callback($item, $save); | ||
112 | |||
113 | if ($save) { | ||
114 | if ($setMetadata) { | ||
115 | $setMetadata($item); | ||
116 | } | ||
117 | |||
118 | $pool->save($item->set($value)); | ||
119 | $save = false; | ||
120 | } | ||
121 | |||
122 | return $value; | ||
123 | } | ||
124 | // if we failed the race, retry locking in blocking mode to wait for the winner | ||
125 | $logger?->info('Item "{key}" is locked, waiting for it to be released', ['key' => $item->getKey()]); | ||
126 | flock($lock, \LOCK_SH); | ||
127 | } finally { | ||
128 | flock($lock, \LOCK_UN); | ||
129 | unset(self::$lockedFiles[$key]); | ||
130 | } | ||
131 | |||
132 | try { | ||
133 | $value = $pool->get($item->getKey(), self::$signalingCallback, 0); | ||
134 | $logger?->info('Item "{key}" retrieved after lock was released', ['key' => $item->getKey()]); | ||
135 | $save = false; | ||
136 | |||
137 | return $value; | ||
138 | } catch (\Exception $e) { | ||
139 | if (self::$signalingException !== $e) { | ||
140 | throw $e; | ||
141 | } | ||
142 | $logger?->info('Item "{key}" not found while lock was released, now retrying', ['key' => $item->getKey()]); | ||
143 | } | ||
144 | } | ||
145 | |||
146 | return null; | ||
147 | } | ||
148 | |||
149 | /** | ||
150 | * @return resource|false | ||
151 | */ | ||
152 | private static function open(int $key) | ||
153 | { | ||
154 | if (null !== $h = self::$openedFiles[$key] ?? null) { | ||
155 | return $h; | ||
156 | } | ||
157 | set_error_handler(static fn () => null); | ||
158 | try { | ||
159 | $h = fopen(self::$files[$key], 'r+'); | ||
160 | } finally { | ||
161 | restore_error_handler(); | ||
162 | } | ||
163 | |||
164 | return self::$openedFiles[$key] = $h ?: @fopen(self::$files[$key], 'r'); | ||
165 | } | ||
166 | } | ||