@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 244 lines 5.7 kB view raw
1<?php 2 3/** 4 * Interface to a directory-based disk cache. Storage persists across requests. 5 * 6 * This cache is very very slow, and most suitable for command line scripts 7 * which need to build large caches derived from sources like working copies 8 * (for example, Diviner). This cache performs better for large amounts of 9 * data than @{class:PhutilOnDiskKeyValueCache} because each key is serialized 10 * individually, but this comes at the cost of having even slower reads and 11 * writes. 12 * 13 * In addition to having slow reads and writes, this entire cache locks for 14 * any read or write activity. 15 * 16 * Keys for this cache treat the character "/" specially, and encode it as 17 * a new directory on disk. This can help keep the cache organized and keep the 18 * number of items in any single directory under control, by using keys like 19 * "ab/cd/efghijklmn". 20 * 21 * @task kvimpl Key-Value Cache Implementation 22 * @task storage Cache Storage 23 */ 24final class PhutilDirectoryKeyValueCache extends PhutilKeyValueCache { 25 26 private $lock; 27 private $cacheDirectory; 28 29 30/* -( Key-Value Cache Implementation )------------------------------------- */ 31 32 33 public function isAvailable() { 34 return true; 35 } 36 37 38 public function getKeys(array $keys) { 39 $this->validateKeys($keys); 40 41 try { 42 $this->lockCache(); 43 } catch (PhutilLockException $ex) { 44 return array(); 45 } 46 47 $now = time(); 48 49 $results = array(); 50 foreach ($keys as $key) { 51 $key_file = $this->getKeyFile($key); 52 try { 53 $data = Filesystem::readFile($key_file); 54 } catch (FilesystemException $ex) { 55 continue; 56 } 57 58 $data = unserialize($data); 59 if (!$data) { 60 continue; 61 } 62 63 if (isset($data['ttl']) && $data['ttl'] < $now) { 64 continue; 65 } 66 67 $results[$key] = $data['value']; 68 } 69 70 $this->unlockCache(); 71 72 return $results; 73 } 74 75 76 public function setKeys(array $keys, $ttl = null) { 77 $this->validateKeys(array_keys($keys)); 78 79 $this->lockCache(15); 80 81 if ($ttl) { 82 $ttl_epoch = time() + $ttl; 83 } else { 84 $ttl_epoch = null; 85 } 86 87 foreach ($keys as $key => $value) { 88 $dict = array( 89 'value' => $value, 90 ); 91 if ($ttl_epoch) { 92 $dict['ttl'] = $ttl_epoch; 93 } 94 95 try { 96 $key_file = $this->getKeyFile($key); 97 $key_dir = dirname($key_file); 98 if (!Filesystem::pathExists($key_dir)) { 99 Filesystem::createDirectory( 100 $key_dir, 101 $mask = 0755, 102 $recursive = true); 103 } 104 105 $new_file = $key_file.'.new'; 106 Filesystem::writeFile($new_file, serialize($dict)); 107 Filesystem::rename($new_file, $key_file); 108 } catch (FilesystemException $ex) { 109 phlog($ex); 110 } 111 } 112 113 $this->unlockCache(); 114 115 return $this; 116 } 117 118 119 public function deleteKeys(array $keys) { 120 $this->validateKeys($keys); 121 122 $this->lockCache(15); 123 124 foreach ($keys as $key) { 125 $path = $this->getKeyFile($key); 126 Filesystem::remove($path); 127 128 // If removing this key leaves the directory empty, clean it up. Then 129 // clean up any empty parent directories. 130 $path = dirname($path); 131 do { 132 if (!Filesystem::isDescendant($path, $this->getCacheDirectory())) { 133 break; 134 } 135 if (Filesystem::listDirectory($path, true)) { 136 break; 137 } 138 Filesystem::remove($path); 139 $path = dirname($path); 140 } while (true); 141 } 142 143 $this->unlockCache(); 144 145 return $this; 146 } 147 148 149 public function destroyCache() { 150 Filesystem::remove($this->getCacheDirectory()); 151 return $this; 152 } 153 154 155/* -( Cache Storage )------------------------------------------------------ */ 156 157 158 /** 159 * @task storage 160 */ 161 public function setCacheDirectory($directory) { 162 $this->cacheDirectory = rtrim($directory, '/').'/'; 163 return $this; 164 } 165 166 167 /** 168 * @task storage 169 */ 170 private function getCacheDirectory() { 171 if (!$this->cacheDirectory) { 172 throw new PhutilInvalidStateException('setCacheDirectory'); 173 } 174 return $this->cacheDirectory; 175 } 176 177 178 /** 179 * @task storage 180 */ 181 private function getKeyFile($key) { 182 // Colon is a drive separator on Windows. 183 $key = str_replace(':', '_', $key); 184 185 // NOTE: We add ".cache" to each file so we don't get a collision if you 186 // set the keys "a" and "a/b". Without ".cache", the file "a" would need 187 // to be both a file and a directory. 188 return $this->getCacheDirectory().$key.'.cache'; 189 } 190 191 192 /** 193 * @task storage 194 */ 195 private function validateKeys(array $keys) { 196 foreach ($keys as $key) { 197 // NOTE: Use of "." is reserved for ".lock", "key.new" and "key.cache". 198 // Use of "_" is reserved for converting ":". 199 if (!preg_match('@^[a-zA-Z0-9/:-]+$@', $key)) { 200 throw new Exception( 201 pht( 202 "Invalid key '%s': directory caches may only contain letters, ". 203 "numbers, hyphen, colon and slash.", 204 $key)); 205 } 206 } 207 } 208 209 210 /** 211 * @task storage 212 */ 213 private function lockCache($wait = 0) { 214 if ($this->lock) { 215 throw new Exception( 216 pht( 217 'Trying to %s with a lock!', 218 __FUNCTION__.'()')); 219 } 220 221 if (!Filesystem::pathExists($this->getCacheDirectory())) { 222 Filesystem::createDirectory($this->getCacheDirectory(), 0755, true); 223 } 224 225 $lock = PhutilFileLock::newForPath($this->getCacheDirectory().'.lock'); 226 $lock->lock($wait); 227 228 $this->lock = $lock; 229 } 230 231 232 /** 233 * @task storage 234 */ 235 private function unlockCache() { 236 if (!$this->lock) { 237 throw new PhutilInvalidStateException('lockCache'); 238 } 239 240 $this->lock->unlock(); 241 $this->lock = null; 242 } 243 244}