Skip to content

Commit aae5f9e

Browse files
author
epriestley
committedDec 21, 2012
Implement a more compact, general database-backed key-value cache
Summary: See discussion in D4204. Facebook currently has a 314MB remarkup cache with a 55MB index, which is slow to access. Under the theory that this is an index size/quality problem (the current index is on a potentially-384-byte field, with many keys sharing prefixes), provide a more general index with fancy new features: - It implements PhutilKeyValueCache, so it can be a component in cache stacks and supports TTL. - It has a 12-byte hash-based key. - It automatically compresses large blocks of data (most of what we store is highly-compressible HTML). Test Plan: - Basics: - Loaded /paste/, saw caches generate and save. - Reloaded /paste/, saw the page hit cache. - GC: - Ran GC daemon, saw nothing. - Set maximum lifetime to 1 second, ran GC daemon, saw it collect the entire cache. - Deflate: - Selected row formats from the database, saw a mixture of 'raw' and 'deflate' storage. - Used profiler to verify that 'deflate' is fast (12 calls @ 220us on my paste list). - Ran unit tests Reviewers: vrana, btrahan Reviewed By: vrana CC: aran Differential Revision: https://secure.phabricator.com/D4259
1 parent 62bc337 commit aae5f9e

9 files changed

+407
-13
lines changed
 

‎conf/default.conf.php

+8
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,13 @@
11301130
'remarkup.enable-embedded-youtube' => false,
11311131

11321132

1133+
// -- Cache ----------------------------------------------------------------- //
1134+
1135+
// Set this to false to disable the use of gzdeflate()-based compression in
1136+
// some caches. This may give you less performant (but more debuggable)
1137+
// caching.
1138+
'cache.enable-deflate' => true,
1139+
11331140
// -- Garbage Collection ---------------------------------------------------- //
11341141

11351142
// Phabricator generates various logs and caches in the database which can
@@ -1160,6 +1167,7 @@
11601167
'gcdaemon.ttl.differential-parse-cache' => 14 * (24 * 60 * 60),
11611168
'gcdaemon.ttl.markup-cache' => 30 * (24 * 60 * 60),
11621169
'gcdaemon.ttl.task-archive' => 14 * (24 * 60 * 60),
1170+
'gcdaemon.ttl.general-cache' => 30 * (24 * 60 * 60),
11631171

11641172

11651173
// -- Feed ------------------------------------------------------------------ //
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CREATE TABLE {$NAMESPACE}_cache.cache_general (
2+
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
3+
cacheKeyHash CHAR(12) BINARY NOT NULL,
4+
cacheKey VARCHAR(128) NOT NULL COLLATE utf8_bin,
5+
cacheFormat VARCHAR(16) NOT NULL COLLATE utf8_bin,
6+
cacheData LONGBLOB NOT NULL,
7+
cacheCreated INT UNSIGNED NOT NULL,
8+
cacheExpires INT UNSIGNED,
9+
KEY `key_cacheCreated` (cacheCreated),
10+
UNIQUE KEY `key_cacheKeyHash` (cacheKeyHash)
11+
) ENGINE=InnoDB, COLLATE utf8_general_ci;

‎src/__phutil_library_map__.php

+4
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@
505505
'JavelinUIExample' => 'applications/uiexample/examples/JavelinUIExample.php',
506506
'JavelinViewExample' => 'applications/uiexample/examples/JavelinViewExample.php',
507507
'JavelinViewExampleServerView' => 'applications/uiexample/examples/JavelinViewExampleServerView.php',
508+
'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php',
508509
'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php',
509510
'LiskDAOSet' => 'infrastructure/storage/lisk/LiskDAOSet.php',
510511
'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php',
@@ -835,6 +836,7 @@
835836
'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php',
836837
'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php',
837838
'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php',
839+
'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php',
838840
'PhabricatorLDAPLoginController' => 'applications/auth/controller/PhabricatorLDAPLoginController.php',
839841
'PhabricatorLDAPProvider' => 'applications/auth/ldap/PhabricatorLDAPProvider.php',
840842
'PhabricatorLDAPRegistrationController' => 'applications/auth/controller/PhabricatorLDAPRegistrationController.php',
@@ -1789,6 +1791,7 @@
17891791
'JavelinUIExample' => 'PhabricatorUIExample',
17901792
'JavelinViewExample' => 'PhabricatorUIExample',
17911793
'JavelinViewExampleServerView' => 'AphrontView',
1794+
'LiskChunkTestCase' => 'PhabricatorTestCase',
17921795
'LiskDAOTestCase' => 'PhabricatorTestCase',
17931796
'LiskEphemeralObjectException' => 'Exception',
17941797
'LiskFixtureTestCase' => 'PhabricatorTestCase',
@@ -2118,6 +2121,7 @@
21182121
'PhabricatorInlineCommentPreviewController' => 'PhabricatorController',
21192122
'PhabricatorInlineSummaryView' => 'AphrontView',
21202123
'PhabricatorJavelinLinter' => 'ArcanistLinter',
2124+
'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache',
21212125
'PhabricatorLDAPLoginController' => 'PhabricatorAuthController',
21222126
'PhabricatorLDAPRegistrationController' => 'PhabricatorAuthController',
21232127
'PhabricatorLDAPUnknownUserException' => 'Exception',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
<?php
2+
3+
final class PhabricatorKeyValueDatabaseCache
4+
extends PhutilKeyValueCache {
5+
6+
const CACHE_FORMAT_RAW = 'raw';
7+
const CACHE_FORMAT_DEFLATE = 'deflate';
8+
9+
public function setKeys(array $keys, $ttl = null) {
10+
$call_id = null;
11+
if ($this->getProfiler()) {
12+
$call_id = $this->getProfiler()->beginServiceCall(
13+
array(
14+
'type' => 'kvcache-set',
15+
'name' => 'phabricator-db',
16+
'keys' => array_keys($keys),
17+
'ttl' => $ttl,
18+
));
19+
}
20+
21+
if ($keys) {
22+
$map = $this->digestKeys(array_keys($keys));
23+
$conn_w = $this->establishConnection('w');
24+
25+
$sql = array();
26+
foreach ($map as $key => $hash) {
27+
$value = $keys[$key];
28+
29+
list($format, $storage_value) = $this->willWriteValue($key, $value);
30+
31+
$sql[] = qsprintf(
32+
$conn_w,
33+
'(%s, %s, %s, %s, %d, %nd)',
34+
$hash,
35+
$key,
36+
$format,
37+
$storage_value,
38+
time(),
39+
$ttl ? (time() + $ttl) : null);
40+
}
41+
42+
$guard = AphrontWriteGuard::beginScopedUnguardedWrites();
43+
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
44+
queryfx(
45+
$conn_w,
46+
'INSERT INTO %T
47+
(cacheKeyHash, cacheKey, cacheFormat, cacheData,
48+
cacheCreated, cacheExpires) VALUES %Q
49+
ON DUPLICATE KEY UPDATE
50+
cacheKey = VALUES(cacheKey),
51+
cacheFormat = VALUES(cacheFormat),
52+
cacheData = VALUES(cacheData),
53+
cacheCreated = VALUES(cacheCreated),
54+
cacheExpires = VALUES(cacheExpires)',
55+
$this->getTableName(),
56+
$chunk);
57+
}
58+
unset($guard);
59+
}
60+
61+
if ($call_id) {
62+
$this->getProfiler()->endServiceCall($call_id, array());
63+
}
64+
65+
return $this;
66+
}
67+
68+
public function getKeys(array $keys) {
69+
$call_id = null;
70+
if ($this->getProfiler()) {
71+
$call_id = $this->getProfiler()->beginServiceCall(
72+
array(
73+
'type' => 'kvcache-get',
74+
'name' => 'phabricator-db',
75+
'keys' => $keys,
76+
));
77+
}
78+
79+
$results = array();
80+
if ($keys) {
81+
$map = $this->digestKeys($keys);
82+
83+
$rows = queryfx_all(
84+
$this->establishConnection('r'),
85+
'SELECT * FROM %T WHERE cacheKeyHash IN (%Ls)',
86+
$this->getTableName(),
87+
$map);
88+
$rows = ipull($rows, null, 'cacheKey');
89+
90+
foreach ($keys as $key) {
91+
if (empty($rows[$key])) {
92+
continue;
93+
}
94+
95+
$row = $rows[$key];
96+
97+
if ($row['cacheExpires'] && ($row['cacheExpires'] < time())) {
98+
continue;
99+
}
100+
101+
try {
102+
$results[$key] = $this->didReadValue(
103+
$row['cacheFormat'],
104+
$row['cacheData']);
105+
} catch (Exception $ex) {
106+
// Treat this as a cache miss.
107+
phlog($ex);
108+
}
109+
}
110+
}
111+
112+
if ($call_id) {
113+
$this->getProfiler()->endServiceCall(
114+
$call_id,
115+
array(
116+
'hits' => array_keys($results),
117+
));
118+
}
119+
120+
return $results;
121+
}
122+
123+
public function deleteKeys(array $keys) {
124+
$call_id = null;
125+
if ($this->getProfiler()) {
126+
$call_id = $this->getProfiler()->beginServiceCall(
127+
array(
128+
'type' => 'kvcache-del',
129+
'name' => 'phabricator-db',
130+
'keys' => $keys,
131+
));
132+
}
133+
134+
if ($keys) {
135+
$map = $this->digestKeys($keys);
136+
queryfx(
137+
$this->establishConnection('w'),
138+
'DELETE FROM %T WHERE cacheKeyHash IN (%Ls)',
139+
$this->getTableName(),
140+
$keys);
141+
}
142+
143+
if ($call_id) {
144+
$this->getProfiler()->endServiceCall($call_id, array());
145+
}
146+
147+
return $this;
148+
}
149+
150+
public function destroyCache() {
151+
queryfx(
152+
$this->establishConnection('w'),
153+
'DELETE FROM %T',
154+
$this->getTableName());
155+
return $this;
156+
}
157+
158+
159+
/* -( Raw Cache Access )--------------------------------------------------- */
160+
161+
162+
public function establishConnection($mode) {
163+
// TODO: This is the only concrete table we have on the database right
164+
// now.
165+
return id(new PhabricatorMarkupCache())->establishConnection($mode);
166+
}
167+
168+
public function getTableName() {
169+
return 'cache_general';
170+
}
171+
172+
173+
/* -( Implementation )----------------------------------------------------- */
174+
175+
176+
private function digestKeys(array $keys) {
177+
$map = array();
178+
foreach ($keys as $key) {
179+
$map[$key] = PhabricatorHash::digestForIndex($key);
180+
}
181+
return $map;
182+
}
183+
184+
private function willWriteValue($key, $value) {
185+
if (!is_string($value)) {
186+
throw new Exception("Only strings may be written to the DB cache!");
187+
}
188+
189+
static $can_deflate;
190+
if ($can_deflate === null) {
191+
$can_deflate = function_exists('gzdeflate') &&
192+
PhabricatorEnv::getEnvConfig('cache.enable-deflate');
193+
}
194+
195+
// If the value is larger than 1KB, we have gzdeflate(), we successfully
196+
// can deflate it, and it benefits from deflation, store it deflated.
197+
if ($can_deflate) {
198+
$len = strlen($value);
199+
if ($len > 1024) {
200+
$deflated = gzdeflate($value);
201+
if ($deflated !== false) {
202+
$deflated_len = strlen($deflated);
203+
if ($deflated_len < ($len / 2)) {
204+
return array(self::CACHE_FORMAT_DEFLATE, $deflated);
205+
}
206+
}
207+
}
208+
}
209+
210+
return array(self::CACHE_FORMAT_RAW, $value);
211+
}
212+
213+
private function didReadValue($format, $value) {
214+
switch ($format) {
215+
case self::CACHE_FORMAT_RAW:
216+
return $value;
217+
case self::CACHE_FORMAT_DEFLATE:
218+
if (!function_exists('gzinflate')) {
219+
throw new Exception("No gzinflate() to read deflated cache.");
220+
}
221+
$value = gzinflate($value);
222+
if ($value === false) {
223+
throw new Exception("Failed to deflate cache.");
224+
}
225+
return $value;
226+
default:
227+
throw new Exception("Unknown cache format.");
228+
}
229+
}
230+
231+
232+
}

‎src/applications/paste/query/PhabricatorPasteQuery.php

+11-13
Original file line numberDiff line numberDiff line change
@@ -124,23 +124,22 @@ private function loadRawContent(array $pastes) {
124124
}
125125

126126
private function loadContent(array $pastes) {
127+
$cache = id(new PhabricatorKeyValueDatabaseCache())
128+
->setProfiler(PhutilServiceProfiler::getInstance());
129+
127130
$keys = array();
128131
foreach ($pastes as $paste) {
129132
$keys[] = $this->getContentCacheKey($paste);
130133
}
131134

132-
// TODO: Move to a more appropriate/general cache once we have one? For
133-
// now, this gets automatic GC.
134-
$caches = id(new PhabricatorMarkupCache())->loadAllWhere(
135-
'cacheKey IN (%Ls)',
136-
$keys);
137-
$caches = mpull($caches, null, 'getCacheKey');
135+
136+
$caches = $cache->getKeys($keys);
138137

139138
$need_raw = array();
140139
foreach ($pastes as $paste) {
141140
$key = $this->getContentCacheKey($paste);
142141
if (isset($caches[$key])) {
143-
$paste->attachContent($caches[$key]->getCacheData());
142+
$paste->attachContent($caches[$key]);
144143
} else {
145144
$need_raw[] = $paste;
146145
}
@@ -150,18 +149,17 @@ private function loadContent(array $pastes) {
150149
return;
151150
}
152151

152+
$write_data = array();
153+
153154
$this->loadRawContent($need_raw);
154155
foreach ($need_raw as $paste) {
155156
$content = $this->buildContent($paste);
156157
$paste->attachContent($content);
157158

158-
$guard = AphrontWriteGuard::beginScopedUnguardedWrites();
159-
id(new PhabricatorMarkupCache())
160-
->setCacheKey($this->getContentCacheKey($paste))
161-
->setCacheData($content)
162-
->replace();
163-
unset($guard);
159+
$write_data[$this->getContentCacheKey($paste)] = (string)$content;
164160
}
161+
162+
$cache->setKeys($write_data);
165163
}
166164

167165

‎src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php

+23
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,15 @@ public function run() {
4949
$n_parse = $this->collectParseCaches();
5050
$n_markup = $this->collectMarkupCaches();
5151
$n_tasks = $this->collectArchivedTasks();
52+
$n_cache = $this->collectGeneralCaches();
5253

5354
$collected = array(
5455
'Herald Transcript' => $n_herald,
5556
'Daemon Log' => $n_daemon,
5657
'Differential Parse Cache' => $n_parse,
5758
'Markup Cache' => $n_markup,
5859
'Archived Tasks' => $n_tasks,
60+
'General Cache Entries' => $n_cache,
5961
);
6062
$collected = array_filter($collected);
6163

@@ -200,4 +202,25 @@ private function collectArchivedTasks() {
200202
return count($task_ids);
201203
}
202204

205+
206+
private function collectGeneralCaches() {
207+
$key = 'gcdaemon.ttl.general-cache';
208+
$ttl = PhabricatorEnv::getEnvConfig($key);
209+
if ($ttl <= 0) {
210+
return 0;
211+
}
212+
213+
$cache = new PhabricatorKeyValueDatabaseCache();
214+
$conn_w = $cache->establishConnection('w');
215+
216+
queryfx(
217+
$conn_w,
218+
'DELETE FROM %T WHERE cacheCreated < %d
219+
ORDER BY cacheCreated ASC LIMIT 100',
220+
$cache->getTableName(),
221+
time() - $ttl);
222+
223+
return $conn_w->getAffectedRows();
224+
}
225+
203226
}

0 commit comments

Comments
 (0)
Failed to load comments.