Changeset View
Changeset View
Standalone View
Standalone View
src/infrastructure/cache/PhutilOnDiskKeyValueCache.php
- This file was added.
<?php | |||||
/** | |||||
* Interface to a disk cache. Storage persists across requests. | |||||
* | |||||
* This cache is very slow compared to caches like APC. It is intended as a | |||||
* specialized alternative to APC when APC is not available. | |||||
* | |||||
* This is a highly specialized cache and not appropriate for use as a | |||||
* generalized key-value cache for arbitrary application data. | |||||
* | |||||
* Also note that reading and writing keys from the cache currently involves | |||||
* loading and saving the entire cache, no matter how little data you touch. | |||||
* | |||||
* @task kvimpl Key-Value Cache Implementation | |||||
* @task storage Cache Storage | |||||
*/ | |||||
final class PhutilOnDiskKeyValueCache extends PhutilKeyValueCache { | |||||
private $cache = array(); | |||||
private $cacheFile; | |||||
private $lock; | |||||
private $wait = 0; | |||||
/* -( Key-Value Cache Implementation )------------------------------------- */ | |||||
public function isAvailable() { | |||||
return true; | |||||
} | |||||
/** | |||||
* Set duration (in seconds) to wait for the file lock. | |||||
*/ | |||||
public function setWait($wait) { | |||||
$this->wait = $wait; | |||||
return $this; | |||||
} | |||||
public function getKeys(array $keys) { | |||||
$now = time(); | |||||
$results = array(); | |||||
$reloaded = false; | |||||
foreach ($keys as $key) { | |||||
// Try to read the value from cache. If we miss, load (or reload) the | |||||
// cache. | |||||
while (true) { | |||||
if (isset($this->cache[$key])) { | |||||
$val = $this->cache[$key]; | |||||
if (empty($val['ttl']) || $val['ttl'] >= $now) { | |||||
$results[$key] = $val['val']; | |||||
break; | |||||
} | |||||
} | |||||
if ($reloaded) { | |||||
break; | |||||
} | |||||
$this->loadCache($hold_lock = false); | |||||
$reloaded = true; | |||||
} | |||||
} | |||||
return $results; | |||||
} | |||||
public function setKeys(array $keys, $ttl = null) { | |||||
if ($ttl) { | |||||
$ttl_epoch = time() + $ttl; | |||||
} else { | |||||
$ttl_epoch = null; | |||||
} | |||||
$dicts = array(); | |||||
foreach ($keys as $key => $value) { | |||||
$dict = array( | |||||
'val' => $value, | |||||
); | |||||
if ($ttl_epoch) { | |||||
$dict['ttl'] = $ttl_epoch; | |||||
} | |||||
$dicts[$key] = $dict; | |||||
} | |||||
$this->loadCache($hold_lock = true); | |||||
foreach ($dicts as $key => $dict) { | |||||
$this->cache[$key] = $dict; | |||||
} | |||||
$this->saveCache(); | |||||
return $this; | |||||
} | |||||
public function deleteKeys(array $keys) { | |||||
$this->loadCache($hold_lock = true); | |||||
foreach ($keys as $key) { | |||||
unset($this->cache[$key]); | |||||
} | |||||
$this->saveCache(); | |||||
return $this; | |||||
} | |||||
public function destroyCache() { | |||||
Filesystem::remove($this->getCacheFile()); | |||||
return $this; | |||||
} | |||||
/* -( Cache Storage )------------------------------------------------------ */ | |||||
/** | |||||
* @task storage | |||||
*/ | |||||
public function setCacheFile($file) { | |||||
$this->cacheFile = $file; | |||||
return $this; | |||||
} | |||||
/** | |||||
* @task storage | |||||
*/ | |||||
private function loadCache($hold_lock) { | |||||
if ($this->lock) { | |||||
throw new Exception( | |||||
pht( | |||||
'Trying to %s with a lock!', | |||||
__FUNCTION__.'()')); | |||||
} | |||||
$lock = PhutilFileLock::newForPath($this->getCacheFile().'.lock'); | |||||
try { | |||||
$lock->lock($this->wait); | |||||
} catch (PhutilLockException $ex) { | |||||
if ($hold_lock) { | |||||
throw $ex; | |||||
} else { | |||||
$this->cache = array(); | |||||
return; | |||||
} | |||||
} | |||||
try { | |||||
$this->cache = array(); | |||||
if (Filesystem::pathExists($this->getCacheFile())) { | |||||
$cache = unserialize(Filesystem::readFile($this->getCacheFile())); | |||||
if ($cache) { | |||||
$this->cache = $cache; | |||||
} | |||||
} | |||||
} catch (Exception $ex) { | |||||
$lock->unlock(); | |||||
throw $ex; | |||||
} | |||||
if ($hold_lock) { | |||||
$this->lock = $lock; | |||||
} else { | |||||
$lock->unlock(); | |||||
} | |||||
} | |||||
/** | |||||
* @task storage | |||||
*/ | |||||
private function saveCache() { | |||||
if (!$this->lock) { | |||||
throw new PhutilInvalidStateException('loadCache'); | |||||
} | |||||
// We're holding a lock so we're safe to do a write to a well-known file. | |||||
// Write to the same directory as the cache so the rename won't imply a | |||||
// copy across volumes. | |||||
$new = $this->getCacheFile().'.new'; | |||||
Filesystem::writeFile($new, serialize($this->cache)); | |||||
Filesystem::rename($new, $this->getCacheFile()); | |||||
$this->lock->unlock(); | |||||
$this->lock = null; | |||||
} | |||||
/** | |||||
* @task storage | |||||
*/ | |||||
private function getCacheFile() { | |||||
if (!$this->cacheFile) { | |||||
throw new PhutilInvalidStateException('setCacheFile'); | |||||
} | |||||
return $this->cacheFile; | |||||
} | |||||
} |