Skip to content

Commit d1125da

Browse files
committed
Implement CachingExecutor using cache TTL, deprecate old CachedExecutor
1 parent fbab3df commit d1125da

7 files changed

Lines changed: 218 additions & 24 deletions

File tree

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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace React\Dns\Query;
4+
5+
use React\Cache\CacheInterface;
6+
use React\Dns\Model\Message;
7+
8+
class CachingExecutor implements ExecutorInterface
9+
{
10+
/**
11+
* Initial implementation uses a fixed TTL for postive DNS responses as well
12+
* as 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+
$executor = $this->executor;
32+
33+
return $cache->get($id)->then(function ($message) use ($nameserver, $query, $id, $cache, $executor) {
34+
// return cached response message on cache hit
35+
if ($message !== null) {
36+
return $message;
37+
}
38+
39+
// perform DNS lookup if not already cached
40+
return $executor->query($nameserver, $query)->then(
41+
function (Message $message) use ($cache, $id) {
42+
// DNS response message received => store in cache and return
43+
$cache->set($id, $message, CachingExecutor::TTL);
44+
45+
return $message;
46+
}
47+
);
48+
});
49+
}
50+
}

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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
12+
class CachingExecutorTest extends TestCase
13+
{
14+
public function testQueryWillReturnPendingPromiseWhenCacheIsPendingWithoutSendingQueryToFallbackExecutor()
15+
{
16+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
17+
$fallback->expects($this->never())->method('query');
18+
19+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
20+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(new Promise(function () { }));
21+
22+
$executor = new CachingExecutor($fallback, $cache);
23+
24+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
25+
26+
$promise = $executor->query('8.8.8.8', $query);
27+
28+
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
29+
}
30+
31+
public function testQueryWillReturnPendingPromiseWhenCacheReturnsMissAndWillSendSameQueryToFallbackExecutor()
32+
{
33+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
34+
35+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
36+
$fallback->expects($this->once())->method('query')->with('8.8.8.8', $query)->willReturn(new Promise(function () { }));
37+
38+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
39+
$cache->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null));
40+
41+
$executor = new CachingExecutor($fallback, $cache);
42+
43+
$promise = $executor->query('8.8.8.8', $query);
44+
45+
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
46+
}
47+
48+
public function testQueryWillReturnResolvedPromiseWhenCacheReturnsHitWithoutSendingQueryToFallbackExecutor()
49+
{
50+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
51+
$fallback->expects($this->never())->method('query');
52+
53+
$message = new Message();
54+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
55+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve($message));
56+
57+
$executor = new CachingExecutor($fallback, $cache);
58+
59+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
60+
61+
$promise = $executor->query('8.8.8.8', $query);
62+
63+
$promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever());
64+
}
65+
66+
public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesAndSaveMessageToCache()
67+
{
68+
$message = new Message();
69+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
70+
$fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message));
71+
72+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
73+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null));
74+
$cache->expects($this->once())->method('set')->with('reactphp.org:1:1', $message, 60);
75+
76+
$executor = new CachingExecutor($fallback, $cache);
77+
78+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
79+
80+
$promise = $executor->query('8.8.8.8', $query);
81+
82+
$promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever());
83+
}
84+
85+
public function testQueryWillReturnRejectedPromiseWhenCacheReturnsMissAndFallbackExecutorRejects()
86+
{
87+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
88+
89+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
90+
$fallback->expects($this->once())->method('query')->willReturn(\React\Promise\reject(new \RuntimeException()));
91+
92+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
93+
$cache->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null));
94+
95+
$executor = new CachingExecutor($fallback, $cache);
96+
97+
$promise = $executor->query('8.8.8.8', $query);
98+
99+
$promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
100+
}
101+
102+
public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromCache()
103+
{
104+
$this->markTestIncomplete();
105+
106+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
107+
$fallback->expects($this->never())->method('query');
108+
109+
$pending = new Promise(function () { }, $this->expectCallableOnce());
110+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
111+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn($pending);
112+
113+
$executor = new CachingExecutor($fallback, $cache);
114+
115+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
116+
117+
$promise = $executor->query('8.8.8.8', $query);
118+
$promise->cancel();
119+
120+
$promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
121+
}
122+
123+
public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromFallbackExecutorWhenCacheReturnsMiss()
124+
{
125+
$this->markTestIncomplete();
126+
127+
$pending = new Promise(function () { }, $this->expectCallableOnce());
128+
$fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
129+
$fallback->expects($this->once())->method('query')->willReturn($pending);
130+
131+
$deferred = new Deferred();
132+
$cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
133+
$cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn($deferred->promise());
134+
135+
$executor = new CachingExecutor($fallback, $cache);
136+
137+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
138+
139+
$promise = $executor->query('8.8.8.8', $query);
140+
$deferred->resolve(null);
141+
$promise->cancel();
142+
143+
$promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
144+
}
145+
}

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)