- <?php
- /**
- * Perforce Swarm
- *
- * @copyright 2013 Perforce Software. All rights reserved.
- * @license Please see LICENSE.txt in top-level folder of this distribution.
- * @version <release>/<patch>
- */
- namespace Record\Cache;
-
- use P4\Model\Connected\ConnectedAbstract as Connected;
-
- /**
- * A basic filesystem cache with multi-server invalidation.
- * The interface is inspired by Zend\Cache, but way simpler.
- *
- * One unique aspect of this cache is that it uses Perforce counters to provide
- * cache validation. Every cache entry can have a corresponding counter. Anytime
- * we read or write a cache entry to a file, we include the counter value for
- * the entry in the filename. Anytime we need to invalidate the cache for a
- * given entry, we increment the corresponding counter value. This ensures that
- * the next read will produce a cache-miss.
- *
- * The cache counters are preferable to local clearing as they will invalidate
- * caches across web-servers in a multi-server environment. To minimize their
- * expense, we only fetch the cache counters once per-instance. The counters can
- * be forcibly refreshed by calling reset().
- *
- * When an item is read from the cache, we hang onto it in memory. This costs
- * memory, but saves time when the same item is read repeatedly. Repeated reads
- * are a common case at the time of this writing. The in-memory cache can be
- * cleared by calling reset().
- */
- class Cache extends Connected
- {
- const COUNTER_PREFIX = 'swarm-cache-';
-
- protected $items = array();
- protected $counters = null;
- protected $cacheDir = null;
-
- /**
- * Get an item from the cache.
- * Note: when getting cached objects, clone them before you modify them.
- * We hang onto an in-memory copy of cached items for performance.
- *
- * @param string $key the item identifier
- * @param bool $success true if successful, false otherwise
- * @return mixed data on success, null on failure
- * @throws Exception if cache directory is not set
- */
- public function getItem($key, &$success = null)
- {
- $file = $this->getFile($key);
- $index = $this->encodeKey($key);
- $success = null;
-
- // check for in-memory (already fetched) item
- if (array_key_exists($index, $this->items)) {
- $success = true;
- return $this->items[$index];
- }
-
- if (!is_readable($file)) {
- $success = false;
- return null;
- }
-
- // unserialize and hang onto item for next time
- $item = unserialize(file_get_contents($file));
- $this->items[$index] = $item;
-
- if ($item === false) {
- $success = false;
- return null;
- }
-
- $success = true;
- return $item;
- }
-
- /**
- * Store an item in the cache.
- *
- * @param string $key the item identifier
- * @param mixed $value the item value to store
- * @return Cache provides fluent interface
- * @throws Exception if unable to write to cache
- */
- public function setItem($key, $value)
- {
- $dir = $this->getCacheDir();
- $file = $this->getFile($key);
- $index = $this->encodeKey($key);
-
- // ensure cache dir exists and is writable
- static::ensureWritable($dir);
-
- if (is_file($file) && !is_writable($file)) {
- @chmod($file, 0700);
- if (!is_writable($file)) {
- throw new Exception(
- "Cannot write to cache file ('" . $file . "'). Check permissions."
- );
- }
- }
-
- file_put_contents($file, serialize($value));
-
- // update in-memory copy
- $this->items[$index] = $value;
-
- return $this;
- }
-
- /**
- * Invalidate cache of the given key (across web-servers).
- * This is accomplished by incrementing the validation number.
- *
- * @param string $key the cache key to invalidate
- * @return Cache provides fluent interface
- */
- public function invalidateItem($key)
- {
- $index = $this->encodeKey($key);
- $counter = static::COUNTER_PREFIX . $index;
- $result = $this->getConnection()->run(
- 'counter',
- array('-u', '-i', $counter)
- );
-
- // clear in-memory copy of item
- unset($this->items[$index]);
-
- // if we have cached counters, update the affected one
- if (is_array($this->counters)) {
- $this->counters[$counter] = $result->getData(0, 'value');
- }
-
- return $this;
- }
-
- /**
- * Invalidate all items in the cache.
- * This is accomplished by incrementing all of the validation numbers.
- *
- * @return Cache provides fluent interface
- */
- public function invalidateAll()
- {
- $counters = $this->getCounters();
- foreach ($counters as $key => $value) {
- $result = $this->getConnection()->run(
- 'counter',
- array('-u', '-i', $key)
- );
- }
-
- // clear in-memory items and counters
- $this->items = array();
- $this->counters = null;
-
- return $this;
- }
-
- /**
- * Set the filesystem path to write cache entries to.
- *
- * @param string $dir the file-system path to write to (will be created)
- * @return Cache provides fluent interface
- */
- public function setCacheDir($dir)
- {
- $this->cacheDir = rtrim($dir, '/');
-
- return $this;
- }
-
- /**
- * Get the filesystem path to write cache entries to.
- *
- * @return string the file-system path to write to
- * @throws Exception if cache directory is not set
- */
- public function getCacheDir()
- {
- if (!$this->cacheDir) {
- throw new Exception("Cannot get cache directory. Directory is unset.");
- }
-
- return $this->cacheDir;
- }
-
- /**
- * Clear the in-memory items and validation numbers.
- * Useful for long-running processes and testing purposes.
- *
- * @return Cache provides fluent interface
- */
- public function reset()
- {
- $this->items = array();
- $this->counters = null;
-
- return $this;
- }
-
- /**
- * Clear the in-memory copy of a specific item.
- *
- * @param string $key the cache key to reset
- * @return Cache provides fluent interface
- */
- public function resetItem($key)
- {
- unset($this->items[$this->encodeKey($key)]);
-
- return $this;
- }
-
- /**
- * Deletes invalidated cache files from the cache directory.
- *
- * @return Cache provides fluent interface
- */
- public function removeInvalidatedFiles()
- {
- $prefix = strtoupper(md5($this->getConnection()->getPort()));
- $regex = '/^' . $prefix . '\-(?P<key>.+)\-(?P<counter>[0-9]+)(\..*)?$/';
- $dir = $this->getCacheDir();
-
- static::ensureWritable($dir);
- $handle = opendir($dir);
- while (($file = readdir($handle)) !== false) {
- if (preg_match($regex, $file, $matches)) {
- $count = $this->getCounter($this->decodeKey($matches['key']));
- if (!$count || (int)$matches['counter'] < $count) {
- // either no counter found, or it is lower than the current one, so remove the file
- unlink($dir . '/' . $file);
- }
- }
- }
- closedir($handle);
-
- return $this;
- }
-
- /**
- * Get the name of the file to read/write a given cache entry.
- * The file is under the cache dir and uses the encoded key and
- * the corresponding cache counter (for invalidation).
- *
- * @param string $key the cache key to get the filename for
- * @return string the file to read/write
- */
- public function getFile($key)
- {
- return $this->getCacheDir()
- . '/' . strtoupper(md5($this->getConnection()->getPort()))
- . '-' . $this->encodeKey($key)
- . '-' . $this->getCounter($key);
- }
-
- /**
- * Throws an exception if $dir is not a directory or is not writable.
- *
- * @param $dir string the name of the directory to check
- * @throws Exception thrown if $dir is not a directory or is not writable
- */
- public static function ensureWritable($dir)
- {
- // ensure cache dir exists and is writable
- if (!is_dir($dir)) {
- @mkdir($dir, 0700, true);
- }
- if (!is_writable($dir)) {
- @chmod($dir, 0700);
- }
- if (!is_dir($dir) || !is_writable($dir)) {
- throw new Exception(
- "Cannot write to cache directory ('" . $dir . "'). Check permissions."
- );
- }
- }
-
- /**
- * Get the validation counter for the given cache key.
- * If no counter exists, 0 will be returned.
- *
- * @param string $key the cache key to get the counter for
- * @return int the cache validation number for the given key
- */
- protected function getCounter($key)
- {
- $counters = $this->getCounters();
- $counter = static::COUNTER_PREFIX . $this->encodeKey($key);
-
- return isset($counters[$counter]) ? (int) $counters[$counter] : 0;
- }
-
- /**
- * Get the cache validation counters. Used to compose cache filenames.
- * The counters are stored in Perforce and read once per-instance or more
- * often if their in-memory copy is cleared.
- *
- * @return array the cache validation counters
- */
- protected function getCounters()
- {
- if ($this->counters === null) {
- $result = $this->getConnection()->run(
- 'counters',
- array('-u', '-e', static::COUNTER_PREFIX . '*')
- );
-
- $counters = array();
- foreach ($result->getData() as $counter) {
- $counters[$counter['counter']] = $counter['value'];
- }
-
- $this->counters = $counters;
- }
-
- return $this->counters;
- }
-
- /**
- * Decodes and returns the key from the encoded version.
- *
- * @param string $key encoded cache key to decode
- * @return string the decoded cache key
- */
- protected function decodeKey($key)
- {
- return pack('H*', $key);
- }
-
- /**
- * Make the given key safe for use in filenames and counters
- *
- * @param string $key the cache key to encode
- * @return string the encoded cache key
- */
- protected function encodeKey($key)
- {
- return strtoupper(bin2hex($key));
- }
- }
# |
Change |
User |
Description |
Committed |
|
#1
|
18334 |
Liz Lam |
initial add of jambox |
9 years ago
|
|