diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 3f751c0e0..fe75c1f2e 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,6 @@ +## 3.25.2 +* Migrating cache to caffeine #522 + ## 3.25.1 * Ensure thread will have name at the logs, behavior changed since `3.25.0` #436. diff --git a/build.gradle b/build.gradle index 4e4d81264..1985eb4e9 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,8 @@ dependencies { implementation('info.picocli:picocli:4.7.1') implementation('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1') + implementation('com.github.ben-manes.caffeine:caffeine:3.1.8') + testAnnotationProcessor("com.google.dagger:dagger-compiler:2.45") testCompileOnly('org.projectlombok:lombok:1.18.+') diff --git a/gradle.properties b/gradle.properties index fe1fadaa2..2f9a39d87 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=3.25.1-snapshot +version=3.25.2-snapshot diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/SolverCache.java b/src/main/java/com/mageddo/dnsproxyserver/solver/SolverCache.java index 5400daf7d..987057653 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/solver/SolverCache.java +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/SolverCache.java @@ -1,15 +1,18 @@ package com.mageddo.dnsproxyserver.solver; -import com.mageddo.commons.caching.LruTTLCache; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Expiry; import com.mageddo.commons.lang.Objects; -import com.mageddo.commons.lang.tuple.Pair; import com.mageddo.dns.utils.Messages; import com.mageddo.dnsproxyserver.solver.CacheName.Name; -import lombok.RequiredArgsConstructor; +import lombok.Builder; +import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.Message; import java.time.Duration; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -20,12 +23,18 @@ import static com.mageddo.dns.utils.Messages.findQuestionType; @Slf4j -@RequiredArgsConstructor public class SolverCache { - private final LruTTLCache cache = new LruTTLCache(2048, Duration.ofSeconds(5), false); - private final Name name; + private final Cache cache; + + public SolverCache(Name name) { + this.name = name; + this.cache = Caffeine.newBuilder() + .maximumSize(2048) + .expireAfter(buildExpiryPolicy()) + .build(); + } public Message handle(Message query, Function delegate) { return Objects.mapOrNull(this.handleRes(query, delegate), Response::getMessage); @@ -33,7 +42,7 @@ public Message handle(Message query, Function delegate) { public Response handleRes(Message query, Function delegate) { final var key = buildKey(query); - final var res = this.cache.computeIfAbsentWithTTL(key, (k) -> { + final var cacheValue = this.cache.get(key, (k) -> { log.trace("status=lookup, key={}, req={}", key, Messages.simplePrint(query)); final var _res = delegate.apply(query); if (_res == null) { @@ -42,12 +51,13 @@ public Response handleRes(Message query, Function delegate) { } final var ttl = _res.getDpsTtl(); log.debug("status=hotload, k={}, ttl={}, simpleMsg={}", k, ttl, Messages.simplePrint(query)); - return Pair.of(_res, ttl); + return CacheValue.of(_res, ttl); }); - if (res == null) { + if (cacheValue == null) { return null; } - return res.withMessage(Messages.mergeId(query, res.getMessage())); + final var response = cacheValue.getResponse(); + return response.withMessage(Messages.mergeId(query, response.getMessage())); } static String buildKey(Message reqMsg) { @@ -56,11 +66,11 @@ static String buildKey(Message reqMsg) { } public int getSize() { - return this.cache.getSize(); + return (int) this.cache.estimatedSize(); } public void clear() { - this.cache.clear(); + this.cache.invalidateAll(); } public Map asMap() { @@ -82,4 +92,51 @@ public Name name() { return this.name; } + public CacheValue get(String key) { + return this.cache.getIfPresent(key); + } + + @Value + @Builder + static class CacheValue { + + private Response response; + private Duration ttl; + + public static CacheValue of(Response res, Duration ttl) { + return CacheValue + .builder() + .response(res) + .ttl(ttl) + .build(); + } + + public LocalDateTime getExpiresAt() { + return this.response + .getCreatedAt() + .plus(this.ttl) + ; + } + } + + private static Expiry buildExpiryPolicy() { + return new Expiry<>() { + @Override + public long expireAfterCreate(String key, CacheValue value, long currentTime) { + return value.getTtl().toNanos(); + } + + @Override + public long expireAfterUpdate(String key, CacheValue value, long currentTime, long currentDuration) { + return currentDuration; + } + + @Override + public long expireAfterRead(String key, CacheValue value, long currentTime, long currentDuration) { + return currentDuration; + } + }; + } + + } diff --git a/src/test/java/com/mageddo/dnsproxyserver/solver/SolversCacheTest.java b/src/test/java/com/mageddo/dnsproxyserver/solver/SolversCacheTest.java index 294f93148..738a78ff8 100644 --- a/src/test/java/com/mageddo/dnsproxyserver/solver/SolversCacheTest.java +++ b/src/test/java/com/mageddo/dnsproxyserver/solver/SolversCacheTest.java @@ -1,14 +1,21 @@ package com.mageddo.dnsproxyserver.solver; +import com.mageddo.commons.concurrent.Threads; import com.mageddo.dns.utils.Messages; import com.mageddo.dnsproxyserver.solver.CacheName.Name; -import com.mageddo.dnsproxyserver.solver.Response; -import com.mageddo.dnsproxyserver.solver.SolverCache; -import testing.templates.MessageTemplates; +import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.xbill.DNS.Flags; +import org.xbill.DNS.Message; +import testing.templates.MessageTemplates; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -21,7 +28,39 @@ class SolversCacheTest { SolverCache cache = new SolverCache(Name.GLOBAL); @Test - void mustCacheAndGetValidResponse(){ + void mustLeadWithConcurrency() { + + // arrange + final var req = MessageTemplates.acmeAQuery(); + final var r = new Random(); + + // act + concurrentRequests(1_000, req, r); + + } + + @Test + void mustCacheForTheSpecifiedTime() { + + // arrange + final var req = MessageTemplates.acmeAQuery(); + final var key = "A-acme.com"; + + // act + final var res = this.cache.handleRes(req, message -> { + return Response.of(Messages.aAnswer(message, "0.0.0.0"), Duration.ofMillis(50)); + }); + + // assert + assertNotNull(res); + assertNotNull(this.cache.get(key)); + + Threads.sleep(res.getDpsTtl().plusMillis(10)); + assertNull(this.cache.get(key)); + } + + @Test + void mustCacheAndGetValidResponse() { // arrange final var req = MessageTemplates.acmeAQuery(); @@ -40,7 +79,7 @@ void mustCacheAndGetValidResponse(){ } @Test - void cantCacheWhenDelegateSolverHasNoAnswer(){ + void cantCacheWhenDelegateSolverHasNoAnswer() { // arrange final var query = MessageTemplates.acmeAQuery(); @@ -52,4 +91,30 @@ void cantCacheWhenDelegateSolverHasNoAnswer(){ assertEquals(0, this.cache.getSize()); } + @SneakyThrows + private void concurrentRequests(int quantity, Message req, Random r) { + final var runnables = new ArrayList>(); + for (int i = 0; i < quantity; i++) { + runnables.add(() -> this.handleRequest(req, r)); + if (i % 10 == 0) { + runnables.add(() -> { + this.cache.clear(); + return null; + }); + } + } + + try (final var executor = Executors.newVirtualThreadPerTaskExecutor()) { + executor.invokeAll(runnables); + } + } + + private Object handleRequest(Message req, Random r) { + this.cache.handleRes(req, message -> { + final var res = Response.internalSuccess(Messages.aAnswer(message, "0.0.0.0")); + Threads.sleep(r.nextInt(10)); + return res; + }); + return null; + } }