@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
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}