@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 recaptime-dev/main 205 lines 4.4 kB view raw
1<?php 2 3/** 4 * Interface to a disk cache. Storage persists across requests. 5 * 6 * This cache is very slow compared to caches like APC. It is intended as a 7 * specialized alternative to APC when APC is not available. 8 * 9 * This is a highly specialized cache and not appropriate for use as a 10 * generalized key-value cache for arbitrary application data. 11 * 12 * Also note that reading and writing keys from the cache currently involves 13 * loading and saving the entire cache, no matter how little data you touch. 14 * 15 * @task kvimpl Key-Value Cache Implementation 16 * @task storage Cache Storage 17 */ 18final class PhutilOnDiskKeyValueCache extends PhutilKeyValueCache { 19 20 private $cache = array(); 21 private $cacheFile; 22 private $lock; 23 private $wait = 0; 24 25 26/* -( Key-Value Cache Implementation )------------------------------------- */ 27 28 29 public function isAvailable() { 30 return true; 31 } 32 33 34 /** 35 * Set duration (in seconds) to wait for the file lock. 36 */ 37 public function setWait($wait) { 38 $this->wait = $wait; 39 return $this; 40 } 41 42 public function getKeys(array $keys) { 43 $now = time(); 44 45 $results = array(); 46 $reloaded = false; 47 foreach ($keys as $key) { 48 49 // Try to read the value from cache. If we miss, load (or reload) the 50 // cache. 51 52 while (true) { 53 if (isset($this->cache[$key])) { 54 $val = $this->cache[$key]; 55 if (empty($val['ttl']) || $val['ttl'] >= $now) { 56 $results[$key] = $val['val']; 57 break; 58 } 59 } 60 61 if ($reloaded) { 62 break; 63 } 64 65 $this->loadCache($hold_lock = false); 66 $reloaded = true; 67 } 68 } 69 70 return $results; 71 } 72 73 74 public function setKeys(array $keys, $ttl = null) { 75 if ($ttl) { 76 $ttl_epoch = time() + $ttl; 77 } else { 78 $ttl_epoch = null; 79 } 80 81 $dicts = array(); 82 foreach ($keys as $key => $value) { 83 $dict = array( 84 'val' => $value, 85 ); 86 if ($ttl_epoch) { 87 $dict['ttl'] = $ttl_epoch; 88 } 89 $dicts[$key] = $dict; 90 } 91 92 $this->loadCache($hold_lock = true); 93 foreach ($dicts as $key => $dict) { 94 $this->cache[$key] = $dict; 95 } 96 $this->saveCache(); 97 98 return $this; 99 } 100 101 102 public function deleteKeys(array $keys) { 103 $this->loadCache($hold_lock = true); 104 foreach ($keys as $key) { 105 unset($this->cache[$key]); 106 } 107 $this->saveCache(); 108 109 return $this; 110 } 111 112 113 public function destroyCache() { 114 Filesystem::remove($this->getCacheFile()); 115 return $this; 116 } 117 118 119/* -( Cache Storage )------------------------------------------------------ */ 120 121 122 /** 123 * @task storage 124 */ 125 public function setCacheFile($file) { 126 $this->cacheFile = $file; 127 return $this; 128 } 129 130 131 /** 132 * @task storage 133 */ 134 private function loadCache($hold_lock) { 135 if ($this->lock) { 136 throw new Exception( 137 pht( 138 'Trying to %s with a lock!', 139 __FUNCTION__.'()')); 140 } 141 142 $lock = PhutilFileLock::newForPath($this->getCacheFile().'.lock'); 143 try { 144 $lock->lock($this->wait); 145 } catch (PhutilLockException $ex) { 146 if ($hold_lock) { 147 throw $ex; 148 } else { 149 $this->cache = array(); 150 return; 151 } 152 } 153 154 try { 155 $this->cache = array(); 156 if (Filesystem::pathExists($this->getCacheFile())) { 157 $cache = unserialize(Filesystem::readFile($this->getCacheFile())); 158 if ($cache) { 159 $this->cache = $cache; 160 } 161 } 162 } catch (Exception $ex) { 163 $lock->unlock(); 164 throw $ex; 165 } 166 167 if ($hold_lock) { 168 $this->lock = $lock; 169 } else { 170 $lock->unlock(); 171 } 172 } 173 174 175 /** 176 * @task storage 177 */ 178 private function saveCache() { 179 if (!$this->lock) { 180 throw new PhutilInvalidStateException('loadCache'); 181 } 182 183 // We're holding a lock so we're safe to do a write to a well-known file. 184 // Write to the same directory as the cache so the rename won't imply a 185 // copy across volumes. 186 $new = $this->getCacheFile().'.new'; 187 Filesystem::writeFile($new, serialize($this->cache)); 188 Filesystem::rename($new, $this->getCacheFile()); 189 190 $this->lock->unlock(); 191 $this->lock = null; 192 } 193 194 195 /** 196 * @task storage 197 */ 198 private function getCacheFile() { 199 if (!$this->cacheFile) { 200 throw new PhutilInvalidStateException('setCacheFile'); 201 } 202 return $this->cacheFile; 203 } 204 205}