From d1125daf8628221ee00885c6215921b788b98940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 25 Jun 2018 12:02:56 +0200 Subject: [PATCH 1/4] Implement CachingExecutor using cache TTL, deprecate old CachedExecutor --- src/Query/CachedExecutor.php | 4 + src/Query/CachingExecutor.php | 50 ++++++++++ src/Query/RecordBag.php | 4 + src/Query/RecordCache.php | 3 + src/Resolver/Factory.php | 5 +- tests/Query/CachingExecutorTest.php | 145 ++++++++++++++++++++++++++++ tests/Resolver/FactoryTest.php | 31 ++---- 7 files changed, 218 insertions(+), 24 deletions(-) create mode 100644 src/Query/CachingExecutor.php create mode 100644 tests/Query/CachingExecutorTest.php diff --git a/src/Query/CachedExecutor.php b/src/Query/CachedExecutor.php index 285936db..8b708943 100644 --- a/src/Query/CachedExecutor.php +++ b/src/Query/CachedExecutor.php @@ -4,6 +4,10 @@ use React\Dns\Model\Message; +/** + * @deprecated unused, exists for BC only + * @see CachingExecutor + */ class CachedExecutor implements ExecutorInterface { private $executor; diff --git a/src/Query/CachingExecutor.php b/src/Query/CachingExecutor.php new file mode 100644 index 00000000..9cb85e26 --- /dev/null +++ b/src/Query/CachingExecutor.php @@ -0,0 +1,50 @@ +executor = $executor; + $this->cache = $cache; + } + + public function query($nameserver, Query $query) + { + $id = $query->name . ':' . $query->type . ':' . $query->class; + $cache = $this->cache; + $executor = $this->executor; + + return $cache->get($id)->then(function ($message) use ($nameserver, $query, $id, $cache, $executor) { + // return cached response message on cache hit + if ($message !== null) { + return $message; + } + + // perform DNS lookup if not already cached + return $executor->query($nameserver, $query)->then( + function (Message $message) use ($cache, $id) { + // DNS response message received => store in cache and return + $cache->set($id, $message, CachingExecutor::TTL); + + return $message; + } + ); + }); + } +} diff --git a/src/Query/RecordBag.php b/src/Query/RecordBag.php index 26007c32..4dc815a9 100644 --- a/src/Query/RecordBag.php +++ b/src/Query/RecordBag.php @@ -4,6 +4,10 @@ use React\Dns\Model\Record; +/** + * @deprecated unused, exists for BC only + * @see CachingExecutor + */ class RecordBag { private $records = array(); diff --git a/src/Query/RecordCache.php b/src/Query/RecordCache.php index 2c541e20..c087e5f0 100644 --- a/src/Query/RecordCache.php +++ b/src/Query/RecordCache.php @@ -10,6 +10,9 @@ /** * Wraps an underlying cache interface and exposes only cached DNS data + * + * @deprecated unused, exists for BC only + * @see CachingExecutor */ class RecordCache { diff --git a/src/Resolver/Factory.php b/src/Resolver/Factory.php index b4a4b096..8d493e23 100644 --- a/src/Resolver/Factory.php +++ b/src/Resolver/Factory.php @@ -5,11 +5,10 @@ use React\Cache\ArrayCache; use React\Cache\CacheInterface; use React\Dns\Config\HostsFile; -use React\Dns\Query\CachedExecutor; +use React\Dns\Query\CachingExecutor; use React\Dns\Query\CoopExecutor; use React\Dns\Query\ExecutorInterface; use React\Dns\Query\HostsFileExecutor; -use React\Dns\Query\RecordCache; use React\Dns\Query\RetryExecutor; use React\Dns\Query\TimeoutExecutor; use React\Dns\Query\UdpTransportExecutor; @@ -84,7 +83,7 @@ protected function createRetryExecutor(LoopInterface $loop) protected function createCachedExecutor(LoopInterface $loop, CacheInterface $cache) { - return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache($cache)); + return new CachingExecutor($this->createRetryExecutor($loop), $cache); } protected function addPortToServerIfMissing($nameserver) diff --git a/tests/Query/CachingExecutorTest.php b/tests/Query/CachingExecutorTest.php new file mode 100644 index 00000000..0b3a2ab0 --- /dev/null +++ b/tests/Query/CachingExecutorTest.php @@ -0,0 +1,145 @@ +getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $fallback->expects($this->never())->method('query'); + + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(new Promise(function () { })); + + $executor = new CachingExecutor($fallback, $cache); + + $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); + + $promise = $executor->query('8.8.8.8', $query); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryWillReturnPendingPromiseWhenCacheReturnsMissAndWillSendSameQueryToFallbackExecutor() + { + $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); + + $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $fallback->expects($this->once())->method('query')->with('8.8.8.8', $query)->willReturn(new Promise(function () { })); + + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $cache->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); + + $executor = new CachingExecutor($fallback, $cache); + + $promise = $executor->query('8.8.8.8', $query); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryWillReturnResolvedPromiseWhenCacheReturnsHitWithoutSendingQueryToFallbackExecutor() + { + $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $fallback->expects($this->never())->method('query'); + + $message = new Message(); + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve($message)); + + $executor = new CachingExecutor($fallback, $cache); + + $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); + + $promise = $executor->query('8.8.8.8', $query); + + $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); + } + + public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesAndSaveMessageToCache() + { + $message = new Message(); + $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message)); + + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null)); + $cache->expects($this->once())->method('set')->with('reactphp.org:1:1', $message, 60); + + $executor = new CachingExecutor($fallback, $cache); + + $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); + + $promise = $executor->query('8.8.8.8', $query); + + $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); + } + + public function testQueryWillReturnRejectedPromiseWhenCacheReturnsMissAndFallbackExecutorRejects() + { + $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); + + $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\reject(new \RuntimeException())); + + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $cache->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); + + $executor = new CachingExecutor($fallback, $cache); + + $promise = $executor->query('8.8.8.8', $query); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromCache() + { + $this->markTestIncomplete(); + + $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $fallback->expects($this->never())->method('query'); + + $pending = new Promise(function () { }, $this->expectCallableOnce()); + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn($pending); + + $executor = new CachingExecutor($fallback, $cache); + + $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); + + $promise = $executor->query('8.8.8.8', $query); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromFallbackExecutorWhenCacheReturnsMiss() + { + $this->markTestIncomplete(); + + $pending = new Promise(function () { }, $this->expectCallableOnce()); + $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $fallback->expects($this->once())->method('query')->willReturn($pending); + + $deferred = new Deferred(); + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn($deferred->promise()); + + $executor = new CachingExecutor($fallback, $cache); + + $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); + + $promise = $executor->query('8.8.8.8', $query); + $deferred->resolve(null); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } +} diff --git a/tests/Resolver/FactoryTest.php b/tests/Resolver/FactoryTest.php index acaeac07..9b1b0f8d 100644 --- a/tests/Resolver/FactoryTest.php +++ b/tests/Resolver/FactoryTest.php @@ -32,7 +32,7 @@ public function createWithoutPortShouldCreateResolverWithDefaultPort() } /** @test */ - public function createCachedShouldCreateResolverWithCachedExecutor() + public function createCachedShouldCreateResolverWithCachingExecutor() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -41,15 +41,13 @@ public function createCachedShouldCreateResolverWithCachedExecutor() $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); $executor = $this->getResolverPrivateExecutor($resolver); - $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor); - $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache'); - $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache'); - $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache); - $this->assertInstanceOf('React\Cache\ArrayCache', $recordCacheCache); + $this->assertInstanceOf('React\Dns\Query\CachingExecutor', $executor); + $cache = $this->getCachingExecutorPrivateMemberValue($executor, 'cache'); + $this->assertInstanceOf('React\Cache\ArrayCache', $cache); } /** @test */ - public function createCachedShouldCreateResolverWithCachedExecutorWithCustomCache() + public function createCachedShouldCreateResolverWithCachingExecutorWithCustomCache() { $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -59,11 +57,9 @@ public function createCachedShouldCreateResolverWithCachedExecutorWithCustomCach $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); $executor = $this->getResolverPrivateExecutor($resolver); - $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor); - $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache'); - $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache'); - $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache); - $this->assertSame($cache, $recordCacheCache); + $this->assertInstanceOf('React\Dns\Query\CachingExecutor', $executor); + $cacheProperty = $this->getCachingExecutorPrivateMemberValue($executor, 'cache'); + $this->assertSame($cache, $cacheProperty); } /** @@ -115,16 +111,9 @@ private function getResolverPrivateMemberValue($resolver, $field) return $reflector->getValue($resolver); } - private function getCachedExecutorPrivateMemberValue($resolver, $field) + private function getCachingExecutorPrivateMemberValue($resolver, $field) { - $reflector = new \ReflectionProperty('React\Dns\Query\CachedExecutor', $field); - $reflector->setAccessible(true); - return $reflector->getValue($resolver); - } - - private function getRecordCachePrivateMemberValue($resolver, $field) - { - $reflector = new \ReflectionProperty('React\Dns\Query\RecordCache', $field); + $reflector = new \ReflectionProperty('React\Dns\Query\CachingExecutor', $field); $reflector->setAccessible(true); return $reflector->getValue($resolver); } From 3bf372fc218468630635a1502d88ecea9b36a244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 3 Jul 2019 14:21:09 +0200 Subject: [PATCH 2/4] Support promise cancellation for CachingExecutor --- src/Query/CachingExecutor.php | 37 ++++++++++++++++++----------- tests/Query/CachingExecutorTest.php | 4 ---- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/Query/CachingExecutor.php b/src/Query/CachingExecutor.php index 9cb85e26..184bf2f7 100644 --- a/src/Query/CachingExecutor.php +++ b/src/Query/CachingExecutor.php @@ -4,6 +4,7 @@ use React\Cache\CacheInterface; use React\Dns\Model\Message; +use React\Promise\Promise; class CachingExecutor implements ExecutorInterface { @@ -30,21 +31,29 @@ public function query($nameserver, Query $query) $cache = $this->cache; $executor = $this->executor; - return $cache->get($id)->then(function ($message) use ($nameserver, $query, $id, $cache, $executor) { - // return cached response message on cache hit - if ($message !== null) { - return $message; - } - - // perform DNS lookup if not already cached - return $executor->query($nameserver, $query)->then( - function (Message $message) use ($cache, $id) { - // DNS response message received => store in cache and return - $cache->set($id, $message, CachingExecutor::TTL); - - return $message; + $pending = $cache->get($id); + return new Promise(function ($resolve, $reject) use ($nameserver, $query, $id, $cache, $executor, &$pending) { + $pending->then( + function ($message) use ($nameserver, $query, $id, $cache, $executor, &$pending) { + // return cached response message on cache hit + if ($message !== null) { + return $message; + } + + // perform DNS lookup if not already cached + return $pending = $executor->query($nameserver, $query)->then( + function (Message $message) use ($cache, $id) { + // DNS response message received => store in cache and return + $cache->set($id, $message, CachingExecutor::TTL); + + return $message; + } + ); } - ); + )->then($resolve, $reject); + }, function ($_, $reject) use (&$pending, $query) { + $reject(new \RuntimeException('DNS query for ' . $query->name . ' has been cancelled')); + $pending->cancel(); }); } } diff --git a/tests/Query/CachingExecutorTest.php b/tests/Query/CachingExecutorTest.php index 0b3a2ab0..53b64d2b 100644 --- a/tests/Query/CachingExecutorTest.php +++ b/tests/Query/CachingExecutorTest.php @@ -101,8 +101,6 @@ public function testQueryWillReturnRejectedPromiseWhenCacheReturnsMissAndFallbac public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromCache() { - $this->markTestIncomplete(); - $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); $fallback->expects($this->never())->method('query'); @@ -122,8 +120,6 @@ public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseF public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromFallbackExecutorWhenCacheReturnsMiss() { - $this->markTestIncomplete(); - $pending = new Promise(function () { }, $this->expectCallableOnce()); $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); $fallback->expects($this->once())->method('query')->willReturn($pending); From 41229e75868d7474e593f0df057d6c337d1c22f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 8 Jul 2019 12:23:06 +0200 Subject: [PATCH 3/4] Do not cache truncated response messages as per RFC See https://tools.ietf.org/html/rfc1035#section-7.4 --- src/Query/CachingExecutor.php | 6 ++++-- tests/Query/CachingExecutorTest.php | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Query/CachingExecutor.php b/src/Query/CachingExecutor.php index 184bf2f7..c585d855 100644 --- a/src/Query/CachingExecutor.php +++ b/src/Query/CachingExecutor.php @@ -43,8 +43,10 @@ function ($message) use ($nameserver, $query, $id, $cache, $executor, &$pending) // perform DNS lookup if not already cached return $pending = $executor->query($nameserver, $query)->then( function (Message $message) use ($cache, $id) { - // DNS response message received => store in cache and return - $cache->set($id, $message, CachingExecutor::TTL); + // DNS response message received => store in cache when not truncated and return + if (!$message->header->isTruncated()) { + $cache->set($id, $message, CachingExecutor::TTL); + } return $message; } diff --git a/tests/Query/CachingExecutorTest.php b/tests/Query/CachingExecutorTest.php index 53b64d2b..2032e962 100644 --- a/tests/Query/CachingExecutorTest.php +++ b/tests/Query/CachingExecutorTest.php @@ -82,6 +82,26 @@ public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbac $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); } + public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesWithTruncatedResponseButShouldNotSaveTruncatedMessageToCache() + { + $message = new Message(); + $message->header->set('tc', 1); + $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message)); + + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null)); + $cache->expects($this->never())->method('set'); + + $executor = new CachingExecutor($fallback, $cache); + + $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); + + $promise = $executor->query('8.8.8.8', $query); + + $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); + } + public function testQueryWillReturnRejectedPromiseWhenCacheReturnsMissAndFallbackExecutorRejects() { $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); From 1cbf82fa76efd5c92f955e0d3c12d54e61333ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 8 Jul 2019 16:35:36 +0200 Subject: [PATCH 4/4] Respect record TTL values from RRset for cache TTL --- src/Query/CachingExecutor.php | 35 ++++++++++++++++++++++++----- tests/Query/CachingExecutorTest.php | 24 +++++++++++++++++++- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/Query/CachingExecutor.php b/src/Query/CachingExecutor.php index c585d855..15e486a2 100644 --- a/src/Query/CachingExecutor.php +++ b/src/Query/CachingExecutor.php @@ -9,8 +9,7 @@ class CachingExecutor implements ExecutorInterface { /** - * Initial implementation uses a fixed TTL for postive DNS responses as well - * as negative responses (NXDOMAIN etc.). + * Default TTL for negative responses (NXDOMAIN etc.). * * @internal */ @@ -29,12 +28,13 @@ public function query($nameserver, Query $query) { $id = $query->name . ':' . $query->type . ':' . $query->class; $cache = $this->cache; + $that = $this; $executor = $this->executor; $pending = $cache->get($id); - return new Promise(function ($resolve, $reject) use ($nameserver, $query, $id, $cache, $executor, &$pending) { + return new Promise(function ($resolve, $reject) use ($nameserver, $query, $id, $cache, $executor, &$pending, $that) { $pending->then( - function ($message) use ($nameserver, $query, $id, $cache, $executor, &$pending) { + function ($message) use ($nameserver, $query, $id, $cache, $executor, &$pending, $that) { // return cached response message on cache hit if ($message !== null) { return $message; @@ -42,10 +42,10 @@ function ($message) use ($nameserver, $query, $id, $cache, $executor, &$pending) // perform DNS lookup if not already cached return $pending = $executor->query($nameserver, $query)->then( - function (Message $message) use ($cache, $id) { + function (Message $message) use ($cache, $id, $that) { // DNS response message received => store in cache when not truncated and return if (!$message->header->isTruncated()) { - $cache->set($id, $message, CachingExecutor::TTL); + $cache->set($id, $message, $that->ttl($message)); } return $message; @@ -58,4 +58,27 @@ function (Message $message) use ($cache, $id) { $pending->cancel(); }); } + + /** + * @param Message $message + * @return int + * @internal + */ + public function ttl(Message $message) + { + // select TTL from answers (should all be the same), use smallest value if available + // @link https://tools.ietf.org/html/rfc2181#section-5.2 + $ttl = null; + foreach ($message->answers as $answer) { + if ($ttl === null || $answer->ttl < $ttl) { + $ttl = $answer->ttl; + } + } + + if ($ttl === null) { + $ttl = self::TTL; + } + + return $ttl; + } } diff --git a/tests/Query/CachingExecutorTest.php b/tests/Query/CachingExecutorTest.php index 2032e962..abd9342a 100644 --- a/tests/Query/CachingExecutorTest.php +++ b/tests/Query/CachingExecutorTest.php @@ -8,6 +8,7 @@ use React\Promise\Promise; use React\Tests\Dns\TestCase; use React\Promise\Deferred; +use React\Dns\Model\Record; class CachingExecutorTest extends TestCase { @@ -63,7 +64,28 @@ public function testQueryWillReturnResolvedPromiseWhenCacheReturnsHitWithoutSend $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); } - public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesAndSaveMessageToCache() + public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesAndSaveMessageToCacheWithMinimumTtlFromRecord() + { + $message = new Message(); + $message->answers[] = new Record('reactphp.org', Message::TYPE_A, Message::CLASS_IN, 3700, '127.0.0.1'); + $message->answers[] = new Record('reactphp.org', Message::TYPE_A, Message::CLASS_IN, 3600, '127.0.0.1'); + $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message)); + + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null)); + $cache->expects($this->once())->method('set')->with('reactphp.org:1:1', $message, 3600); + + $executor = new CachingExecutor($fallback, $cache); + + $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); + + $promise = $executor->query('8.8.8.8', $query); + + $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); + } + + public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesAndSaveMessageToCacheWithDefaultTtl() { $message = new Message(); $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();