Skip to content

Commit c018263

Browse files
author
Robert Bittle
authored
Merge pull request #24 from guywithnose/master
TCG-2460 - Automatically refresh frequently used keys before they expire
2 parents 0e8a29e + 3114e9b commit c018263

File tree

2 files changed

+87
-11
lines changed

2 files changed

+87
-11
lines changed

src/Predis.php

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,37 @@ class Predis implements Memoize
2323
*/
2424
private $refresh;
2525

26+
/**
27+
* The percentage of requests that check for refreshes
28+
*
29+
* @var int
30+
*/
31+
private $refreshPercent;
32+
33+
/**
34+
* The multiplier to use on runtime when deciding to do a refresh
35+
*
36+
* @var float
37+
*/
38+
private $runtimeMultiplier;
39+
2640
/**
2741
* Sets the predis client.
2842
*
29-
* @param ClientInterface $client The predis client to use
30-
* @param boolean $refresh If true we will always overwrite cache even if it is already set
43+
* @param ClientInterface $client The predis client to use
44+
* @param boolean $refresh If true we will always overwrite cache even if it is already set
45+
* @param int $refreshPercent The percentage of requests that check for refreshes
3146
*/
32-
public function __construct(ClientInterface $client, bool $refresh = false)
33-
{
47+
public function __construct(
48+
ClientInterface $client,
49+
bool $refresh = false,
50+
int $refreshPercent = 0,
51+
float $runtimeMultiplier = 3
52+
) {
3453
$this->client = $client;
3554
$this->refresh = $refresh;
55+
$this->refreshPercent = $refreshPercent;
56+
$this->runtimeMultiplier = $runtimeMultiplier;
3657
}
3758

3859
/**
@@ -51,19 +72,37 @@ public function memoizeCallable(string $key, callable $compute, int $cacheTime =
5172
{
5273
if (!$this->refresh) {
5374
try {
75+
if (rand(1, 100) <= $this->refreshPercent) {
76+
// {$refreshPercent}% of requests should check to see if this key is almost expired.
77+
// We don't want to check this on every call to preserve performance.
78+
// Also, this functionality is only important for requests that have many concurrent calls.
79+
$runtime = $this->client->get("{$key}.runtime");
80+
$ttl = $this->client->pttl($key) / 1000;
81+
if ($runtime && $runtime * $this->runtimeMultiplier > $ttl) {
82+
return $this->getData($key, $compute, $cacheTime);
83+
}
84+
}
85+
5486
$cached = $this->client->get($key);
5587
if ($cached !== null) {
5688
$data = json_decode($cached, true);
5789
return $data['result'];
5890
}
5991
} catch (\Exception $e) {
60-
return call_user_func($compute);
92+
return $this->getData($key, $compute, $cacheTime);
6193
}
6294
}
6395

96+
return $this->getData($key, $compute, $cacheTime);
97+
}
98+
99+
private function getData(string $key, callable $compute, int $cacheTime = null)
100+
{
101+
$start = microtime(true);
64102
$result = call_user_func($compute);
103+
$runtime = microtime(true) - $start;
65104

66-
$this->cache($key, json_encode(['result' => $result]), $cacheTime);
105+
$this->cache($key, json_encode(['result' => $result]), $cacheTime, $runtime);
67106

68107
return $result;
69108
}
@@ -77,10 +116,11 @@ public function memoizeCallable(string $key, callable $compute, int $cacheTime =
77116
*
78117
* @return void
79118
*/
80-
private function cache(string $key, string $value, int $cacheTime = null)
119+
private function cache(string $key, string $value, int $cacheTime = null, float $runtime)
81120
{
82121
try {
83122
$this->client->set($key, $value);
123+
$this->client->set("{$key}.runtime", $runtime);
84124

85125
if ($cacheTime !== null) {
86126
$this->client->expire($key, $cacheTime);

tests/PredisTest.php

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,39 @@ public function memoizeCallableWithExceptionOnGet()
6969
$this->assertSame(1, $count);
7070
}
7171

72+
/**
73+
* @test
74+
* @covers ::__construct
75+
* @covers ::memoizeCallable
76+
*/
77+
public function memoizeCallableRefreshRequest()
78+
{
79+
$count = 0;
80+
$key = 'foo';
81+
$value = 'bar';
82+
$cachedValue = json_encode(['result' => $value]);
83+
$cacheTime = 1234;
84+
$compute = function () use (&$count, $value) {
85+
$count++;
86+
87+
return $value;
88+
};
89+
90+
$client = $this->getPredisMock();
91+
$client->expects($this->at(0))->method('get')->with(
92+
$this->equalTo("{$key}.runtime")
93+
)->will($this->returnValue(.2));
94+
$client->expects($this->at(1))->method('pttl')->with($this->equalTo($key))->will($this->returnValue(10));
95+
$client->expects($this->at(2))->method('set')->with($this->equalTo($key), $this->equalTo($cachedValue));
96+
$client->expects($this->at(3))->method('set')->with($this->equalTo("{$key}.runtime"), $this->lessThan(1));
97+
$client->expects($this->at(4))->method('expire')->with($this->equalTo($key), $this->equalTo($cacheTime));
98+
99+
$memoizer = new Predis($client, false, 100);
100+
101+
$this->assertSame($value, $memoizer->memoizeCallable($key, $compute, $cacheTime));
102+
$this->assertSame(1, $count);
103+
}
104+
72105
/**
73106
* @test
74107
* @covers ::__construct
@@ -88,9 +121,10 @@ public function memoizeCallableWithUncachedKey()
88121
};
89122

90123
$client = $this->getPredisMock();
91-
$client->expects($this->once())->method('get')->with($this->equalTo($key))->will($this->returnValue(null));
92-
$client->expects($this->once())->method('set')->with($this->equalTo($key), $this->equalTo($cachedValue));
93-
$client->expects($this->once())->method('expire')->with($this->equalTo($key), $this->equalTo($cacheTime));
124+
$client->expects($this->at(0))->method('get')->with($this->equalTo($key))->will($this->returnValue(null));
125+
$client->expects($this->at(1))->method('set')->with($this->equalTo($key), $this->equalTo($cachedValue));
126+
$client->expects($this->at(2))->method('set')->with($this->equalTo("{$key}.runtime"), $this->lessThan(1));
127+
$client->expects($this->at(3))->method('expire')->with($this->equalTo($key), $this->equalTo($cacheTime));
94128

95129
$memoizer = new Predis($client);
96130

@@ -132,6 +166,8 @@ public function memoizeCallableWithUncachedKeyWithExceptionOnSet()
132166

133167
private function getPredisMock() : ClientInterface
134168
{
135-
return $this->getMockBuilder('\Predis\Client')->setMethods(['get', 'set', 'expire'])->getMock();
169+
return $this->getMockBuilder('\Predis\Client')->setMethods(
170+
['get', 'set', 'expire', 'del', 'exec', 'unwatch', 'pttl']
171+
)->getMock();
136172
}
137173
}

0 commit comments

Comments
 (0)