@@ -411,37 +411,55 @@ public function setTempDirectory(string $dir): self
411411 private function loadCache (): void
412412 {
413413 $ file = $ this ->getCacheFile ();
414+
415+ // Solving atomicity to work everywhere is really pain in the ass.
416+ // 1) We want to do as little as possible IO calls on production and also directory and file can be not writable (#19)
417+ // so on Linux we include the file directly without shared lock, therefore, the file must be created atomically by renaming.
418+ // 2) On Windows file cannot be renamed-to while is open (ie by include() #11), so we have to acquire a lock.
419+ $ lock = defined ('PHP_WINDOWS_VERSION_BUILD ' )
420+ ? $ this ->acquireLock ("$ file.lock " , LOCK_SH )
421+ : null ;
422+
414423 $ data = @include $ file ; // @ file may not exist
415424 if (is_array ($ data )) {
416425 [$ this ->classes , $ this ->missing ] = $ data ;
417426 return ;
418427 }
419428
429+ if ($ lock ) {
430+ flock ($ lock , LOCK_UN ); // release shared lock so we can get exclusive
431+ }
420432 $ lock = $ this ->acquireLock ("$ file.lock " , LOCK_EX );
421433
434+ // while waiting for exclusive lock, someone might have already created the cache
422435 $ data = @include $ file ; // @ file may not exist
423436 if (is_array ($ data )) {
424437 [$ this ->classes , $ this ->missing ] = $ data ;
425- } else {
426- $ this ->rebuild ();
438+ return ;
427439 }
428440
429- flock ($ lock , LOCK_UN );
430- fclose ($ lock );
431- @unlink ("$ file.lock " ); // @ file may become locked on Windows
441+ $ this ->classes = $ this ->missing = [];
442+ $ this ->refreshClasses ();
443+ $ this ->saveCache ($ lock );
444+ // On Windows concurrent creation and deletion of a file can cause a error 'permission denied',
445+ // therefore, we will not delete the lock file. Windows is peace of shit.
432446 }
433447
434448
435449 /**
436450 * Writes class list to cache.
437451 */
438- private function saveCache (): void
452+ private function saveCache ($ lock = null ): void
439453 {
454+ // we have to acquire a lock to be able safely rename file
455+ // on Linux: that another thread does not rename the same named file earlier
456+ // on Windows: that the file is not read by another thread
440457 $ file = $ this ->getCacheFile ();
441- $ tempFile = $ file . uniqid ( '' , true ) . ' .tmp ' ;
458+ $ lock = $ lock ?: $ this -> acquireLock ( " $ file .lock " , LOCK_EX ) ;
442459 $ code = "<?php \nreturn " . var_export ([$ this ->classes , $ this ->missing ], true ) . "; \n" ;
443- if (file_put_contents ($ tempFile , $ code ) !== strlen ($ code ) || !rename ($ tempFile , $ file )) {
444- @unlink ($ tempFile ); // @ - file may not exist
460+
461+ if (file_put_contents ("$ file.tmp " , $ code ) !== strlen ($ code ) || !rename ("$ file.tmp " , $ file )) {
462+ @unlink ("$ file.tmp " ); // @ file may not exist
445463 throw new \RuntimeException ("Unable to create ' $ file'. " );
446464 }
447465 if (function_exists ('opcache_invalidate ' )) {
@@ -452,7 +470,7 @@ private function saveCache(): void
452470
453471 private function acquireLock (string $ file , int $ mode )
454472 {
455- $ handle = @fopen ($ file , 'cb+ ' ); // @ is escalated to exception
473+ $ handle = @fopen ($ file , 'w ' ); // @ is escalated to exception
456474 if (!$ handle ) {
457475 throw new \RuntimeException ("Unable to create file ' $ file'. " . error_get_last ()['message ' ]);
458476 } elseif (!@flock ($ handle , $ mode )) { // @ is escalated to exception
0 commit comments