@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 471 lines 12 kB view raw
1<?php 2 3/** 4 * 5 * @task request Request Cache 6 * @task immutable Immutable Cache 7 * @task setup Setup Cache 8 * @task compress Compression 9 */ 10final class PhabricatorCaches extends Phobject { 11 12 private static $requestCache; 13 14 public static function getNamespace() { 15 return PhabricatorEnv::getEnvConfig('phabricator.cache-namespace'); 16 } 17 18 private static function newStackFromCaches(array $caches) { 19 $caches = self::addNamespaceToCaches($caches); 20 $caches = self::addProfilerToCaches($caches); 21 return id(new PhutilKeyValueCacheStack()) 22 ->setCaches($caches); 23 } 24 25/* -( Request Cache )------------------------------------------------------ */ 26 27 28 /** 29 * Get a request cache stack. 30 * 31 * This cache stack is destroyed after each logical request. In particular, 32 * it is destroyed periodically by the daemons, while `static` caches are 33 * not. 34 * 35 * @return PhutilKeyValueCacheStack Request cache stack. 36 */ 37 public static function getRequestCache() { 38 if (!self::$requestCache) { 39 self::$requestCache = new PhutilInRequestKeyValueCache(); 40 } 41 return self::$requestCache; 42 } 43 44 45 /** 46 * Destroy the request cache. 47 * 48 * This is called at the beginning of each logical request. 49 * 50 * @return void 51 */ 52 public static function destroyRequestCache() { 53 self::$requestCache = null; 54 55 // See T12997. Force the GC to run when the request cache is destroyed to 56 // clean up any cycles which may still be hanging around. 57 if (function_exists('gc_collect_cycles')) { 58 gc_collect_cycles(); 59 } 60 } 61 62 63/* -( Immutable Cache )---------------------------------------------------- */ 64 65 66 /** 67 * Gets an immutable cache stack. 68 * 69 * This stack trades mutability away for improved performance. Normally, it is 70 * APC + DB. 71 * 72 * In the general case with multiple web frontends, this stack can not be 73 * cleared, so it is only appropriate for use if the value of a given key is 74 * permanent and immutable. 75 * 76 * @return PhutilKeyValueCacheStack Best immutable stack available. 77 * @task immutable 78 */ 79 public static function getImmutableCache() { 80 static $cache; 81 if (!$cache) { 82 $caches = self::buildImmutableCaches(); 83 $cache = self::newStackFromCaches($caches); 84 } 85 return $cache; 86 } 87 88 89 /** 90 * Build the immutable cache stack. 91 * 92 * @return list<PhutilKeyValueCache> List of caches. 93 * @task immutable 94 */ 95 private static function buildImmutableCaches() { 96 $caches = array(); 97 98 $apc = new PhutilAPCKeyValueCache(); 99 if ($apc->isAvailable()) { 100 $caches[] = $apc; 101 } 102 103 $caches[] = new PhabricatorKeyValueDatabaseCache(); 104 105 return $caches; 106 } 107 108 public static function getMutableCache() { 109 static $cache; 110 if (!$cache) { 111 $caches = self::buildMutableCaches(); 112 $cache = self::newStackFromCaches($caches); 113 } 114 return $cache; 115 } 116 117 private static function buildMutableCaches() { 118 $caches = array(); 119 120 $caches[] = new PhabricatorKeyValueDatabaseCache(); 121 122 return $caches; 123 } 124 125 public static function getMutableStructureCache() { 126 static $cache; 127 if (!$cache) { 128 $caches = self::buildMutableStructureCaches(); 129 $cache = self::newStackFromCaches($caches); 130 } 131 return $cache; 132 } 133 134 private static function buildMutableStructureCaches() { 135 $caches = array(); 136 137 $cache = new PhabricatorKeyValueDatabaseCache(); 138 $cache = new PhabricatorKeyValueSerializingCacheProxy($cache); 139 $caches[] = $cache; 140 141 return $caches; 142 } 143 144/* -( Runtime Cache )------------------------------------------------------ */ 145 146 147 /** 148 * Get a runtime cache stack. 149 * 150 * This stack is just APC. It's fast, it's effectively immutable, and it 151 * gets thrown away when the webserver restarts. 152 * 153 * This cache is suitable for deriving runtime caches, like a map of Conduit 154 * method names to provider classes. 155 * 156 * @return PhutilKeyValueCacheStack Best runtime stack available. 157 */ 158 public static function getRuntimeCache() { 159 static $cache; 160 if (!$cache) { 161 $caches = self::buildRuntimeCaches(); 162 $cache = self::newStackFromCaches($caches); 163 } 164 return $cache; 165 } 166 167 168 private static function buildRuntimeCaches() { 169 $caches = array(); 170 171 $apc = new PhutilAPCKeyValueCache(); 172 if ($apc->isAvailable()) { 173 $caches[] = $apc; 174 } 175 176 return $caches; 177 } 178 179 180/* -( Repository Graph Cache )--------------------------------------------- */ 181 182 183 public static function getRepositoryGraphL1Cache() { 184 static $cache; 185 if (!$cache) { 186 $caches = self::buildRepositoryGraphL1Caches(); 187 $cache = self::newStackFromCaches($caches); 188 } 189 return $cache; 190 } 191 192 private static function buildRepositoryGraphL1Caches() { 193 $caches = array(); 194 195 $request = new PhutilInRequestKeyValueCache(); 196 $request->setLimit(32); 197 $caches[] = $request; 198 199 $apc = new PhutilAPCKeyValueCache(); 200 if ($apc->isAvailable()) { 201 $caches[] = $apc; 202 } 203 204 return $caches; 205 } 206 207 public static function getRepositoryGraphL2Cache() { 208 static $cache; 209 if (!$cache) { 210 $caches = self::buildRepositoryGraphL2Caches(); 211 $cache = self::newStackFromCaches($caches); 212 } 213 return $cache; 214 } 215 216 private static function buildRepositoryGraphL2Caches() { 217 $caches = array(); 218 $caches[] = new PhabricatorKeyValueDatabaseCache(); 219 return $caches; 220 } 221 222 223/* -( Server State Cache )------------------------------------------------- */ 224 225 226 /** 227 * Highly specialized cache for storing server process state. 228 * 229 * We use this cache to track initial steps in the setup phase, before 230 * configuration is loaded. 231 * 232 * This cache does NOT use the cache namespace (it must be accessed before 233 * we build configuration), and is global across all instances on the host. 234 * 235 * @return PhutilKeyValueCacheStack Best available server state cache stack. 236 * @task setup 237 */ 238 public static function getServerStateCache() { 239 static $cache; 240 if (!$cache) { 241 $caches = self::buildSetupCaches('phabricator-server'); 242 243 // NOTE: We are NOT adding a cache namespace here! This cache is shared 244 // across all instances on the host. 245 246 $caches = self::addProfilerToCaches($caches); 247 $cache = id(new PhutilKeyValueCacheStack()) 248 ->setCaches($caches); 249 250 } 251 return $cache; 252 } 253 254 255 256/* -( Setup Cache )-------------------------------------------------------- */ 257 258 259 /** 260 * Highly specialized cache for performing setup checks. We use this cache 261 * to determine if we need to run expensive setup checks when the page 262 * loads. Without it, we would need to run these checks every time. 263 * 264 * Normally, this cache is just APC. In the absence of APC, this cache 265 * degrades into a slow, quirky on-disk cache. 266 * 267 * NOTE: Do not use this cache for anything else! It is not a general-purpose 268 * cache! 269 * 270 * @return PhutilKeyValueCacheStack Most qualified available cache stack. 271 * @task setup 272 */ 273 public static function getSetupCache() { 274 static $cache; 275 if (!$cache) { 276 $caches = self::buildSetupCaches('phabricator-setup'); 277 $cache = self::newStackFromCaches($caches); 278 } 279 return $cache; 280 } 281 282 283 /** 284 * @task setup 285 */ 286 private static function buildSetupCaches($cache_name) { 287 // If this is the CLI, just build a setup cache. 288 if (php_sapi_name() == 'cli') { 289 return array(); 290 } 291 292 // In most cases, we should have APC. This is an ideal cache for our 293 // purposes -- it's fast and empties on server restart. 294 $apc = new PhutilAPCKeyValueCache(); 295 if ($apc->isAvailable()) { 296 return array($apc); 297 } 298 299 // If we don't have APC, build a poor approximation on disk. This is still 300 // much better than nothing; some setup steps are quite slow. 301 $disk_path = self::getSetupCacheDiskCachePath($cache_name); 302 if ($disk_path) { 303 $disk = new PhutilOnDiskKeyValueCache(); 304 $disk->setCacheFile($disk_path); 305 $disk->setWait(0.1); 306 if ($disk->isAvailable()) { 307 return array($disk); 308 } 309 } 310 311 return array(); 312 } 313 314 315 /** 316 * @task setup 317 */ 318 private static function getSetupCacheDiskCachePath($name) { 319 // The difficulty here is in choosing a path which will change on server 320 // restart (we MUST have this property), but as rarely as possible 321 // otherwise (we desire this property to give the cache the best hit rate 322 // we can). 323 324 // Unfortunately, we don't have a very good strategy for minimizing the 325 // churn rate of the cache. We previously tried to use the parent process 326 // PID in some cases, but this was not reliable. See T9599 for one case of 327 // this. 328 329 $pid_basis = getmypid(); 330 331 // If possible, we also want to know when the process launched, so we can 332 // drop the cache if a process restarts but gets the same PID an earlier 333 // process had. "/proc" is not available everywhere (e.g., not on OSX), but 334 // check if we have it. 335 $epoch_basis = null; 336 $stat = @stat("/proc/{$pid_basis}"); 337 if ($stat !== false) { 338 $epoch_basis = $stat['ctime']; 339 } 340 341 $tmp_dir = sys_get_temp_dir(); 342 343 $tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.$name; 344 if (!file_exists($tmp_path)) { 345 @mkdir($tmp_path); 346 } 347 348 $is_ok = self::testTemporaryDirectory($tmp_path); 349 if (!$is_ok) { 350 $tmp_path = $tmp_dir; 351 $is_ok = self::testTemporaryDirectory($tmp_path); 352 if (!$is_ok) { 353 // We can't find anywhere to write the cache, so just bail. 354 return null; 355 } 356 } 357 358 $tmp_name = 'setup-'.$pid_basis; 359 if ($epoch_basis) { 360 $tmp_name .= '.'.$epoch_basis; 361 } 362 $tmp_name .= '.cache'; 363 364 return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name; 365 } 366 367 368 /** 369 * @task setup 370 */ 371 private static function testTemporaryDirectory($dir) { 372 if (!@file_exists($dir)) { 373 return false; 374 } 375 if (!@is_dir($dir)) { 376 return false; 377 } 378 if (!@is_writable($dir)) { 379 return false; 380 } 381 382 return true; 383 } 384 385 private static function addProfilerToCaches(array $caches) { 386 foreach ($caches as $key => $cache) { 387 $pcache = new PhutilKeyValueCacheProfiler($cache); 388 $pcache->setProfiler(PhutilServiceProfiler::getInstance()); 389 $caches[$key] = $pcache; 390 } 391 return $caches; 392 } 393 394 private static function addNamespaceToCaches(array $caches) { 395 $namespace = self::getNamespace(); 396 if (!$namespace) { 397 return $caches; 398 } 399 400 foreach ($caches as $key => $cache) { 401 $ncache = new PhutilKeyValueCacheNamespace($cache); 402 $ncache->setNamespace($namespace); 403 $caches[$key] = $ncache; 404 } 405 406 return $caches; 407 } 408 409 410 /** 411 * Deflate a value, if deflation is available and has an impact. 412 * 413 * If the value is larger than 1KB, we have `gzdeflate()`, we successfully 414 * can deflate it, and it benefits from deflation, we deflate it. Otherwise 415 * we leave it as-is. 416 * 417 * Data can later be inflated with @{method:inflateData}. 418 * 419 * @param string $value String to attempt to deflate. 420 * @return string|null Deflated string, or null if it was not deflated. 421 * @task compress 422 */ 423 public static function maybeDeflateData($value) { 424 $len = strlen($value); 425 if ($len <= 1024) { 426 return null; 427 } 428 429 if (!function_exists('gzdeflate')) { 430 return null; 431 } 432 433 $deflated = gzdeflate($value); 434 if ($deflated === false) { 435 return null; 436 } 437 438 $deflated_len = strlen($deflated); 439 if ($deflated_len >= ($len / 2)) { 440 return null; 441 } 442 443 return $deflated; 444 } 445 446 447 /** 448 * Inflate data previously deflated by @{method:maybeDeflateData}. 449 * 450 * @param string $value Deflated data, from @{method:maybeDeflateData}. 451 * @return string Original, uncompressed data. 452 * @task compress 453 */ 454 public static function inflateData($value) { 455 if (!function_exists('gzinflate')) { 456 throw new Exception( 457 pht( 458 '%s is not available; unable to read deflated data!', 459 'gzinflate()')); 460 } 461 462 $value = gzinflate($value); 463 if ($value === false) { 464 throw new Exception(pht('Failed to inflate data!')); 465 } 466 467 return $value; 468 } 469 470 471}