Fixes CodeParser concurrent access errors

Big thanks to @shina, @BearishSun and @MarcoROG for their help with finding a solution
Fixes #1250
This commit is contained in:
Samuel Georges 2016-08-27 12:45:30 +10:00
parent 749e4c031c
commit 1e0741e407
1 changed files with 129 additions and 42 deletions

View File

@ -1,6 +1,5 @@
<?php namespace Cms\Classes;
use File;
use Lang;
use Cache;
use Config;
@ -63,28 +62,65 @@ class CodeParser
}
/*
* Try to load the parsed data from the file cache
* Try to load the parsed data from the cache
*/
$path = $this->getFilePath();
$path = $this->getCacheFilePath();
$result = [
'filePath' => $path,
'className' => null,
'source' => null,
'offset' => 0
];
if (File::isFile($path)) {
/*
* There are two types of possible caching scenarios, either stored
* in the cache itself, or stored as a cache file. In both cases,
* make sure the cache is not stale and use it.
*/
if (is_file($path)) {
$cachedInfo = $this->getCachedFileInfo();
if ($cachedInfo !== null && $cachedInfo['mtime'] == $this->object->mtime) {
$hasCache = $cachedInfo !== null;
/*
* Valid cache, return result
*/
if ($hasCache && $cachedInfo['mtime'] == $this->object->mtime) {
$result['className'] = $cachedInfo['className'];
$result['source'] = 'cache';
return self::$cache[$this->filePath] = $result;
}
/*
* Cache expired, cache file not stale, refresh cache and return result
*/
if (!$hasCache && filemtime($path) >= $this->object->mtime) {
$className = $this->extractClassFromFile($path);
if ($className) {
$result['className'] = $className;
$result['source'] = 'file-cache';
$this->storeCachedInfo($result);
return $result;
}
}
}
/*
* If the file was not found, or the cache is stale, prepare the new file and cache information about it
*/
$uniqueName = uniqid().'_'.abs(crc32(md5(mt_rand())));
$result['className'] = $this->rebuild($path);
$result['source'] = 'parser';
$this->storeCachedInfo($result);
return $result;
}
/**
* Rebuilds the current file cache.
* @param string The path in which the cached file should be stored
*/
protected function rebuild($path)
{
$uniqueName = str_replace('.', '', uniqid('', true)).'_'.abs(crc32(mt_rand()));
$className = 'Cms'.$uniqueName.'Class';
$body = $this->object->code;
@ -113,30 +149,13 @@ class CodeParser
$this->validate($fileContents);
$dir = dirname($path);
if (!File::isDirectory($dir) && !@File::makeDirectory($dir, 0777, true)) {
throw new SystemException(Lang::get('system::lang.directory.create_fail', ['name'=>$dir]));
$this->makeDirectoryForPath($path);
if (!@file_put_contents($path, $fileContents, LOCK_EX)) {
throw new SystemException(Lang::get('system::lang.file.create_fail', ['name'=>$path]));
}
if (!@File::put($path, $fileContents)) {
throw new SystemException(Lang::get('system::lang.file.create_fail', ['name'=>$dir]));
}
$cached = $this->getCachedInfo();
if (!$cached) {
$cached = [];
}
$result['className'] = $className;
$result['source'] = 'parser';
$cacheItem = $result;
$cacheItem['mtime'] = $this->object->mtime;
$cached[$this->filePath] = $cacheItem;
Cache::put($this->dataCacheKey, base64_encode(serialize($cached)), 1440);
return self::$cache[$this->filePath] = $result;
return $className;
}
/**
@ -155,7 +174,7 @@ class CodeParser
require_once $data['filePath'];
}
if (!class_exists($className) && ($data = $this->handleCorruptCache())) {
if (!class_exists($className) && ($data = $this->handleCorruptCache($data))) {
$className = $data['className'];
}
@ -168,12 +187,17 @@ class CodeParser
* flush the request cache, and repeat the cycle.
* @return void
*/
protected function handleCorruptCache()
protected function handleCorruptCache($data)
{
$path = $this->getFilePath();
$path = array_get($data, 'filePath', $this->getCacheFilePath());
if (File::isFile($path)) {
File::delete($path);
if (is_file($path)) {
if ($className = $this->extractClassFromFile($path)) {
$data['className'] = $className;
return $data;
}
@unlink($path);
}
unset(self::$cache[$this->filePath]);
@ -181,26 +205,40 @@ class CodeParser
return $this->parse();
}
//
// Cache
//
/**
* Evaluates PHP content in order to detect syntax errors.
* The method handles PHP errors and throws exceptions.
* Stores result data inside cache.
* @param array $result
* @return void
*/
protected function validate($php)
protected function storeCachedInfo($result)
{
eval('?>'.$php);
$cacheItem = $result;
$cacheItem['mtime'] = $this->object->mtime;
$cached = $this->getCachedInfo() ?: [];
$cached[$this->filePath] = $cacheItem;
Cache::put($this->dataCacheKey, base64_encode(serialize($cached)), 1440);
self::$cache[$this->filePath] = $result;
}
/**
* Returns path to the cached parsed file
* @return string
*/
protected function getFilePath()
protected function getCacheFilePath()
{
$hash = abs(crc32($this->filePath));
$result = storage_path().'/cms/cache/';
$result .= substr($hash, 0, 2).'/';
$result .= substr($hash, 2, 2).'/';
$result .= basename($this->filePath).'.php';
$result .= basename($this->filePath);
$result .= '.php';
return $result;
}
@ -239,4 +277,53 @@ class CodeParser
return null;
}
//
// Helpers
//
/**
* Evaluates PHP content in order to detect syntax errors.
* The method handles PHP errors and throws exceptions.
*/
protected function validate($php)
{
eval('?>'.$php);
}
/**
* Extracts the class name from a cache file
* @return string
*/
protected function extractClassFromFile($path)
{
$fileContent = file_get_contents($path);
$matches = [];
$pattern = '/Cms\S+_\S+Class/';
preg_match($pattern, $fileContent, $matches);
if (!empty($matches[0])) {
return $matches[0];
}
return null;
}
/**
* Make directory with concurrency support
*/
protected function makeDirectoryForPath($path)
{
$count = 0;
$dir = dirname($path);
while (!is_dir($dir) && !@mkdir($dir, 0777, true)) {
usleep(rand(50000, 200000));
if ($count++ > 10) {
throw new SystemException(Lang::get('system::lang.directory.create_fail', ['name'=>$dir]));
}
}
}
}