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