@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Interface to a disk cache. Storage persists across requests.
5 *
6 * This cache is very slow compared to caches like APC. It is intended as a
7 * specialized alternative to APC when APC is not available.
8 *
9 * This is a highly specialized cache and not appropriate for use as a
10 * generalized key-value cache for arbitrary application data.
11 *
12 * Also note that reading and writing keys from the cache currently involves
13 * loading and saving the entire cache, no matter how little data you touch.
14 *
15 * @task kvimpl Key-Value Cache Implementation
16 * @task storage Cache Storage
17 */
18final class PhutilOnDiskKeyValueCache extends PhutilKeyValueCache {
19
20 private $cache = array();
21 private $cacheFile;
22 private $lock;
23 private $wait = 0;
24
25
26/* -( Key-Value Cache Implementation )------------------------------------- */
27
28
29 public function isAvailable() {
30 return true;
31 }
32
33
34 /**
35 * Set duration (in seconds) to wait for the file lock.
36 */
37 public function setWait($wait) {
38 $this->wait = $wait;
39 return $this;
40 }
41
42 public function getKeys(array $keys) {
43 $now = time();
44
45 $results = array();
46 $reloaded = false;
47 foreach ($keys as $key) {
48
49 // Try to read the value from cache. If we miss, load (or reload) the
50 // cache.
51
52 while (true) {
53 if (isset($this->cache[$key])) {
54 $val = $this->cache[$key];
55 if (empty($val['ttl']) || $val['ttl'] >= $now) {
56 $results[$key] = $val['val'];
57 break;
58 }
59 }
60
61 if ($reloaded) {
62 break;
63 }
64
65 $this->loadCache($hold_lock = false);
66 $reloaded = true;
67 }
68 }
69
70 return $results;
71 }
72
73
74 public function setKeys(array $keys, $ttl = null) {
75 if ($ttl) {
76 $ttl_epoch = time() + $ttl;
77 } else {
78 $ttl_epoch = null;
79 }
80
81 $dicts = array();
82 foreach ($keys as $key => $value) {
83 $dict = array(
84 'val' => $value,
85 );
86 if ($ttl_epoch) {
87 $dict['ttl'] = $ttl_epoch;
88 }
89 $dicts[$key] = $dict;
90 }
91
92 $this->loadCache($hold_lock = true);
93 foreach ($dicts as $key => $dict) {
94 $this->cache[$key] = $dict;
95 }
96 $this->saveCache();
97
98 return $this;
99 }
100
101
102 public function deleteKeys(array $keys) {
103 $this->loadCache($hold_lock = true);
104 foreach ($keys as $key) {
105 unset($this->cache[$key]);
106 }
107 $this->saveCache();
108
109 return $this;
110 }
111
112
113 public function destroyCache() {
114 Filesystem::remove($this->getCacheFile());
115 return $this;
116 }
117
118
119/* -( Cache Storage )------------------------------------------------------ */
120
121
122 /**
123 * @task storage
124 */
125 public function setCacheFile($file) {
126 $this->cacheFile = $file;
127 return $this;
128 }
129
130
131 /**
132 * @task storage
133 */
134 private function loadCache($hold_lock) {
135 if ($this->lock) {
136 throw new Exception(
137 pht(
138 'Trying to %s with a lock!',
139 __FUNCTION__.'()'));
140 }
141
142 $lock = PhutilFileLock::newForPath($this->getCacheFile().'.lock');
143 try {
144 $lock->lock($this->wait);
145 } catch (PhutilLockException $ex) {
146 if ($hold_lock) {
147 throw $ex;
148 } else {
149 $this->cache = array();
150 return;
151 }
152 }
153
154 try {
155 $this->cache = array();
156 if (Filesystem::pathExists($this->getCacheFile())) {
157 $cache = unserialize(Filesystem::readFile($this->getCacheFile()));
158 if ($cache) {
159 $this->cache = $cache;
160 }
161 }
162 } catch (Exception $ex) {
163 $lock->unlock();
164 throw $ex;
165 }
166
167 if ($hold_lock) {
168 $this->lock = $lock;
169 } else {
170 $lock->unlock();
171 }
172 }
173
174
175 /**
176 * @task storage
177 */
178 private function saveCache() {
179 if (!$this->lock) {
180 throw new PhutilInvalidStateException('loadCache');
181 }
182
183 // We're holding a lock so we're safe to do a write to a well-known file.
184 // Write to the same directory as the cache so the rename won't imply a
185 // copy across volumes.
186 $new = $this->getCacheFile().'.new';
187 Filesystem::writeFile($new, serialize($this->cache));
188 Filesystem::rename($new, $this->getCacheFile());
189
190 $this->lock->unlock();
191 $this->lock = null;
192 }
193
194
195 /**
196 * @task storage
197 */
198 private function getCacheFile() {
199 if (!$this->cacheFile) {
200 throw new PhutilInvalidStateException('setCacheFile');
201 }
202 return $this->cacheFile;
203 }
204
205}