Skip to content

Commit 29a86ab

Browse files
committed
fix: index memory usage and LRU based memory eviction
1 parent 8b1bba9 commit 29a86ab

14 files changed

Lines changed: 988 additions & 276 deletions
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
--TEST--
2+
FPM: LRU eviction works per-partition and never wipes the pool cache
3+
--SKIPIF--
4+
<?php include __DIR__ . '/skipif.inc'; ?>
5+
--FILE--
6+
<?php
7+
8+
require_once __DIR__ . '/tester.inc';
9+
10+
$cfg = <<<EOT
11+
[global]
12+
error_log = {{FILE:LOG}}
13+
[opcache]
14+
listen = {{ADDR}}
15+
pm = static
16+
pm.max_children = 1
17+
pm.max_requests = 0
18+
catch_workers_output = yes
19+
EOT;
20+
21+
$code = <<<'PHP'
22+
<?php
23+
$cache = UserCache\Cache::getPool('p');
24+
$blob = str_repeat('x', 8192);
25+
$action = $_GET['action'] ?? 'fill';
26+
27+
if ($action === 'fill') {
28+
$stored = 0;
29+
for ($i = 0; $i < 476; $i++) {
30+
if (!$cache->store('k' . $i, $blob . $i)) {
31+
break;
32+
}
33+
$stored++;
34+
}
35+
var_dump($stored > 400);
36+
echo "fill\n";
37+
return;
38+
}
39+
40+
if ($action === 'touch') {
41+
$alive = 0;
42+
for ($i = 0; $i < 10; $i++) {
43+
if ($cache->fetch('k' . $i) !== null) {
44+
$alive++;
45+
}
46+
}
47+
var_dump($alive === 10);
48+
echo "touch\n";
49+
return;
50+
}
51+
52+
$ok = 0;
53+
for ($i = 0; $i < 30; $i++) {
54+
if ($cache->store('new' . $i, $blob . 'n' . $i)) {
55+
$ok++;
56+
}
57+
}
58+
$touched = 0;
59+
for ($i = 0; $i < 10; $i++) {
60+
if ($cache->fetch('k' . $i) !== null) {
61+
$touched++;
62+
}
63+
}
64+
$status = UserCache\Cache::getStatus();
65+
var_dump($ok === 30);
66+
var_dump($touched === 10);
67+
var_dump($status->getEntryCount() > 400);
68+
var_dump($status->getEvictionCount() > 0);
69+
var_dump($status->getExpungeCount() === 0);
70+
echo "pressure\n";
71+
PHP;
72+
73+
$tester = new FPM\Tester($cfg, $code);
74+
$tester->start(iniEntries: [
75+
'opcache.enable' => '1',
76+
'user_cache.shm_size' => '4M',
77+
]);
78+
$tester->expectLogStartNotices();
79+
80+
$tester->request(query: 'action=fill')->expectBody("bool(true)\nfill");
81+
82+
/* A later request re-stamps a slice of old keys with a fresh request clock. */
83+
sleep(1);
84+
$tester->request(query: 'action=touch')->expectBody("bool(true)\ntouch");
85+
86+
sleep(1);
87+
$tester->request(query: 'action=pressure')->expectBody(
88+
"bool(true)\n" .
89+
"bool(true)\n" .
90+
"bool(true)\n" .
91+
"bool(true)\n" .
92+
"bool(true)\n" .
93+
"pressure"
94+
);
95+
96+
$tester->terminate();
97+
$tester->expectLogTerminatingNotices();
98+
$tester->close();
99+
100+
?>
101+
--EXPECT--
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
UserCache\Cache: user_cache.entries_hint sizes the entry table
3+
--INI--
4+
user_cache.enable=1
5+
user_cache.enable_cli=1
6+
opcache.file_cache_only=0
7+
user_cache.shm_size=16M
8+
user_cache.entries_hint=1000
9+
--FILE--
10+
<?php
11+
/* capacity = next_prime(ceil(hint / 0.75)) = next_prime(1334) */
12+
var_dump(UserCache\Cache::getStatus()->getEntryCapacity());
13+
14+
/* Auto sizing (hint 0): one expected entry per 2KB of segment. */
15+
$php = escapeshellarg(getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY);
16+
$args = '-n -d user_cache.enable=1 -d user_cache.enable_cli=1 -d user_cache.shm_size=16M';
17+
$code = 'echo UserCache\\Cache::getStatus()->getEntryCapacity();';
18+
echo shell_exec("$php $args -d user_cache.entries_hint=0 -r " . escapeshellarg($code)), "\n";
19+
20+
/* A negative hint is rejected at INI time and falls back to auto. */
21+
echo shell_exec("$php $args -d user_cache.entries_hint=-1 -r " . escapeshellarg($code) . " 2>&1"), "\n";
22+
23+
/* A hint the segment cannot index is clamped with a warning. */
24+
echo shell_exec("$php $args -d user_cache.entries_hint=16777213 -r " . escapeshellarg($code) . " 2>&1"), "\n";
25+
?>
26+
--EXPECTF--
27+
int(1361)
28+
10937
29+
%Auser_cache.entries_hint must be greater than or equal to 0, -1 given%A10937
30+
%Auser_cache.entries_hint (16777213) exceeds what user_cache.shm_size can index; clamping capacity to %d%A
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
UserCache\Cache: LRU eviction skips entries whose key is under an active lock
3+
--INI--
4+
user_cache.enable=1
5+
user_cache.enable_cli=1
6+
opcache.file_cache_only=0
7+
user_cache.shm_size=4M
8+
--FILE--
9+
<?php
10+
$cache = UserCache\Cache::getPool('lockskip');
11+
$blob = str_repeat('x', 8192);
12+
13+
for ($i = 0; $i < 476; $i++) {
14+
if (!$cache->store('k' . $i, $blob . $i)) {
15+
break;
16+
}
17+
}
18+
19+
/* Hold a per-key lock on one cold entry. */
20+
var_dump($cache->lock('k3'));
21+
22+
/* Advance the coarse access clock, then churn far past the old cohort so
23+
* every unlocked old entry becomes an eviction victim. */
24+
sleep(1);
25+
$cache->store('clock', 'tick', 60);
26+
for ($i = 0; $i < 600; $i++) {
27+
$cache->store('new' . $i, $blob . 'n' . $i);
28+
}
29+
30+
/* The locked key survived the full churn; its unlocked neighbors did not. */
31+
var_dump($cache->fetch('k3') !== null);
32+
var_dump($cache->fetch('k2', 'MISS') === 'MISS');
33+
var_dump($cache->fetch('k4', 'MISS') === 'MISS');
34+
var_dump(UserCache\Cache::getStatus()->getExpungeCount() === 0);
35+
36+
var_dump($cache->unlock('k3'));
37+
?>
38+
--EXPECT--
39+
bool(true)
40+
bool(true)
41+
bool(true)
42+
bool(true)
43+
bool(true)
44+
bool(true)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
--TEST--
2+
UserCache\Cache: LRU eviction under memory pressure keeps recent entries and never wipes
3+
--INI--
4+
user_cache.enable=1
5+
user_cache.enable_cli=1
6+
opcache.file_cache_only=0
7+
user_cache.shm_size=4M
8+
--FILE--
9+
<?php
10+
$cache = UserCache\Cache::getPool('lru');
11+
$blob = str_repeat('x', 8192);
12+
13+
/* Fill the data region completely (no pressure yet). */
14+
$stored = 0;
15+
for ($i = 0; $i < 476; $i++) {
16+
if (!$cache->store('k' . $i, $blob . $i)) {
17+
break;
18+
}
19+
$stored++;
20+
}
21+
var_dump($stored > 400);
22+
23+
/* Advance the coarse access clock to the next second; the TTL'd store
24+
* refreshes the per-request clock memo through its real time() read. */
25+
sleep(1);
26+
$cache->store('clock', 'tick', 60);
27+
28+
/* Re-stamp a slice of old keys. */
29+
for ($i = 0; $i < 10; $i++) {
30+
$cache->fetch('k' . $i);
31+
}
32+
33+
/* Force eviction pressure. */
34+
$pressured_ok = 0;
35+
for ($i = 0; $i < 30; $i++) {
36+
if ($cache->store('new' . $i, $blob . 'n' . $i)) {
37+
$pressured_ok++;
38+
}
39+
}
40+
var_dump($pressured_ok === 30);
41+
42+
$touched_alive = 0;
43+
for ($i = 0; $i < 10; $i++) {
44+
if ($cache->fetch('k' . $i) !== null) {
45+
$touched_alive++;
46+
}
47+
}
48+
$new_alive = 0;
49+
for ($i = 0; $i < 30; $i++) {
50+
if ($cache->fetch('new' . $i) !== null) {
51+
$new_alive++;
52+
}
53+
}
54+
55+
$status = UserCache\Cache::getStatus();
56+
var_dump($touched_alive === 10);
57+
var_dump($new_alive === 30);
58+
/* Cache stays nearly full: eviction is targeted, not a wipe. */
59+
var_dump($status->getEntryCount() > 400);
60+
var_dump($status->getEvictionCount() > 0);
61+
var_dump($status->getExpungeCount() === 0);
62+
var_dump($status->getStoreFailureCount() === 0);
63+
?>
64+
--EXPECT--
65+
bool(true)
66+
bool(true)
67+
bool(true)
68+
bool(true)
69+
bool(true)
70+
bool(true)
71+
bool(true)
72+
bool(true)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
UserCache\Cache: user_cache.eviction_policy selects lru, clear or none behavior
3+
--INI--
4+
user_cache.enable=1
5+
user_cache.enable_cli=1
6+
opcache.file_cache_only=0
7+
user_cache.shm_size=4M
8+
--FILE--
9+
<?php
10+
$php = escapeshellarg(getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY);
11+
$args = '-n -d user_cache.enable=1 -d user_cache.enable_cli=1 -d user_cache.shm_size=4M';
12+
$code = <<<'CODE'
13+
$cache = UserCache\Cache::getPool('p');
14+
$blob = str_repeat('x', 8192);
15+
$ok = 0;
16+
for ($i = 0; $i < 600; $i++) {
17+
if ($cache->store('k' . $i, $blob . $i)) {
18+
$ok++;
19+
}
20+
}
21+
$status = UserCache\Cache::getStatus();
22+
printf(
23+
"stored=%d full=%d evicted=%d wiped=%d failed=%d\n",
24+
$ok,
25+
(int) ($status->getEntryCount() > 400),
26+
(int) ($status->getEvictionCount() > 0),
27+
(int) ($status->getExpungeCount() > 0),
28+
(int) ($status->getStoreFailureCount() > 0)
29+
);
30+
CODE;
31+
32+
foreach (['lru', 'clear', 'none'] as $policy) {
33+
echo $policy, ': ', shell_exec(
34+
"$php $args -d user_cache.eviction_policy=$policy -r " . escapeshellarg($code)
35+
);
36+
}
37+
38+
/* Invalid values are rejected at INI time and fall back to the lru default. */
39+
echo shell_exec("$php $args -d user_cache.eviction_policy=bogus -r " . escapeshellarg('echo "rejected\n";') . " 2>&1");
40+
?>
41+
--EXPECTF--
42+
lru: stored=600 full=1 evicted=1 wiped=0 failed=0
43+
clear: stored=600 full=0 evicted=0 wiped=1 failed=0
44+
none: stored=%d full=1 evicted=0 wiped=0 failed=1
45+
%Auser_cache.eviction_policy must be one of "lru", "clear" or "none"%Arejected

ext/user_cache/tests/user_cache_info_counters.phpt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ user_cache.enable=1
55
user_cache.enable_cli=1
66
opcache.file_cache_only=0
77
user_cache.shm_size=2M
8+
user_cache.eviction_policy=clear
89
--FILE--
910
<?php
1011
$cache = UserCache\Cache::getPool('counters');

0 commit comments

Comments
 (0)