Path: blob/master/src/infrastructure/cache/PhutilDirectoryKeyValueCache.php
12241 views
<?php12/**3* Interface to a directory-based disk cache. Storage persists across requests.4*5* This cache is very very slow, and most suitable for command line scripts6* which need to build large caches derived from sources like working copies7* (for example, Diviner). This cache performs better for large amounts of8* data than @{class:PhutilOnDiskKeyValueCache} because each key is serialized9* individually, but this comes at the cost of having even slower reads and10* writes.11*12* In addition to having slow reads and writes, this entire cache locks for13* any read or write activity.14*15* Keys for this cache treat the character "/" specially, and encode it as16* a new directory on disk. This can help keep the cache organized and keep the17* number of items in any single directory under control, by using keys like18* "ab/cd/efghijklmn".19*20* @task kvimpl Key-Value Cache Implementation21* @task storage Cache Storage22*/23final class PhutilDirectoryKeyValueCache extends PhutilKeyValueCache {2425private $lock;26private $cacheDirectory;272829/* -( Key-Value Cache Implementation )------------------------------------- */303132public function isAvailable() {33return true;34}353637public function getKeys(array $keys) {38$this->validateKeys($keys);3940try {41$this->lockCache();42} catch (PhutilLockException $ex) {43return array();44}4546$now = time();4748$results = array();49foreach ($keys as $key) {50$key_file = $this->getKeyFile($key);51try {52$data = Filesystem::readFile($key_file);53} catch (FilesystemException $ex) {54continue;55}5657$data = unserialize($data);58if (!$data) {59continue;60}6162if (isset($data['ttl']) && $data['ttl'] < $now) {63continue;64}6566$results[$key] = $data['value'];67}6869$this->unlockCache();7071return $results;72}737475public function setKeys(array $keys, $ttl = null) {76$this->validateKeys(array_keys($keys));7778$this->lockCache(15);7980if ($ttl) {81$ttl_epoch = time() + $ttl;82} else {83$ttl_epoch = null;84}8586foreach ($keys as $key => $value) {87$dict = array(88'value' => $value,89);90if ($ttl_epoch) {91$dict['ttl'] = $ttl_epoch;92}9394try {95$key_file = $this->getKeyFile($key);96$key_dir = dirname($key_file);97if (!Filesystem::pathExists($key_dir)) {98Filesystem::createDirectory(99$key_dir,100$mask = 0755,101$recursive = true);102}103104$new_file = $key_file.'.new';105Filesystem::writeFile($new_file, serialize($dict));106Filesystem::rename($new_file, $key_file);107} catch (FilesystemException $ex) {108phlog($ex);109}110}111112$this->unlockCache();113114return $this;115}116117118public function deleteKeys(array $keys) {119$this->validateKeys($keys);120121$this->lockCache(15);122123foreach ($keys as $key) {124$path = $this->getKeyFile($key);125Filesystem::remove($path);126127// If removing this key leaves the directory empty, clean it up. Then128// clean up any empty parent directories.129$path = dirname($path);130do {131if (!Filesystem::isDescendant($path, $this->getCacheDirectory())) {132break;133}134if (Filesystem::listDirectory($path, true)) {135break;136}137Filesystem::remove($path);138$path = dirname($path);139} while (true);140}141142$this->unlockCache();143144return $this;145}146147148public function destroyCache() {149Filesystem::remove($this->getCacheDirectory());150return $this;151}152153154/* -( Cache Storage )------------------------------------------------------ */155156157/**158* @task storage159*/160public function setCacheDirectory($directory) {161$this->cacheDirectory = rtrim($directory, '/').'/';162return $this;163}164165166/**167* @task storage168*/169private function getCacheDirectory() {170if (!$this->cacheDirectory) {171throw new PhutilInvalidStateException('setCacheDirectory');172}173return $this->cacheDirectory;174}175176177/**178* @task storage179*/180private function getKeyFile($key) {181// Colon is a drive separator on Windows.182$key = str_replace(':', '_', $key);183184// NOTE: We add ".cache" to each file so we don't get a collision if you185// set the keys "a" and "a/b". Without ".cache", the file "a" would need186// to be both a file and a directory.187return $this->getCacheDirectory().$key.'.cache';188}189190191/**192* @task storage193*/194private function validateKeys(array $keys) {195foreach ($keys as $key) {196// NOTE: Use of "." is reserved for ".lock", "key.new" and "key.cache".197// Use of "_" is reserved for converting ":".198if (!preg_match('@^[a-zA-Z0-9/:-]+$@', $key)) {199throw new Exception(200pht(201"Invalid key '%s': directory caches may only contain letters, ".202"numbers, hyphen, colon and slash.",203$key));204}205}206}207208209/**210* @task storage211*/212private function lockCache($wait = 0) {213if ($this->lock) {214throw new Exception(215pht(216'Trying to %s with a lock!',217__FUNCTION__.'()'));218}219220if (!Filesystem::pathExists($this->getCacheDirectory())) {221Filesystem::createDirectory($this->getCacheDirectory(), 0755, true);222}223224$lock = PhutilFileLock::newForPath($this->getCacheDirectory().'.lock');225$lock->lock($wait);226227$this->lock = $lock;228}229230231/**232* @task storage233*/234private function unlockCache() {235if (!$this->lock) {236throw new PhutilInvalidStateException('lockCache');237}238239$this->lock->unlock();240$this->lock = null;241}242243}244245246