Path: blob/master/src/applications/cache/PhabricatorCaches.php
12242 views
<?php12/**3*4* @task request Request Cache5* @task immutable Immutable Cache6* @task setup Setup Cache7* @task compress Compression8*/9final class PhabricatorCaches extends Phobject {1011private static $requestCache;1213public static function getNamespace() {14return PhabricatorEnv::getEnvConfig('phabricator.cache-namespace');15}1617private static function newStackFromCaches(array $caches) {18$caches = self::addNamespaceToCaches($caches);19$caches = self::addProfilerToCaches($caches);20return id(new PhutilKeyValueCacheStack())21->setCaches($caches);22}2324/* -( Request Cache )------------------------------------------------------ */252627/**28* Get a request cache stack.29*30* This cache stack is destroyed after each logical request. In particular,31* it is destroyed periodically by the daemons, while `static` caches are32* not.33*34* @return PhutilKeyValueCacheStack Request cache stack.35*/36public static function getRequestCache() {37if (!self::$requestCache) {38self::$requestCache = new PhutilInRequestKeyValueCache();39}40return self::$requestCache;41}424344/**45* Destroy the request cache.46*47* This is called at the beginning of each logical request.48*49* @return void50*/51public static function destroyRequestCache() {52self::$requestCache = null;5354// See T12997. Force the GC to run when the request cache is destroyed to55// clean up any cycles which may still be hanging around.56if (function_exists('gc_collect_cycles')) {57gc_collect_cycles();58}59}606162/* -( Immutable Cache )---------------------------------------------------- */636465/**66* Gets an immutable cache stack.67*68* This stack trades mutability away for improved performance. Normally, it is69* APC + DB.70*71* In the general case with multiple web frontends, this stack can not be72* cleared, so it is only appropriate for use if the value of a given key is73* permanent and immutable.74*75* @return PhutilKeyValueCacheStack Best immutable stack available.76* @task immutable77*/78public static function getImmutableCache() {79static $cache;80if (!$cache) {81$caches = self::buildImmutableCaches();82$cache = self::newStackFromCaches($caches);83}84return $cache;85}868788/**89* Build the immutable cache stack.90*91* @return list<PhutilKeyValueCache> List of caches.92* @task immutable93*/94private static function buildImmutableCaches() {95$caches = array();9697$apc = new PhutilAPCKeyValueCache();98if ($apc->isAvailable()) {99$caches[] = $apc;100}101102$caches[] = new PhabricatorKeyValueDatabaseCache();103104return $caches;105}106107public static function getMutableCache() {108static $cache;109if (!$cache) {110$caches = self::buildMutableCaches();111$cache = self::newStackFromCaches($caches);112}113return $cache;114}115116private static function buildMutableCaches() {117$caches = array();118119$caches[] = new PhabricatorKeyValueDatabaseCache();120121return $caches;122}123124public static function getMutableStructureCache() {125static $cache;126if (!$cache) {127$caches = self::buildMutableStructureCaches();128$cache = self::newStackFromCaches($caches);129}130return $cache;131}132133private static function buildMutableStructureCaches() {134$caches = array();135136$cache = new PhabricatorKeyValueDatabaseCache();137$cache = new PhabricatorKeyValueSerializingCacheProxy($cache);138$caches[] = $cache;139140return $caches;141}142143/* -( Runtime Cache )------------------------------------------------------ */144145146/**147* Get a runtime cache stack.148*149* This stack is just APC. It's fast, it's effectively immutable, and it150* gets thrown away when the webserver restarts.151*152* This cache is suitable for deriving runtime caches, like a map of Conduit153* method names to provider classes.154*155* @return PhutilKeyValueCacheStack Best runtime stack available.156*/157public static function getRuntimeCache() {158static $cache;159if (!$cache) {160$caches = self::buildRuntimeCaches();161$cache = self::newStackFromCaches($caches);162}163return $cache;164}165166167private static function buildRuntimeCaches() {168$caches = array();169170$apc = new PhutilAPCKeyValueCache();171if ($apc->isAvailable()) {172$caches[] = $apc;173}174175return $caches;176}177178179/* -( Repository Graph Cache )--------------------------------------------- */180181182public static function getRepositoryGraphL1Cache() {183static $cache;184if (!$cache) {185$caches = self::buildRepositoryGraphL1Caches();186$cache = self::newStackFromCaches($caches);187}188return $cache;189}190191private static function buildRepositoryGraphL1Caches() {192$caches = array();193194$request = new PhutilInRequestKeyValueCache();195$request->setLimit(32);196$caches[] = $request;197198$apc = new PhutilAPCKeyValueCache();199if ($apc->isAvailable()) {200$caches[] = $apc;201}202203return $caches;204}205206public static function getRepositoryGraphL2Cache() {207static $cache;208if (!$cache) {209$caches = self::buildRepositoryGraphL2Caches();210$cache = self::newStackFromCaches($caches);211}212return $cache;213}214215private static function buildRepositoryGraphL2Caches() {216$caches = array();217$caches[] = new PhabricatorKeyValueDatabaseCache();218return $caches;219}220221222/* -( Server State Cache )------------------------------------------------- */223224225/**226* Highly specialized cache for storing server process state.227*228* We use this cache to track initial steps in the setup phase, before229* configuration is loaded.230*231* This cache does NOT use the cache namespace (it must be accessed before232* we build configuration), and is global across all instances on the host.233*234* @return PhutilKeyValueCacheStack Best available server state cache stack.235* @task setup236*/237public static function getServerStateCache() {238static $cache;239if (!$cache) {240$caches = self::buildSetupCaches('phabricator-server');241242// NOTE: We are NOT adding a cache namespace here! This cache is shared243// across all instances on the host.244245$caches = self::addProfilerToCaches($caches);246$cache = id(new PhutilKeyValueCacheStack())247->setCaches($caches);248249}250return $cache;251}252253254255/* -( Setup Cache )-------------------------------------------------------- */256257258/**259* Highly specialized cache for performing setup checks. We use this cache260* to determine if we need to run expensive setup checks when the page261* loads. Without it, we would need to run these checks every time.262*263* Normally, this cache is just APC. In the absence of APC, this cache264* degrades into a slow, quirky on-disk cache.265*266* NOTE: Do not use this cache for anything else! It is not a general-purpose267* cache!268*269* @return PhutilKeyValueCacheStack Most qualified available cache stack.270* @task setup271*/272public static function getSetupCache() {273static $cache;274if (!$cache) {275$caches = self::buildSetupCaches('phabricator-setup');276$cache = self::newStackFromCaches($caches);277}278return $cache;279}280281282/**283* @task setup284*/285private static function buildSetupCaches($cache_name) {286// If this is the CLI, just build a setup cache.287if (php_sapi_name() == 'cli') {288return array();289}290291// In most cases, we should have APC. This is an ideal cache for our292// purposes -- it's fast and empties on server restart.293$apc = new PhutilAPCKeyValueCache();294if ($apc->isAvailable()) {295return array($apc);296}297298// If we don't have APC, build a poor approximation on disk. This is still299// much better than nothing; some setup steps are quite slow.300$disk_path = self::getSetupCacheDiskCachePath($cache_name);301if ($disk_path) {302$disk = new PhutilOnDiskKeyValueCache();303$disk->setCacheFile($disk_path);304$disk->setWait(0.1);305if ($disk->isAvailable()) {306return array($disk);307}308}309310return array();311}312313314/**315* @task setup316*/317private static function getSetupCacheDiskCachePath($name) {318// The difficulty here is in choosing a path which will change on server319// restart (we MUST have this property), but as rarely as possible320// otherwise (we desire this property to give the cache the best hit rate321// we can).322323// Unfortunately, we don't have a very good strategy for minimizing the324// churn rate of the cache. We previously tried to use the parent process325// PID in some cases, but this was not reliable. See T9599 for one case of326// this.327328$pid_basis = getmypid();329330// If possible, we also want to know when the process launched, so we can331// drop the cache if a process restarts but gets the same PID an earlier332// process had. "/proc" is not available everywhere (e.g., not on OSX), but333// check if we have it.334$epoch_basis = null;335$stat = @stat("/proc/{$pid_basis}");336if ($stat !== false) {337$epoch_basis = $stat['ctime'];338}339340$tmp_dir = sys_get_temp_dir();341342$tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.$name;343if (!file_exists($tmp_path)) {344@mkdir($tmp_path);345}346347$is_ok = self::testTemporaryDirectory($tmp_path);348if (!$is_ok) {349$tmp_path = $tmp_dir;350$is_ok = self::testTemporaryDirectory($tmp_path);351if (!$is_ok) {352// We can't find anywhere to write the cache, so just bail.353return null;354}355}356357$tmp_name = 'setup-'.$pid_basis;358if ($epoch_basis) {359$tmp_name .= '.'.$epoch_basis;360}361$tmp_name .= '.cache';362363return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name;364}365366367/**368* @task setup369*/370private static function testTemporaryDirectory($dir) {371if (!@file_exists($dir)) {372return false;373}374if (!@is_dir($dir)) {375return false;376}377if (!@is_writable($dir)) {378return false;379}380381return true;382}383384private static function addProfilerToCaches(array $caches) {385foreach ($caches as $key => $cache) {386$pcache = new PhutilKeyValueCacheProfiler($cache);387$pcache->setProfiler(PhutilServiceProfiler::getInstance());388$caches[$key] = $pcache;389}390return $caches;391}392393private static function addNamespaceToCaches(array $caches) {394$namespace = self::getNamespace();395if (!$namespace) {396return $caches;397}398399foreach ($caches as $key => $cache) {400$ncache = new PhutilKeyValueCacheNamespace($cache);401$ncache->setNamespace($namespace);402$caches[$key] = $ncache;403}404405return $caches;406}407408409/**410* Deflate a value, if deflation is available and has an impact.411*412* If the value is larger than 1KB, we have `gzdeflate()`, we successfully413* can deflate it, and it benefits from deflation, we deflate it. Otherwise414* we leave it as-is.415*416* Data can later be inflated with @{method:inflateData}.417*418* @param string String to attempt to deflate.419* @return string|null Deflated string, or null if it was not deflated.420* @task compress421*/422public static function maybeDeflateData($value) {423$len = strlen($value);424if ($len <= 1024) {425return null;426}427428if (!function_exists('gzdeflate')) {429return null;430}431432$deflated = gzdeflate($value);433if ($deflated === false) {434return null;435}436437$deflated_len = strlen($deflated);438if ($deflated_len >= ($len / 2)) {439return null;440}441442return $deflated;443}444445446/**447* Inflate data previously deflated by @{method:maybeDeflateData}.448*449* @param string Deflated data, from @{method:maybeDeflateData}.450* @return string Original, uncompressed data.451* @task compress452*/453public static function inflateData($value) {454if (!function_exists('gzinflate')) {455throw new Exception(456pht(457'%s is not available; unable to read deflated data!',458'gzinflate()'));459}460461$value = gzinflate($value);462if ($value === false) {463throw new Exception(pht('Failed to inflate data!'));464}465466return $value;467}468469470}471472473