1<?php 2/* 3 * Copyright 2008 Google Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18/* 19 * This class implements a basic on disk storage. While that does 20 * work quite well it's not the most elegant and scalable solution. 21 * It will also get you into a heap of trouble when you try to run 22 * this in a clustered environment. In those cases please use the 23 * MySql back-end 24 * 25 * @author Chris Chabot <chabotc@google.com> 26 */ 27class Google_FileCache extends Google_Cache { 28 private $path; 29 30 public function __construct() { 31 global $apiConfig; 32 $this->path = $apiConfig['ioFileCache_directory']; 33 } 34 35 private function isLocked($storageFile) { 36 // our lock file convention is simple: /the/file/path.lock 37 return file_exists($storageFile . '.lock'); 38 } 39 40 private function createLock($storageFile) { 41 $storageDir = dirname($storageFile); 42 if (! is_dir($storageDir)) { 43 // @codeCoverageIgnoreStart 44 if (! @mkdir($storageDir, 0755, true)) { 45 // make sure the failure isn't because of a concurrency issue 46 if (! is_dir($storageDir)) { 47 throw new Google_CacheException("Could not create storage directory: $storageDir"); 48 } 49 } 50 // @codeCoverageIgnoreEnd 51 } 52 @touch($storageFile . '.lock'); 53 } 54 55 private function removeLock($storageFile) { 56 // suppress all warnings, if some other process removed it that's ok too 57 @unlink($storageFile . '.lock'); 58 } 59 60 private function waitForLock($storageFile) { 61 // 20 x 250 = 5 seconds 62 $tries = 20; 63 $cnt = 0; 64 do { 65 // make sure PHP picks up on file changes. This is an expensive action but really can't be avoided 66 clearstatcache(); 67 // 250 ms is a long time to sleep, but it does stop the server from burning all resources on polling locks.. 68 usleep(250); 69 $cnt ++; 70 } while ($cnt <= $tries && $this->isLocked($storageFile)); 71 if ($this->isLocked($storageFile)) { 72 // 5 seconds passed, assume the owning process died off and remove it 73 $this->removeLock($storageFile); 74 } 75 } 76 77 private function getCacheDir($hash) { 78 // use the first 2 characters of the hash as a directory prefix 79 // this should prevent slowdowns due to huge directory listings 80 // and thus give some basic amount of scalability 81 return $this->path . '/' . substr($hash, 0, 2); 82 } 83 84 private function getCacheFile($hash) { 85 return $this->getCacheDir($hash) . '/' . $hash; 86 } 87 88 public function get($key, $expiration = false) { 89 $storageFile = $this->getCacheFile(md5($key)); 90 // See if this storage file is locked, if so we wait up to 5 seconds for the lock owning process to 91 // complete it's work. If the lock is not released within that time frame, it's cleaned up. 92 // This should give us a fair amount of 'Cache Stampeding' protection 93 if ($this->isLocked($storageFile)) { 94 $this->waitForLock($storageFile); 95 } 96 if (file_exists($storageFile) && is_readable($storageFile)) { 97 $now = time(); 98 if (! $expiration || (($mtime = @filemtime($storageFile)) !== false && ($now - $mtime) < $expiration)) { 99 if (($data = @file_get_contents($storageFile)) !== false) { 100 $data = unserialize($data); 101 return $data; 102 } 103 } 104 } 105 return false; 106 } 107 108 public function set($key, $value) { 109 $storageDir = $this->getCacheDir(md5($key)); 110 $storageFile = $this->getCacheFile(md5($key)); 111 if ($this->isLocked($storageFile)) { 112 // some other process is writing to this file too, wait until it's done to prevent hiccups 113 $this->waitForLock($storageFile); 114 } 115 if (! is_dir($storageDir)) { 116 if (! @mkdir($storageDir, 0755, true)) { 117 throw new Google_CacheException("Could not create storage directory: $storageDir"); 118 } 119 } 120 // we serialize the whole request object, since we don't only want the 121 // responseContent but also the postBody used, headers, size, etc 122 $data = serialize($value); 123 $this->createLock($storageFile); 124 if (! @file_put_contents($storageFile, $data)) { 125 $this->removeLock($storageFile); 126 throw new Google_CacheException("Could not store data in the file"); 127 } 128 $this->removeLock($storageFile); 129 } 130 131 public function delete($key) { 132 $file = $this->getCacheFile(md5($key)); 133 if (! @unlink($file)) { 134 throw new Google_CacheException("Cache file could not be deleted"); 135 } 136 } 137} 138