mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-20 04:20:55 +01:00
1ee426e4ac
Summary: Ref T11613. In D16503/T11598 I refined the setup flow to improve messaging for early-stage setup issues, but failed to fully untangle things. We sometimes still try to access a cache which uses configuration before we build configuration, which causes an error. Instead, store "are we in flight / has setup ever worked?" in a separate cache which doesn't use the cache namespace. This stops us from trying to read config before building config. Test Plan: Hit bad extension error with a fake extension, got a proper setup help page: {F1812803} Solved the error, reloaded, broke things again, got a "friendly" page: {F1812805} Reviewers: chad Reviewed By: chad Maniphest Tasks: T11613 Differential Revision: https://secure.phabricator.com/D16542
394 lines
10 KiB
PHP
394 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
*
|
|
* @task request Request Cache
|
|
* @task immutable Immutable Cache
|
|
* @task setup Setup Cache
|
|
* @task compress Compression
|
|
*/
|
|
final class PhabricatorCaches extends Phobject {
|
|
|
|
private static $requestCache;
|
|
|
|
public static function getNamespace() {
|
|
return PhabricatorEnv::getEnvConfig('phabricator.cache-namespace');
|
|
}
|
|
|
|
private static function newStackFromCaches(array $caches) {
|
|
$caches = self::addNamespaceToCaches($caches);
|
|
$caches = self::addProfilerToCaches($caches);
|
|
return id(new PhutilKeyValueCacheStack())
|
|
->setCaches($caches);
|
|
}
|
|
|
|
/* -( Request Cache )------------------------------------------------------ */
|
|
|
|
|
|
/**
|
|
* Get a request cache stack.
|
|
*
|
|
* This cache stack is destroyed after each logical request. In particular,
|
|
* it is destroyed periodically by the daemons, while `static` caches are
|
|
* not.
|
|
*
|
|
* @return PhutilKeyValueCacheStack Request cache stack.
|
|
*/
|
|
public static function getRequestCache() {
|
|
if (!self::$requestCache) {
|
|
self::$requestCache = new PhutilInRequestKeyValueCache();
|
|
}
|
|
return self::$requestCache;
|
|
}
|
|
|
|
|
|
/**
|
|
* Destroy the request cache.
|
|
*
|
|
* This is called at the beginning of each logical request.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function destroyRequestCache() {
|
|
self::$requestCache = null;
|
|
}
|
|
|
|
|
|
/* -( Immutable Cache )---------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Gets an immutable cache stack.
|
|
*
|
|
* This stack trades mutability away for improved performance. Normally, it is
|
|
* APC + DB.
|
|
*
|
|
* In the general case with multiple web frontends, this stack can not be
|
|
* cleared, so it is only appropriate for use if the value of a given key is
|
|
* permanent and immutable.
|
|
*
|
|
* @return PhutilKeyValueCacheStack Best immutable stack available.
|
|
* @task immutable
|
|
*/
|
|
public static function getImmutableCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildImmutableCaches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
|
|
/**
|
|
* Build the immutable cache stack.
|
|
*
|
|
* @return list<PhutilKeyValueCache> List of caches.
|
|
* @task immutable
|
|
*/
|
|
private static function buildImmutableCaches() {
|
|
$caches = array();
|
|
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
$caches[] = $apc;
|
|
}
|
|
|
|
$caches[] = new PhabricatorKeyValueDatabaseCache();
|
|
|
|
return $caches;
|
|
}
|
|
|
|
|
|
/* -( Repository Graph Cache )--------------------------------------------- */
|
|
|
|
|
|
public static function getRepositoryGraphL1Cache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildRepositoryGraphL1Caches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
private static function buildRepositoryGraphL1Caches() {
|
|
$caches = array();
|
|
|
|
$request = new PhutilInRequestKeyValueCache();
|
|
$request->setLimit(32);
|
|
$caches[] = $request;
|
|
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
$caches[] = $apc;
|
|
}
|
|
|
|
return $caches;
|
|
}
|
|
|
|
public static function getRepositoryGraphL2Cache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildRepositoryGraphL2Caches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
private static function buildRepositoryGraphL2Caches() {
|
|
$caches = array();
|
|
$caches[] = new PhabricatorKeyValueDatabaseCache();
|
|
return $caches;
|
|
}
|
|
|
|
|
|
/* -( Server State Cache )------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Highly specialized cache for storing server process state.
|
|
*
|
|
* We use this cache to track initial steps in the setup phase, before
|
|
* configuration is loaded.
|
|
*
|
|
* This cache does NOT use the cache namespace (it must be accessed before
|
|
* we build configuration), and is global across all instances on the host.
|
|
*
|
|
* @return PhutilKeyValueCacheStack Best available server state cache stack.
|
|
* @task setup
|
|
*/
|
|
public static function getServerStateCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildSetupCaches('phabricator-server');
|
|
|
|
// NOTE: We are NOT adding a cache namespace here! This cache is shared
|
|
// across all instances on the host.
|
|
|
|
$caches = self::addProfilerToCaches($caches);
|
|
$cache = id(new PhutilKeyValueCacheStack())
|
|
->setCaches($caches);
|
|
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
|
|
|
|
/* -( Setup Cache )-------------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Highly specialized cache for performing setup checks. We use this cache
|
|
* to determine if we need to run expensive setup checks when the page
|
|
* loads. Without it, we would need to run these checks every time.
|
|
*
|
|
* Normally, this cache is just APC. In the absence of APC, this cache
|
|
* degrades into a slow, quirky on-disk cache.
|
|
*
|
|
* NOTE: Do not use this cache for anything else! It is not a general-purpose
|
|
* cache!
|
|
*
|
|
* @return PhutilKeyValueCacheStack Most qualified available cache stack.
|
|
* @task setup
|
|
*/
|
|
public static function getSetupCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildSetupCaches('phabricator-setup');
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
|
|
/**
|
|
* @task setup
|
|
*/
|
|
private static function buildSetupCaches($cache_name) {
|
|
// If this is the CLI, just build a setup cache.
|
|
if (php_sapi_name() == 'cli') {
|
|
return array();
|
|
}
|
|
|
|
// In most cases, we should have APC. This is an ideal cache for our
|
|
// purposes -- it's fast and empties on server restart.
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
return array($apc);
|
|
}
|
|
|
|
// If we don't have APC, build a poor approximation on disk. This is still
|
|
// much better than nothing; some setup steps are quite slow.
|
|
$disk_path = self::getSetupCacheDiskCachePath($cache_name);
|
|
if ($disk_path) {
|
|
$disk = new PhutilOnDiskKeyValueCache();
|
|
$disk->setCacheFile($disk_path);
|
|
$disk->setWait(0.1);
|
|
if ($disk->isAvailable()) {
|
|
return array($disk);
|
|
}
|
|
}
|
|
|
|
return array();
|
|
}
|
|
|
|
|
|
/**
|
|
* @task setup
|
|
*/
|
|
private static function getSetupCacheDiskCachePath($name) {
|
|
// The difficulty here is in choosing a path which will change on server
|
|
// restart (we MUST have this property), but as rarely as possible
|
|
// otherwise (we desire this property to give the cache the best hit rate
|
|
// we can).
|
|
|
|
// Unfortunately, we don't have a very good strategy for minimizing the
|
|
// churn rate of the cache. We previously tried to use the parent process
|
|
// PID in some cases, but this was not reliable. See T9599 for one case of
|
|
// this.
|
|
|
|
$pid_basis = getmypid();
|
|
|
|
// If possible, we also want to know when the process launched, so we can
|
|
// drop the cache if a process restarts but gets the same PID an earlier
|
|
// process had. "/proc" is not available everywhere (e.g., not on OSX), but
|
|
// check if we have it.
|
|
$epoch_basis = null;
|
|
$stat = @stat("/proc/{$pid_basis}");
|
|
if ($stat !== false) {
|
|
$epoch_basis = $stat['ctime'];
|
|
}
|
|
|
|
$tmp_dir = sys_get_temp_dir();
|
|
|
|
$tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.$name;
|
|
if (!file_exists($tmp_path)) {
|
|
@mkdir($tmp_path);
|
|
}
|
|
|
|
$is_ok = self::testTemporaryDirectory($tmp_path);
|
|
if (!$is_ok) {
|
|
$tmp_path = $tmp_dir;
|
|
$is_ok = self::testTemporaryDirectory($tmp_path);
|
|
if (!$is_ok) {
|
|
// We can't find anywhere to write the cache, so just bail.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
$tmp_name = 'setup-'.$pid_basis;
|
|
if ($epoch_basis) {
|
|
$tmp_name .= '.'.$epoch_basis;
|
|
}
|
|
$tmp_name .= '.cache';
|
|
|
|
return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name;
|
|
}
|
|
|
|
|
|
/**
|
|
* @task setup
|
|
*/
|
|
private static function testTemporaryDirectory($dir) {
|
|
if (!@file_exists($dir)) {
|
|
return false;
|
|
}
|
|
if (!@is_dir($dir)) {
|
|
return false;
|
|
}
|
|
if (!@is_writable($dir)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static function addProfilerToCaches(array $caches) {
|
|
foreach ($caches as $key => $cache) {
|
|
$pcache = new PhutilKeyValueCacheProfiler($cache);
|
|
$pcache->setProfiler(PhutilServiceProfiler::getInstance());
|
|
$caches[$key] = $pcache;
|
|
}
|
|
return $caches;
|
|
}
|
|
|
|
private static function addNamespaceToCaches(array $caches) {
|
|
$namespace = self::getNamespace();
|
|
if (!$namespace) {
|
|
return $caches;
|
|
}
|
|
|
|
foreach ($caches as $key => $cache) {
|
|
$ncache = new PhutilKeyValueCacheNamespace($cache);
|
|
$ncache->setNamespace($namespace);
|
|
$caches[$key] = $ncache;
|
|
}
|
|
|
|
return $caches;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deflate a value, if deflation is available and has an impact.
|
|
*
|
|
* If the value is larger than 1KB, we have `gzdeflate()`, we successfully
|
|
* can deflate it, and it benefits from deflation, we deflate it. Otherwise
|
|
* we leave it as-is.
|
|
*
|
|
* Data can later be inflated with @{method:inflateData}.
|
|
*
|
|
* @param string String to attempt to deflate.
|
|
* @return string|null Deflated string, or null if it was not deflated.
|
|
* @task compress
|
|
*/
|
|
public static function maybeDeflateData($value) {
|
|
$len = strlen($value);
|
|
if ($len <= 1024) {
|
|
return null;
|
|
}
|
|
|
|
if (!function_exists('gzdeflate')) {
|
|
return null;
|
|
}
|
|
|
|
$deflated = gzdeflate($value);
|
|
if ($deflated === false) {
|
|
return null;
|
|
}
|
|
|
|
$deflated_len = strlen($deflated);
|
|
if ($deflated_len >= ($len / 2)) {
|
|
return null;
|
|
}
|
|
|
|
return $deflated;
|
|
}
|
|
|
|
|
|
/**
|
|
* Inflate data previously deflated by @{method:maybeDeflateData}.
|
|
*
|
|
* @param string Deflated data, from @{method:maybeDeflateData}.
|
|
* @return string Original, uncompressed data.
|
|
* @task compress
|
|
*/
|
|
public static function inflateData($value) {
|
|
if (!function_exists('gzinflate')) {
|
|
throw new Exception(
|
|
pht(
|
|
'%s is not available; unable to read deflated data!',
|
|
'gzinflate()'));
|
|
}
|
|
|
|
$value = gzinflate($value);
|
|
if ($value === false) {
|
|
throw new Exception(pht('Failed to inflate data!'));
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
|
|
}
|