Skip to content

Commit bfae657

Browse files
authored
Merge pull request #102 from clue-labs/cache
Forward compatibility with upcoming Cache 0.5
2 parents a3712f0 + ef8aaaa commit bfae657

File tree

3 files changed

+118
-7
lines changed

3 files changed

+118
-7
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"license": "MIT",
66
"require": {
77
"php": ">=5.3.0",
8-
"react/cache": "~0.4.0|~0.3.0",
8+
"react/cache": "^0.5 || ^0.4 || ^0.3",
99
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5",
1010
"react/promise": "^2.1 || ^1.2.1",
1111
"react/promise-timer": "^1.2",

src/Query/RecordCache.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
use React\Dns\Model\Message;
77
use React\Dns\Model\Record;
88
use React\Promise;
9+
use React\Promise\PromiseInterface;
910

11+
/**
12+
* Wraps an underlying cache interface and exposes only cached DNS data
13+
*/
1014
class RecordCache
1115
{
1216
private $cache;
@@ -17,6 +21,13 @@ public function __construct(CacheInterface $cache)
1721
$this->cache = $cache;
1822
}
1923

24+
/**
25+
* Looks up the cache if there's a cached answer for the given query
26+
*
27+
* @param Query $query
28+
* @return PromiseInterface Promise<Record[],mixed> resolves with array of Record objects on sucess
29+
* or rejects with mixed values when query is not cached already.
30+
*/
2031
public function lookup(Query $query)
2132
{
2233
$id = $this->serializeQueryToIdentity($query);
@@ -26,8 +37,17 @@ public function lookup(Query $query)
2637
return $this->cache
2738
->get($id)
2839
->then(function ($value) use ($query, $expiredAt) {
40+
// cache 0.5+ resolves with null on cache miss, return explicit cache miss here
41+
if ($value === null) {
42+
return Promise\reject();
43+
}
44+
45+
/* @var $recordBag RecordBag */
2946
$recordBag = unserialize($value);
3047

48+
// reject this cache hit if the query was started before the time we expired the cache?
49+
// todo: this is a legacy left over, this value is never actually set, so this never applies.
50+
// todo: this should probably validate the cache time instead.
3151
if (null !== $expiredAt && $expiredAt <= $query->currentTime) {
3252
return Promise\reject();
3353
}
@@ -36,13 +56,26 @@ public function lookup(Query $query)
3656
});
3757
}
3858

59+
/**
60+
* Stores all records from this response message in the cache
61+
*
62+
* @param int $currentTime
63+
* @param Message $message
64+
* @uses self::storeRecord()
65+
*/
3966
public function storeResponseMessage($currentTime, Message $message)
4067
{
4168
foreach ($message->answers as $record) {
4269
$this->storeRecord($currentTime, $record);
4370
}
4471
}
4572

73+
/**
74+
* Stores a single record from a response message in the cache
75+
*
76+
* @param int $currentTime
77+
* @param Record $record
78+
*/
4679
public function storeRecord($currentTime, Record $record)
4780
{
4881
$id = $this->serializeRecordToIdentity($record);
@@ -53,13 +86,21 @@ public function storeRecord($currentTime, Record $record)
5386
->get($id)
5487
->then(
5588
function ($value) {
89+
if ($value === null) {
90+
// cache 0.5+ cache miss resolves with null, return empty bag here
91+
return new RecordBag();
92+
}
93+
94+
// reuse existing bag on cache hit to append new record to it
5695
return unserialize($value);
5796
},
5897
function ($e) {
98+
// legacy cache < 0.5 cache miss rejects promise, return empty bag here
5999
return new RecordBag();
60100
}
61101
)
62-
->then(function ($recordBag) use ($id, $currentTime, $record, $cache) {
102+
->then(function (RecordBag $recordBag) use ($id, $currentTime, $record, $cache) {
103+
// add a record to the existing (possibly empty) record bag and save to cache
63104
$recordBag->set($currentTime, $record);
64105
$cache->set($id, serialize($recordBag));
65106
});

tests/Query/RecordCacheTest.php

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,93 @@
99
use React\Dns\Query\RecordCache;
1010
use React\Dns\Query\Query;
1111
use React\Promise\PromiseInterface;
12+
use React\Promise\Promise;
1213

1314
class RecordCacheTest extends TestCase
1415
{
1516
/**
16-
* @covers React\Dns\Query\RecordCache
17-
* @test
18-
*/
19-
public function lookupOnEmptyCacheShouldReturnNull()
17+
* @covers React\Dns\Query\RecordCache
18+
* @test
19+
*/
20+
public function lookupOnNewCacheMissShouldReturnNull()
2021
{
2122
$query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
2223

23-
$cache = new RecordCache(new ArrayCache());
24+
$base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
25+
$base->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null));
26+
27+
$cache = new RecordCache($base);
28+
$promise = $cache->lookup($query);
29+
30+
$this->assertInstanceOf('React\Promise\RejectedPromise', $promise);
31+
}
32+
33+
/**
34+
* @covers React\Dns\Query\RecordCache
35+
* @test
36+
*/
37+
public function lookupOnLegacyCacheMissShouldReturnNull()
38+
{
39+
$query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
40+
41+
$base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
42+
$base->expects($this->once())->method('get')->willReturn(\React\Promise\reject());
43+
44+
$cache = new RecordCache($base);
2445
$promise = $cache->lookup($query);
2546

2647
$this->assertInstanceOf('React\Promise\RejectedPromise', $promise);
2748
}
2849

50+
/**
51+
* @covers React\Dns\Query\RecordCache
52+
* @test
53+
*/
54+
public function storeRecordPendingCacheDoesNotSetCache()
55+
{
56+
$query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
57+
$pending = new Promise(function () { });
58+
59+
$base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
60+
$base->expects($this->once())->method('get')->willReturn($pending);
61+
$base->expects($this->never())->method('set');
62+
63+
$cache = new RecordCache($base);
64+
$cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'));
65+
}
66+
67+
/**
68+
* @covers React\Dns\Query\RecordCache
69+
* @test
70+
*/
71+
public function storeRecordOnNewCacheMissSetsCache()
72+
{
73+
$query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
74+
75+
$base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
76+
$base->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null));
77+
$base->expects($this->once())->method('set')->with($this->isType('string'), $this->isType('string'));
78+
79+
$cache = new RecordCache($base);
80+
$cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'));
81+
}
82+
83+
/**
84+
* @covers React\Dns\Query\RecordCache
85+
* @test
86+
*/
87+
public function storeRecordOnOldCacheMissSetsCache()
88+
{
89+
$query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
90+
91+
$base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
92+
$base->expects($this->once())->method('get')->willReturn(\React\Promise\reject());
93+
$base->expects($this->once())->method('set')->with($this->isType('string'), $this->isType('string'));
94+
95+
$cache = new RecordCache($base);
96+
$cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'));
97+
}
98+
2999
/**
30100
* @covers React\Dns\Query\RecordCache
31101
* @test

0 commit comments

Comments
 (0)