@@ -52,6 +52,9 @@ private static function hasApcu(): bool
5252 /**
5353 * Get a value from cache or execute callback to generate it
5454 *
55+ * Uses mutex locking to prevent cache stampede (thundering herd problem).
56+ * Only one process computes the value while others wait.
57+ *
5558 * @param string $key Unique cache key
5659 * @param callable $callback Function to generate value if not cached
5760 * @param int $ttl Time to live in seconds (default: 300 = 5 minutes)
@@ -65,13 +68,46 @@ public static function remember(string $key, callable $callback, int $ttl = 300)
6568 return $ cached ;
6669 }
6770
68- // Execute callback to get fresh value
69- $ value = $ callback ();
71+ // Acquire mutex lock to prevent stampede
72+ $ lockKey = self ::hashKey ($ key ) . '.lock ' ;
73+ $ lockFile = self ::getCacheDir () . '/ ' . $ lockKey ;
74+ $ lockHandle = @fopen ($ lockFile , 'c ' );
75+
76+ if ($ lockHandle === false ) {
77+ // If we can't get a lock, just execute callback (graceful degradation)
78+ return $ callback ();
79+ }
80+
81+ try {
82+ // Try to acquire exclusive lock (non-blocking first)
83+ if (!flock ($ lockHandle , LOCK_EX | LOCK_NB )) {
84+ // Another process is computing, wait for it and try cache again
85+ flock ($ lockHandle , LOCK_EX );
86+ $ cached = self ::get ($ key );
87+ if ($ cached !== null ) {
88+ return $ cached ;
89+ }
90+ }
91+
92+ // Double-check cache after acquiring lock
93+ $ cached = self ::get ($ key );
94+ if ($ cached !== null ) {
95+ return $ cached ;
96+ }
97+
98+ // Execute callback to get fresh value
99+ $ value = $ callback ();
70100
71- // Store in cache
72- self ::set ($ key , $ value , $ ttl );
101+ // Store in cache
102+ self ::set ($ key , $ value , $ ttl );
73103
74- return $ value ;
104+ return $ value ;
105+ } finally {
106+ flock ($ lockHandle , LOCK_UN );
107+ fclose ($ lockHandle );
108+ // Clean up lock file (best effort)
109+ @unlink ($ lockFile );
110+ }
75111 }
76112
77113 /**
@@ -181,15 +217,25 @@ public static function clearByPrefix(string $prefix): int
181217 }
182218
183219 /**
184- * Clear all cache entries
220+ * Clear all Pinakes cache entries
221+ *
222+ * Only clears entries with the 'pinakes_' prefix to avoid clearing
223+ * other applications' cache entries that may share the same APCu instance.
185224 *
186225 * @return bool Success status
187226 */
188227 public static function flush (): bool
189228 {
190- // Try APCu first
229+ // Try APCu first - only clear pinakes_* keys, not the entire cache
191230 if (self ::hasApcu ()) {
192- return apcu_clear_cache ();
231+ $ success = true ;
232+ $ iterator = new \APCUIterator ('/^pinakes_/ ' );
233+ foreach ($ iterator as $ item ) {
234+ if (!apcu_delete ($ item ['key ' ])) {
235+ $ success = false ;
236+ }
237+ }
238+ return $ success ;
193239 }
194240
195241 // Fallback to file cache - delete all files
@@ -223,6 +269,9 @@ private static function hashKey(string $key): string
223269
224270 /**
225271 * Get value from file cache
272+ *
273+ * Uses file locking (flock) to prevent reading incomplete/corrupted data
274+ * and safe unserialize to prevent object injection attacks.
226275 */
227276 private static function getFromFile (string $ hashedKey ): mixed
228277 {
@@ -232,24 +281,45 @@ private static function getFromFile(string $hashedKey): mixed
232281 return null ;
233282 }
234283
235- $ content = @file_get_contents ($ path );
236- if ($ content === false ) {
284+ // Open file with shared lock for reading
285+ $ handle = @fopen ($ path , 'r ' );
286+ if ($ handle === false ) {
237287 return null ;
238288 }
239289
240- $ data = @ unserialize ( $ content );
241- if ( $ data === false || ! is_array ( $ data )) {
242- @ unlink ( $ path );
243- return null ;
244- }
290+ try {
291+ // Acquire shared lock for reading
292+ if (! flock ( $ handle , LOCK_SH )) {
293+ return null ;
294+ }
245295
246- // Check expiration
247- if (isset ($ data ['expires ' ]) && $ data ['expires ' ] < time ()) {
248- @unlink ($ path );
249- return null ;
250- }
296+ $ content = stream_get_contents ($ handle );
297+ if ($ content === false || $ content === '' ) {
298+ return null ;
299+ }
300+
301+ // Use safe unserialize to prevent object injection attacks
302+ $ data = @unserialize ($ content , ['allowed_classes ' => false ]);
303+ if ($ data === false || !\is_array ($ data )) {
304+ flock ($ handle , LOCK_UN );
305+ fclose ($ handle );
306+ @unlink ($ path );
307+ return null ;
308+ }
251309
252- return $ data ['value ' ] ?? null ;
310+ // Check expiration
311+ if (isset ($ data ['expires ' ]) && $ data ['expires ' ] < time ()) {
312+ flock ($ handle , LOCK_UN );
313+ fclose ($ handle );
314+ @unlink ($ path );
315+ return null ;
316+ }
317+
318+ return $ data ['value ' ] ?? null ;
319+ } finally {
320+ flock ($ handle , LOCK_UN );
321+ fclose ($ handle );
322+ }
253323 }
254324
255325 /**
0 commit comments