Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/cache/PhutilDirectoryKeyValueCache.php
12241 views
1
<?php
2
3
/**
4
* Interface to a directory-based disk cache. Storage persists across requests.
5
*
6
* This cache is very very slow, and most suitable for command line scripts
7
* which need to build large caches derived from sources like working copies
8
* (for example, Diviner). This cache performs better for large amounts of
9
* data than @{class:PhutilOnDiskKeyValueCache} because each key is serialized
10
* individually, but this comes at the cost of having even slower reads and
11
* writes.
12
*
13
* In addition to having slow reads and writes, this entire cache locks for
14
* any read or write activity.
15
*
16
* Keys for this cache treat the character "/" specially, and encode it as
17
* a new directory on disk. This can help keep the cache organized and keep the
18
* number of items in any single directory under control, by using keys like
19
* "ab/cd/efghijklmn".
20
*
21
* @task kvimpl Key-Value Cache Implementation
22
* @task storage Cache Storage
23
*/
24
final class PhutilDirectoryKeyValueCache extends PhutilKeyValueCache {
25
26
private $lock;
27
private $cacheDirectory;
28
29
30
/* -( Key-Value Cache Implementation )------------------------------------- */
31
32
33
public function isAvailable() {
34
return true;
35
}
36
37
38
public function getKeys(array $keys) {
39
$this->validateKeys($keys);
40
41
try {
42
$this->lockCache();
43
} catch (PhutilLockException $ex) {
44
return array();
45
}
46
47
$now = time();
48
49
$results = array();
50
foreach ($keys as $key) {
51
$key_file = $this->getKeyFile($key);
52
try {
53
$data = Filesystem::readFile($key_file);
54
} catch (FilesystemException $ex) {
55
continue;
56
}
57
58
$data = unserialize($data);
59
if (!$data) {
60
continue;
61
}
62
63
if (isset($data['ttl']) && $data['ttl'] < $now) {
64
continue;
65
}
66
67
$results[$key] = $data['value'];
68
}
69
70
$this->unlockCache();
71
72
return $results;
73
}
74
75
76
public function setKeys(array $keys, $ttl = null) {
77
$this->validateKeys(array_keys($keys));
78
79
$this->lockCache(15);
80
81
if ($ttl) {
82
$ttl_epoch = time() + $ttl;
83
} else {
84
$ttl_epoch = null;
85
}
86
87
foreach ($keys as $key => $value) {
88
$dict = array(
89
'value' => $value,
90
);
91
if ($ttl_epoch) {
92
$dict['ttl'] = $ttl_epoch;
93
}
94
95
try {
96
$key_file = $this->getKeyFile($key);
97
$key_dir = dirname($key_file);
98
if (!Filesystem::pathExists($key_dir)) {
99
Filesystem::createDirectory(
100
$key_dir,
101
$mask = 0755,
102
$recursive = true);
103
}
104
105
$new_file = $key_file.'.new';
106
Filesystem::writeFile($new_file, serialize($dict));
107
Filesystem::rename($new_file, $key_file);
108
} catch (FilesystemException $ex) {
109
phlog($ex);
110
}
111
}
112
113
$this->unlockCache();
114
115
return $this;
116
}
117
118
119
public function deleteKeys(array $keys) {
120
$this->validateKeys($keys);
121
122
$this->lockCache(15);
123
124
foreach ($keys as $key) {
125
$path = $this->getKeyFile($key);
126
Filesystem::remove($path);
127
128
// If removing this key leaves the directory empty, clean it up. Then
129
// clean up any empty parent directories.
130
$path = dirname($path);
131
do {
132
if (!Filesystem::isDescendant($path, $this->getCacheDirectory())) {
133
break;
134
}
135
if (Filesystem::listDirectory($path, true)) {
136
break;
137
}
138
Filesystem::remove($path);
139
$path = dirname($path);
140
} while (true);
141
}
142
143
$this->unlockCache();
144
145
return $this;
146
}
147
148
149
public function destroyCache() {
150
Filesystem::remove($this->getCacheDirectory());
151
return $this;
152
}
153
154
155
/* -( Cache Storage )------------------------------------------------------ */
156
157
158
/**
159
* @task storage
160
*/
161
public function setCacheDirectory($directory) {
162
$this->cacheDirectory = rtrim($directory, '/').'/';
163
return $this;
164
}
165
166
167
/**
168
* @task storage
169
*/
170
private function getCacheDirectory() {
171
if (!$this->cacheDirectory) {
172
throw new PhutilInvalidStateException('setCacheDirectory');
173
}
174
return $this->cacheDirectory;
175
}
176
177
178
/**
179
* @task storage
180
*/
181
private function getKeyFile($key) {
182
// Colon is a drive separator on Windows.
183
$key = str_replace(':', '_', $key);
184
185
// NOTE: We add ".cache" to each file so we don't get a collision if you
186
// set the keys "a" and "a/b". Without ".cache", the file "a" would need
187
// to be both a file and a directory.
188
return $this->getCacheDirectory().$key.'.cache';
189
}
190
191
192
/**
193
* @task storage
194
*/
195
private function validateKeys(array $keys) {
196
foreach ($keys as $key) {
197
// NOTE: Use of "." is reserved for ".lock", "key.new" and "key.cache".
198
// Use of "_" is reserved for converting ":".
199
if (!preg_match('@^[a-zA-Z0-9/:-]+$@', $key)) {
200
throw new Exception(
201
pht(
202
"Invalid key '%s': directory caches may only contain letters, ".
203
"numbers, hyphen, colon and slash.",
204
$key));
205
}
206
}
207
}
208
209
210
/**
211
* @task storage
212
*/
213
private function lockCache($wait = 0) {
214
if ($this->lock) {
215
throw new Exception(
216
pht(
217
'Trying to %s with a lock!',
218
__FUNCTION__.'()'));
219
}
220
221
if (!Filesystem::pathExists($this->getCacheDirectory())) {
222
Filesystem::createDirectory($this->getCacheDirectory(), 0755, true);
223
}
224
225
$lock = PhutilFileLock::newForPath($this->getCacheDirectory().'.lock');
226
$lock->lock($wait);
227
228
$this->lock = $lock;
229
}
230
231
232
/**
233
* @task storage
234
*/
235
private function unlockCache() {
236
if (!$this->lock) {
237
throw new PhutilInvalidStateException('lockCache');
238
}
239
240
$this->lock->unlock();
241
$this->lock = null;
242
}
243
244
}
245
246