Skip to content

Commit 27bcefb

Browse files
authored
Merge pull request #129 from clue-labs/caching
Implement `CachingExecutor` using cache TTL, deprecate old `CachedExecutor`
2 parents fbab3df + 1cbf82f commit 27bcefb

File tree

7 files changed

+290
-24
lines changed

7 files changed

+290
-24
lines changed

src/Query/CachedExecutor.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
use React\Dns\Model\Message;
66

7+
/**
8+
* @deprecated unused, exists for BC only
9+
* @see CachingExecutor
10+
*/
711
class CachedExecutor implements ExecutorInterface
812
{
913
private $executor;

src/Query/CachingExecutor.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace React\Dns\Query;
4+
5+
use React\Cache\CacheInterface;
6+
use React\Dns\Model\Message;
7+
use React\Promise\Promise;
8+
9+
class CachingExecutor implements ExecutorInterface
10+
{
11+
/**
12+
* Default TTL for negative responses (NXDOMAIN etc.).
13+
*
14+
* @internal
15+
*/
16+
const TTL = 60;
17+
18+
private $executor;
19+
private $cache;
20+
21+
public function __construct(ExecutorInterface $executor, CacheInterface $cache)
22+
{
23+
$this->executor = $executor;
24+
$this->cache = $cache;
25+
}
26+
27+
public function query($nameserver, Query $query)
28+
{
29+
$id = $query->name . ':' . $query->type . ':' . $query->class;
30+
$cache = $this->cache;
31+
$that = $this;
32+
$executor = $this->executor;
33+
34+
$pending = $cache->get($id);
35+
return new Promise(function ($resolve, $reject) use ($nameserver, $query, $id, $cache, $executor, &$pending, $that) {
36+
$pending->then(
37+
function ($message) use ($nameserver, $query, $id, $cache, $executor, &$pending, $that) {
38+
// return cached response message on cache hit
39+
if ($message !== null) {
40+
return $message;
41+
}
42+
43+
// perform DNS lookup if not already cached
44+
return $pending = $executor->query($nameserver, $query)->then(
45+
function (Message $message) use ($cache, $id, $that) {
46+
// DNS response message received => store in cache when not truncated and return
47+
if (!$message->header->isTruncated()) {
48+
$cache->set($id, $message, $that->ttl($message));
49+
}
50+
51+
return $message;
52+
}
53+
);
54+
}
55+
)->then($resolve, $reject);
56+
}, function ($_, $reject) use (&$pending, $query) {
57+
$reject(new \RuntimeException('DNS query for ' . $query->name . ' has been cancelled'));
58+
$pending->cancel();
59+
});
60+
}
61+
62+
/**
63+
* @param Message $message
64+
* @return int
65+
* @internal
66+
*/
67+
public function ttl(Message $message)
68+
{
69+
// select TTL from answers (should all be the same), use smallest value if available
70+
// @link https://tools.ietf.org/html/rfc2181#section-5.2
71+
$ttl = null;
72+
foreach ($message->answers as $answer) {
73+
if ($ttl === null || $answer->ttl < $ttl) {
74+
$ttl = $answer->ttl;
75+
}
76+
}
77+
78+
if ($ttl === null) {
79+
$ttl = self::TTL;
80+
}
81+
82+
return $ttl;
83+
}
84+
}

src/Query/RecordBag.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
use React\Dns\Model\Record;
66

7+
/**
8+
* @deprecated unused, exists for BC only
9+
* @see CachingExecutor
10+
*/
711
class RecordBag
812
{
913
private $records = array();

src/Query/RecordCache.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
/**
1212
* Wraps an underlying cache interface and exposes only cached DNS data
13+
*
14+
* @deprecated unused, exists for BC only
15+
* @see CachingExecutor
1316
*/
1417
class RecordCache
1518
{

src/Resolver/Factory.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
use React\Cache\ArrayCache;
66
use React\Cache\CacheInterface;
77
use React\Dns\Config\HostsFile;
8-
use React\Dns\Query\CachedExecutor;
8+
use React\Dns\Query\CachingExecutor;
99
use React\Dns\Query\CoopExecutor;
1010
use React\Dns\Query\ExecutorInterface;
1111
use React\Dns\Query\HostsFileExecutor;
12-
use React\Dns\Query\RecordCache;
1312
use React\Dns\Query\RetryExecutor;
1413
use React\Dns\Query\TimeoutExecutor;
1514
use React\Dns\Query\UdpTransportExecutor;
@@ -84,7 +83,7 @@ protected function createRetryExecutor(LoopInterface $loop)
8483

8584
protected function createCachedExecutor(LoopInterface $loop, CacheInterface $cache)
8685
{
87-
return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache($cache));
86+
return new CachingExecutor($this->createRetryExecutor($loop), $cache);
8887
}
8988

9089
protected function addPortToServerIfMissing($nameserver)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
namespace React\Tests\Dns\Query;
4+
5+
use React\Dns\Model\Message;
6+
use React\Dns\Query\CachingExecutor;
7+
use React\Dns\Query\Query;
8+
use React\Promise\Promise;
9+
use React\Tests\Dns\TestCase;
10+
use React\Promise\Deferred;
11+
use React\Dns\Model\Record;
12+
13+
class CachingExecutorTest extends TestCase
14+
{
15+
public function testQueryWillReturnPendingPromiseWhenCacheIsPendingWithoutSendingQueryToFallbackExecutor()
16+
{
17+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
18+
$fallback->expects($this->never())->method('query');
19+
20+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
21+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(new Promise(function () { }));
22+
23+
$executor = new CachingExecutor($fallback, $cache);
24+
25+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
26+
27+
$promise = $executor->query('8.8.8.8', $query);
28+
29+
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
30+
}
31+
32+
public function testQueryWillReturnPendingPromiseWhenCacheReturnsMissAndWillSendSameQueryToFallbackExecutor()
33+
{
34+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
35+
36+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
37+
$fallback->expects($this->once())->method('query')->with('8.8.8.8', $query)->willReturn(new Promise(function () { }));
38+
39+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
40+
$cache->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null));
41+
42+
$executor = new CachingExecutor($fallback, $cache);
43+
44+
$promise = $executor->query('8.8.8.8', $query);
45+
46+
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
47+
}
48+
49+
public function testQueryWillReturnResolvedPromiseWhenCacheReturnsHitWithoutSendingQueryToFallbackExecutor()
50+
{
51+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
52+
$fallback->expects($this->never())->method('query');
53+
54+
$message = new Message();
55+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
56+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve($message));
57+
58+
$executor = new CachingExecutor($fallback, $cache);
59+
60+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
61+
62+
$promise = $executor->query('8.8.8.8', $query);
63+
64+
$promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever());
65+
}
66+
67+
public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesAndSaveMessageToCacheWithMinimumTtlFromRecord()
68+
{
69+
$message = new Message();
70+
$message->answers[] = new Record('reactphp.org', Message::TYPE_A, Message::CLASS_IN, 3700, '127.0.0.1');
71+
$message->answers[] = new Record('reactphp.org', Message::TYPE_A, Message::CLASS_IN, 3600, '127.0.0.1');
72+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
73+
$fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message));
74+
75+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
76+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null));
77+
$cache->expects($this->once())->method('set')->with('reactphp.org:1:1', $message, 3600);
78+
79+
$executor = new CachingExecutor($fallback, $cache);
80+
81+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
82+
83+
$promise = $executor->query('8.8.8.8', $query);
84+
85+
$promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever());
86+
}
87+
88+
public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesAndSaveMessageToCacheWithDefaultTtl()
89+
{
90+
$message = new Message();
91+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
92+
$fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message));
93+
94+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
95+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null));
96+
$cache->expects($this->once())->method('set')->with('reactphp.org:1:1', $message, 60);
97+
98+
$executor = new CachingExecutor($fallback, $cache);
99+
100+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
101+
102+
$promise = $executor->query('8.8.8.8', $query);
103+
104+
$promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever());
105+
}
106+
107+
public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesWithTruncatedResponseButShouldNotSaveTruncatedMessageToCache()
108+
{
109+
$message = new Message();
110+
$message->header->set('tc', 1);
111+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
112+
$fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message));
113+
114+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
115+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null));
116+
$cache->expects($this->never())->method('set');
117+
118+
$executor = new CachingExecutor($fallback, $cache);
119+
120+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
121+
122+
$promise = $executor->query('8.8.8.8', $query);
123+
124+
$promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever());
125+
}
126+
127+
public function testQueryWillReturnRejectedPromiseWhenCacheReturnsMissAndFallbackExecutorRejects()
128+
{
129+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
130+
131+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
132+
$fallback->expects($this->once())->method('query')->willReturn(\React\Promise\reject(new \RuntimeException()));
133+
134+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
135+
$cache->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null));
136+
137+
$executor = new CachingExecutor($fallback, $cache);
138+
139+
$promise = $executor->query('8.8.8.8', $query);
140+
141+
$promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
142+
}
143+
144+
public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromCache()
145+
{
146+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
147+
$fallback->expects($this->never())->method('query');
148+
149+
$pending = new Promise(function () { }, $this->expectCallableOnce());
150+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
151+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn($pending);
152+
153+
$executor = new CachingExecutor($fallback, $cache);
154+
155+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
156+
157+
$promise = $executor->query('8.8.8.8', $query);
158+
$promise->cancel();
159+
160+
$promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
161+
}
162+
163+
public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromFallbackExecutorWhenCacheReturnsMiss()
164+
{
165+
$pending = new Promise(function () { }, $this->expectCallableOnce());
166+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
167+
$fallback->expects($this->once())->method('query')->willReturn($pending);
168+
169+
$deferred = new Deferred();
170+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
171+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn($deferred->promise());
172+
173+
$executor = new CachingExecutor($fallback, $cache);
174+
175+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
176+
177+
$promise = $executor->query('8.8.8.8', $query);
178+
$deferred->resolve(null);
179+
$promise->cancel();
180+
181+
$promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
182+
}
183+
}

tests/Resolver/FactoryTest.php

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public function createWithoutPortShouldCreateResolverWithDefaultPort()
3232
}
3333

3434
/** @test */
35-
public function createCachedShouldCreateResolverWithCachedExecutor()
35+
public function createCachedShouldCreateResolverWithCachingExecutor()
3636
{
3737
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
3838

@@ -41,15 +41,13 @@ public function createCachedShouldCreateResolverWithCachedExecutor()
4141

4242
$this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver);
4343
$executor = $this->getResolverPrivateExecutor($resolver);
44-
$this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor);
45-
$recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache');
46-
$recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache');
47-
$this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache);
48-
$this->assertInstanceOf('React\Cache\ArrayCache', $recordCacheCache);
44+
$this->assertInstanceOf('React\Dns\Query\CachingExecutor', $executor);
45+
$cache = $this->getCachingExecutorPrivateMemberValue($executor, 'cache');
46+
$this->assertInstanceOf('React\Cache\ArrayCache', $cache);
4947
}
5048

5149
/** @test */
52-
public function createCachedShouldCreateResolverWithCachedExecutorWithCustomCache()
50+
public function createCachedShouldCreateResolverWithCachingExecutorWithCustomCache()
5351
{
5452
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
5553
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
@@ -59,11 +57,9 @@ public function createCachedShouldCreateResolverWithCachedExecutorWithCustomCach
5957

6058
$this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver);
6159
$executor = $this->getResolverPrivateExecutor($resolver);
62-
$this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor);
63-
$recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache');
64-
$recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache');
65-
$this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache);
66-
$this->assertSame($cache, $recordCacheCache);
60+
$this->assertInstanceOf('React\Dns\Query\CachingExecutor', $executor);
61+
$cacheProperty = $this->getCachingExecutorPrivateMemberValue($executor, 'cache');
62+
$this->assertSame($cache, $cacheProperty);
6763
}
6864

6965
/**
@@ -115,16 +111,9 @@ private function getResolverPrivateMemberValue($resolver, $field)
115111
return $reflector->getValue($resolver);
116112
}
117113

118-
private function getCachedExecutorPrivateMemberValue($resolver, $field)
114+
private function getCachingExecutorPrivateMemberValue($resolver, $field)
119115
{
120-
$reflector = new \ReflectionProperty('React\Dns\Query\CachedExecutor', $field);
121-
$reflector->setAccessible(true);
122-
return $reflector->getValue($resolver);
123-
}
124-
125-
private function getRecordCachePrivateMemberValue($resolver, $field)
126-
{
127-
$reflector = new \ReflectionProperty('React\Dns\Query\RecordCache', $field);
116+
$reflector = new \ReflectionProperty('React\Dns\Query\CachingExecutor', $field);
128117
$reflector->setAccessible(true);
129118
return $reflector->getValue($resolver);
130119
}

0 commit comments

Comments
 (0)