Skip to content

Commit

Permalink
Migrating cache to caffeine (#523)
Browse files Browse the repository at this point in the history
* testing cache ttl

* testing

* adjusting test

* release notes

* clean codfe

* clean code

* clean code

* [Gradle Release Plugin] - new version commit:  '3.25.2-snapshot'.
  • Loading branch information
mageddo authored Jul 31, 2024
1 parent e66a6fc commit c2a39b9
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 18 deletions.
3 changes: 3 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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.+')
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=3.25.1-snapshot
version=3.25.2-snapshot
81 changes: 69 additions & 12 deletions src/main/java/com/mageddo/dnsproxyserver/solver/SolverCache.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,20 +23,26 @@
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<String, CacheValue> cache;

public SolverCache(Name name) {
this.name = name;
this.cache = Caffeine.newBuilder()
.maximumSize(2048)
.expireAfter(buildExpiryPolicy())
.build();
}

public Message handle(Message query, Function<Message, Response> delegate) {
return Objects.mapOrNull(this.handleRes(query, delegate), Response::getMessage);
}

public Response handleRes(Message query, Function<Message, Response> 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) {
Expand All @@ -42,12 +51,13 @@ public Response handleRes(Message query, Function<Message, Response> 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) {
Expand All @@ -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<String, CacheEntry> asMap() {
Expand All @@ -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<String, CacheValue> 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;
}
};
}


}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand All @@ -40,7 +79,7 @@ void mustCacheAndGetValidResponse(){
}

@Test
void cantCacheWhenDelegateSolverHasNoAnswer(){
void cantCacheWhenDelegateSolverHasNoAnswer() {
// arrange
final var query = MessageTemplates.acmeAQuery();

Expand All @@ -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<Callable<Object>>();
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;
}
}

0 comments on commit c2a39b9

Please sign in to comment.