Repository: mageddo/dns-proxy-server
Pull request #396 (2023-03-23 01:11:20)

Cache improvement for a concurrent approach synchronized by key
Relates to #395 


Patch:
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 945526ef..78b839f8 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -4,6 +4,8 @@
 * Defined a new process of releasing the stable versions, see "Getting Started -> Release Version Control" 
 for more details
 * Fixed arm64 images stopped being pushed at `3.19`
+* Cache improvement for a concurrent approach synchronized by key
+* Increasing tcp and udp server parallelism from 20 to 50 threads each, threads are created lazilly
 
 ## 3.14.5
 * Specify minimum required docker api version `1.21` (as DPS 2)
diff --git a/build.gradle b/build.gradle
index cb95a9f1..c1e643f5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -36,7 +36,7 @@ dependencies {
   implementation 'org.graalvm.sdk:graal-sdk:22.3.1'
   implementation('jakarta.enterprise:jakarta.enterprise.cdi-api:2.0.2')
   implementation('jakarta.ws.rs:jakarta.ws.rs-api:2.1.6')
-  implementation('com.mageddo.commons:commons-lang:0.1.12')
+  implementation('com.mageddo.commons:commons-lang:0.1.15')
   implementation('org.apache.commons:commons-exec:1.3')
 
   implementation('ch.qos.logback:logback-classic:1.4.5')
diff --git a/gradle.properties b/gradle.properties
index b33d3d6d..32fe7907 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,1 +1,1 @@
-version=3.15.2-snapshot
+version=3.15.3-snapshot
diff --git a/src/main/java/com/mageddo/commons/io/IoUtils.java b/src/main/java/com/mageddo/commons/io/IoUtils.java
deleted file mode 100644
index 8b354606..00000000
--- a/src/main/java/com/mageddo/commons/io/IoUtils.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.mageddo.commons.io;
-
-import com.mageddo.dnsproxyserver.config.entrypoint.ConfigProps;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.util.Properties;
-
-public class IoUtils {
-
-  private IoUtils() {
-  }
-
-  public static void silentClose(Closeable c) {
-    try {
-      if (c != null) {
-        c.close();
-      }
-    } catch (IOException e) {
-    }
-  }
-
-  public static Properties loadPropertiesFromResources(String path) {
-    final var in = getResourceAsStream(path);
-    final var properties = new Properties();
-    try {
-      properties.load(in);
-      return properties;
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-
-  public static InputStream getResourceAsStream(String path) {
-    return ConfigProps.class.getResourceAsStream(path);
-  }
-}
diff --git a/src/main/java/com/mageddo/dnsproxyserver/config/entrypoint/LogLevel.java b/src/main/java/com/mageddo/dnsproxyserver/config/entrypoint/LogLevel.java
index 00d9eacd..446dce90 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/config/entrypoint/LogLevel.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/config/entrypoint/LogLevel.java
@@ -9,6 +9,7 @@ public enum LogLevel {
   WARNING("WARN"),
   INFO,
   DEBUG,
+  TRACE,
   ;
 
   private final String slf4jName;
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/Messages.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/Messages.java
index d8c6eed8..a4ac3be4 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/Messages.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/Messages.java
@@ -1,9 +1,11 @@
 package com.mageddo.dnsproxyserver.server.dns;
 
+import com.mageddo.commons.lang.Objects;
 import com.mageddo.dnsproxyserver.config.Config.Entry;
 import com.mageddo.dnsproxyserver.server.dns.solver.Response;
 import com.mageddo.dnsproxyserver.utils.Ips;
 import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
 import org.xbill.DNS.AAAARecord;
 import org.xbill.DNS.ARecord;
 import org.xbill.DNS.CNAMERecord;
@@ -19,6 +21,7 @@ import org.xbill.DNS.Type;
 import java.time.Duration;
 import java.util.Optional;
 
+@Slf4j
 public class Messages {
 
   private Messages() {
@@ -35,14 +38,31 @@ public class Messages {
     if (message == null) {
       return null;
     }
-    final var answer = findFirstAnswerRecord(message);
-    if (answer == null) {
-      return Optional
-        .ofNullable(findQuestionHostname(message))
-        .map(Hostname::getValue)
-        .orElse("N/A");
+    try {
+      final var answer = findFirstAnswerRecord(message);
+      final var rcode = message.getRcode();
+      if (answer != null) {
+        return String.format("rc=%d, res=%s", rcode, simplePrint(answer));
+      }
+      final var question = message.getQuestion();
+      final var type = Objects.useItOrDefault(
+        Objects.toString(Entry.Type.of(question.getType())),
+        () -> String.valueOf(question.getType())
+      );
+      final var hostname = question.getName().toString(true);
+      final var sb = new StringBuilder();
+      if (Messages.hasFlag(message, Flags.QR)) {
+        sb.append("rc=")
+          .append(rcode)
+          .append(", ")
+        ;
+      }
+      sb.append(String.format("query=%s:%s", type, hostname));
+      return sb.toString();
+    } catch (Throwable e) {
+      log.warn("status=failedToSimplePrint, msg={}", message, e);
+      return String.valueOf(message);
     }
-    return String.format("%s", simplePrint(answer));
   }
 
   public static String detailedPrint(Message msg) {
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/RequestHandlerDefault.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/RequestHandlerDefault.java
index ea552519..9952ecbd 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/RequestHandlerDefault.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/RequestHandlerDefault.java
@@ -5,14 +5,15 @@ import com.mageddo.dnsproxyserver.server.dns.solver.CacheName.Name;
 import com.mageddo.dnsproxyserver.server.dns.solver.Response;
 import com.mageddo.dnsproxyserver.server.dns.solver.SolverCache;
 import com.mageddo.dnsproxyserver.server.dns.solver.SolverProvider;
-import com.mageddo.dnsproxyserver.utils.Classes;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.time.StopWatch;
+import org.apache.commons.lang3.tuple.Pair;
 import org.xbill.DNS.Message;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
+import java.util.ArrayList;
 import java.util.Optional;
 
 import static com.mageddo.dnsproxyserver.server.dns.Messages.simplePrint;
@@ -39,14 +40,15 @@ public class RequestHandlerDefault implements RequestHandler {
   }
 
   Message solve(Message query, String kind) {
-
+    final var queryStr = simplePrint(query);
     final var stopWatch = StopWatch.createStarted();
+    log.debug("status=solveReq, kind={}, query={}", kind, queryStr);
     try {
-      final var r = Optional
+      final var res = Optional
         .ofNullable(this.cache.handle(query, this::solve0))
         .orElseGet(() -> buildDefaultRes(query));
-      log.debug("status=solved, kind={}, time={}, res={}", kind, stopWatch.getTime(), simplePrint(r));
-      return r;
+      log.debug("status=solveRes, kind={}, time={}, res={}, req={}", kind, stopWatch.getTime(), simplePrint(res), queryStr);
+      return res;
     } catch (Exception e) {
       log.warn(
         "status=solverFailed, totalTime={}, eClass={}, msg={}",
@@ -59,31 +61,42 @@ public class RequestHandlerDefault implements RequestHandler {
   Response solve0(Message reqMsg) {
     final var stopWatch = StopWatch.createStarted();
     final var solvers = this.solverProvider.getSolvers();
-    for (final var solver : solvers) {
-      stopWatch.split();
-      final var solverName = Classes.findSimpleName(solver);
-      try {
-        final var reqStr = simplePrint(reqMsg);
-        log.trace("status=trySolve, solver={}, req={}", solverName, reqStr);
-        final var res = solver.handle(reqMsg);
-        if (res == null) {
-          log.trace(
-            "status=notSolved, currentSolverTime={}, totalTime={}, solver={}, req={}",
-            stopWatch.getTime() - stopWatch.getSplitTime(), stopWatch.getTime(), solverName, reqStr
+    final var timeSummary = new ArrayList<>();
+    try {
+      for (final var solver : solvers) {
+        stopWatch.split();
+        final var solverName = solver.name();
+        try {
+          final var reqStr = simplePrint(reqMsg);
+          log.trace("status=trySolve, solver={}, req={}", solverName, reqStr);
+          final var res = solver.handle(reqMsg);
+          final var solverTime = stopWatch.getTime() - stopWatch.getSplitTime();
+          if (log.isDebugEnabled()) {
+            timeSummary.add(Pair.of(solverName, solverTime));
+          }
+          if (res == null) {
+            log.trace(
+              "status=notSolved, currentSolverTime={}, totalTime={}, solver={}, req={}",
+              solverTime, stopWatch.getTime(), solverName, reqStr
+            );
+            continue;
+          }
+          log.debug(
+            "status=solved, currentSolverTime={}, totalTime={}, solver={}, req={}, res={}",
+            solverTime, stopWatch.getTime(), solverName, reqStr, simplePrint(res)
+          );
+          return res;
+        } catch (Exception e) {
+          log.warn(
+            "status=solverFailed, currentSolverTime={}, totalTime={}, solver={}, eClass={}, msg={}",
+            stopWatch.getTime() - stopWatch.getSplitTime(), stopWatch.getTime(), solverName,
+            ClassUtils.getSimpleName(e), e.getMessage(), e
           );
-          continue;
         }
-        log.debug(
-          "status=solved, currentSolverTime={}, totalTime={}, solver={}, req={}, res={}",
-          stopWatch.getTime() - stopWatch.getSplitTime(), stopWatch.getTime(), solverName, reqStr, simplePrint(res)
-        );
-        return res;
-      } catch (Exception e) {
-        log.warn(
-          "status=solverFailed, currentSolverTime={}, totalTime={}, solver={}, eClass={}, msg={}",
-          stopWatch.getTime() - stopWatch.getSplitTime(), stopWatch.getTime(), solverName,
-          ClassUtils.getSimpleName(e), e.getMessage(), e
-        );
+      }
+    } finally {
+      if (log.isDebugEnabled()) {
+        log.debug("status=solveSummary, summary={}", timeSummary);
       }
     }
     return null;
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/TCPServer.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/TCPServer.java
index 7cb7d656..dff6f4cc 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/TCPServer.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/TCPServer.java
@@ -34,7 +34,7 @@ public class TCPServer {
 
   @Inject
   public TCPServer() {
-    this.pool = ThreadPool.newFixed(20);
+    this.pool = ThreadPool.newFixed(50);
     this.clients = new LinkedHashSet<>();
   }
 
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/UDPServer.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/UDPServer.java
index 3b96bce9..2c137f3e 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/UDPServer.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/UDPServer.java
@@ -23,7 +23,7 @@ public class UDPServer {
   public UDPServer(SocketAddress address, RequestHandler requestHandler) {
     this.address = address;
     this.requestHandler = requestHandler;
-    this.pool = ThreadPool.newFixed(20);
+    this.pool = ThreadPool.newFixed(50);
   }
 
   public void start() {
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/CacheEntry.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/CacheEntry.java
index e626aa67..8cd9cf3b 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/CacheEntry.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/CacheEntry.java
@@ -11,5 +11,5 @@ import java.time.LocalDateTime;
 public class CacheEntry {
   private String key;
   private Duration ttl;
-  private LocalDateTime createdAt;
+  private LocalDateTime expiresAt;
 }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Response.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Response.java
index 76ffd59f..cfc74fbf 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Response.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Response.java
@@ -28,11 +28,11 @@ public class Response {
   @NonNull
   private LocalDateTime createdAt;
 
-  public static Response of(Message message){
+  public static Response of(Message message) {
     return of(message, Messages.DEFAULT_TTL_DURATION);
   }
 
-  public static Response of(Message message, Duration ttl){
+  public static Response of(Message message, Duration ttl) {
     return Response
       .builder()
       .message(message)
@@ -48,4 +48,10 @@ public class Response {
       .build()
       ;
   }
+
+  public Response withTTL(Duration ttl) {
+    return this.toBuilder()
+      .ttl(ttl)
+      .build();
+  }
 }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCache.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCache.java
index b5e8e1b6..ab410cc9 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCache.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCache.java
@@ -35,7 +35,7 @@ public class SolverCache {
 
   public Response handleRes(Message query, Function<Message, Response> delegate) {
     final var key = buildKey(query);
-    final var res = this.cache.computeIfAbsent0(key, (k) -> {
+    final var res = this.cache.computeIfAbsentWithTTL(key, (k) -> {
       log.trace("status=lookup, key={}, req={}", key, Messages.simplePrint(query));
       final var _res = delegate.apply(query);
       if (_res == null) {
@@ -74,7 +74,7 @@ public class SolverCache {
       final var entry = new CacheEntry()
         .setKey(k)
         .setTtl(v.getTtl())
-        .setCreatedAt(v.getCreatedAt());
+        .setExpiresAt(v.getExpiresAt());
       tmpMap.put(k, entry);
     }
     return tmpMap;
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCachedRemote.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCachedRemote.java
index 5433662f..e9c20afa 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCachedRemote.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCachedRemote.java
@@ -1,5 +1,6 @@
 package com.mageddo.dnsproxyserver.server.dns.solver;
 
+import com.mageddo.commons.lang.Objects;
 import com.mageddo.dnsproxyserver.server.dns.Messages;
 import com.mageddo.dnsproxyserver.server.dns.solver.CacheName.Name;
 import lombok.extern.slf4j.Slf4j;
@@ -31,10 +32,6 @@ public class SolverCachedRemote implements Solver {
       log.debug("status=remoteHotLoading, query={}", Messages.simplePrint(query));
       return this.solverRemote.handle(query);
     });
-    return res
-      .toBuilder()
-      .ttl(Duration.ofSeconds(20))
-      .build()
-      ;
+    return Objects.mapOrNull(res, response -> response.withTTL(Duration.ofSeconds(20)));
   }
 }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemote.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemote.java
index 350f9a8d..2c2758f6 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemote.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemote.java
@@ -4,6 +4,7 @@ import com.mageddo.dnsproxyserver.server.dns.Messages;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.time.StopWatch;
 import org.xbill.DNS.Flags;
 import org.xbill.DNS.Message;
 import org.xbill.DNS.Rcode;
@@ -28,35 +29,46 @@ public class SolverRemote implements Solver {
 
   @Override
   public Response handle(Message query) {
+    final var stopWatch = StopWatch.createStarted();
     Message lastErrorMsg = null;
     for (int i = 0; i < this.delegate.resolvers().size(); i++) {
+      stopWatch.split();
       final Resolver resolver = this.delegate.resolvers().get(i);
       try {
 
         final var res = Messages.setFlag(resolver.send(query), Flags.RA);
 
         if (res.getRcode() == Rcode.NOERROR) {
-          log.trace("status=found, i={}, req={}, res={}, server={}", i, simplePrint(query), simplePrint(res), resolver);
+          log.trace(
+            "status=found, i={}, time={}, req={}, res={}, server={}",
+            i, stopWatch.getTime(), simplePrint(query), simplePrint(res), resolver
+          );
           return Response.of(res, DEFAULT_SUCCESS_TTL);
         } else {
           lastErrorMsg = res;
-          log.trace("status=notFound, i={}, req={}, res={}, server={}", i, simplePrint(query), simplePrint(res), resolver);
+          log.trace(
+            "status=notFound, i={}, time={}, req={}, res={}, server={}",
+            i, stopWatch.getTime(), simplePrint(query), simplePrint(res), resolver
+          );
         }
       } catch (IOException e) {
+        final var time = stopWatch.getTime() - stopWatch.getSplitTime();
         if (e.getMessage().contains("Timed out while trying")) {
           log.info(
-            "status=timedOut, req={}, msg={} class={}",
-            simplePrint(query), e.getMessage(), ClassUtils.getSimpleName(e)
+            "status=timedOut, i={}, time={}, req={}, msg={} class={}",
+            i, time, simplePrint(query), e.getMessage(), ClassUtils.getSimpleName(e)
           );
           continue;
         }
         log.warn(
-          "status=failed, i={}, req={}, server={}, errClass={}, msg={}",
-          i, simplePrint(query), resolver, ClassUtils.getSimpleName(e), e.getMessage(), e
+          "status=failed, i={}, time={}, req={}, server={}, errClass={}, msg={}",
+          i, time, simplePrint(query), resolver, ClassUtils.getSimpleName(e), e.getMessage(), e
         );
       }
     }
-    if (lastErrorMsg == null) {
+    final var hasErrorResult = lastErrorMsg == null;
+    log.debug("status=finally, time={}, hasErrorResult={}", stopWatch.getTime(), hasErrorResult);
+    if (hasErrorResult) {
       return null;
     }
     return Response.of(lastErrorMsg, DEFAULT_NXDOMAIN_TTL);
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/rest/reqres/CacheEntryResV1.java b/src/main/java/com/mageddo/dnsproxyserver/server/rest/reqres/CacheEntryResV1.java
index e8732d72..01fcc3aa 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/rest/reqres/CacheEntryResV1.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/rest/reqres/CacheEntryResV1.java
@@ -15,7 +15,7 @@ public class CacheEntryResV1 {
 
   private String key;
   private String ttl;
-  private String createdAt;
+  private String expiresAt;
 
   public static List<CacheEntryResV1> of(List<CacheEntry> entries) {
     return entries
@@ -28,7 +28,7 @@ public class CacheEntryResV1 {
     return new CacheEntryResV1()
       .setKey(entry.getKey())
       .setTtl(String.valueOf(entry.getTtl()))
-      .setCreatedAt(String.valueOf(entry.getCreatedAt()))
+      .setExpiresAt(String.valueOf(entry.getExpiresAt()))
       ;
   }
 


Test patch:
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/MessagesTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/MessagesTest.java
index 89301a94..17ba628e 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/server/dns/MessagesTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/MessagesTest.java
@@ -5,6 +5,9 @@ import org.junit.jupiter.api.Test;
 import org.xbill.DNS.Flags;
 import org.xbill.DNS.Rcode;
 
+import static com.mageddo.dnsproxyserver.server.dns.Messages.findFirstAnswerRecord;
+import static com.mageddo.dnsproxyserver.templates.MessageTemplates.acmeAResponse;
+import static com.mageddo.dnsproxyserver.templates.MessageTemplates.acmeNxDomain;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -68,4 +71,63 @@ class MessagesTest {
 
   }
 
+  @Test
+  void mustBuildSimplePrintReq(){
+    // arrange
+    final var msg = MessageTemplates.acmeAQuery();
+
+    // act
+    final var str = Messages.simplePrint(msg);
+
+    // assert
+    assertEquals("""
+      query=A:acme.com""",
+      str
+    );
+  }
+
+  @Test
+  void mustBuildSimpleAnswer(){
+    // arrange
+    final var answer = findFirstAnswerRecord(acmeAResponse());
+
+    // act
+    final var str = Messages.simplePrint(answer);
+
+    // assert
+    assertEquals("""
+      acme.com.    30  IN  A  10.10.0.1""",
+      str
+    );
+  }
+
+  @Test
+  void mustBuildSimplePrintResponse(){
+    // arrange
+    final var res = acmeAResponse();
+
+    // act
+    final var str = Messages.simplePrint(res);
+
+    // assert
+    assertEquals("""
+      rc=0, res=acme.com.    30  IN  A  10.10.0.1""",
+      str
+    );
+  }
+
+  @Test
+  void mustBuildSimplePrintNxDomainResponse(){
+    // arrange
+    final var res = acmeNxDomain();
+
+    // act
+    final var str = Messages.simplePrint(res);
+
+    // assert
+    assertEquals("""
+      rc=3, query=A:acme.com""",
+      str
+    );
+  }
 }
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCachedRemoteTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCachedRemoteTest.java
new file mode 100644
index 00000000..99da4771
--- /dev/null
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverCachedRemoteTest.java
@@ -0,0 +1,34 @@
+package com.mageddo.dnsproxyserver.server.dns.solver;
+
+import com.mageddo.dnsproxyserver.templates.MessageTemplates;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class SolverCachedRemoteTest {
+
+  @Mock
+  SolverCache solverCache;
+
+  @Spy
+  @InjectMocks
+  SolverCachedRemote solver;
+
+  @Test
+  void mustLeadWithNullResponses(){
+    // arrange
+    final var query = MessageTemplates.acmeQuadAQuery();
+
+    // act
+    final var res = this.solver.handle(query);
+
+    // assert
+    assertNull(res);
+  }
+}
diff --git a/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java b/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java
index 4b785f06..35883112 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java
@@ -22,4 +22,12 @@ public class MessageTemplates {
   public static Message buildNXAnswer(Message query) {
     return Messages.nxDomain(query);
   }
+
+  public static Message acmeAResponse() {
+    return Messages.answer(MessageTemplates.acmeAQuery(), IpTemplates.LOCAL);
+  }
+
+  public static Message acmeNxDomain() {
+    return Messages.nxDomain(MessageTemplates.acmeAQuery());
+  }
 }