SingletonCallAspect.java

1
package org.cardanofoundation.explorer.api.config.aop.singletoncache;
2
3
import java.time.LocalDate;
4
import java.time.LocalDateTime;
5
import java.time.format.DateTimeFormatter;
6
import java.util.concurrent.TimeUnit;
7
8
import lombok.RequiredArgsConstructor;
9
import lombok.extern.log4j.Log4j2;
10
11
import org.springframework.beans.factory.annotation.Value;
12
import org.springframework.data.redis.core.RedisTemplate;
13
import org.springframework.stereotype.Component;
14
15
import com.google.gson.Gson;
16
import com.google.gson.GsonBuilder;
17
import com.google.gson.JsonDeserializer;
18
import com.google.gson.JsonPrimitive;
19
import com.google.gson.JsonSerializer;
20
import org.aspectj.lang.ProceedingJoinPoint;
21
import org.aspectj.lang.annotation.Around;
22
import org.aspectj.lang.annotation.Aspect;
23
import org.aspectj.lang.reflect.CodeSignature;
24
25
@Aspect
26
@Component
27
@Log4j2
28
@RequiredArgsConstructor
29
public class SingletonCallAspect {
30
31
  @Value("${application.network}")
32
  private String network;
33
34
  private static final String LOCKED = "LOCKED";
35
  private static final String PREFIX_KEY = "METHOD_CACHE:";
36
37
  private final RedisTemplate<String, Object> redisTemplate;
38
39
  /* Config Gson for working with LocalDate/LocalDateTime */
40
  private static final Gson GSON =
41
      new GsonBuilder()
42
          .registerTypeAdapter(
43
              LocalDate.class,
44
              (JsonSerializer<LocalDate>)
45
                  (value, type, context) ->
46 1 1. lambda$static$0 : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$0 → NO_COVERAGE
                      new JsonPrimitive(value.format(DateTimeFormatter.ISO_LOCAL_DATE)))
47
          .registerTypeAdapter(
48
              LocalDateTime.class,
49
              (JsonSerializer<LocalDateTime>)
50
                  (value, type, context) ->
51 1 1. lambda$static$1 : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$1 → NO_COVERAGE
                      new JsonPrimitive(value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)))
52
          .registerTypeAdapter(
53
              LocalDate.class,
54
              (JsonDeserializer<LocalDate>)
55
                  (jsonElement, type, context) ->
56 1 1. lambda$static$2 : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$2 → NO_COVERAGE
                      LocalDate.parse(
57
                          jsonElement.getAsJsonPrimitive().getAsString(),
58
                          DateTimeFormatter.ISO_LOCAL_DATE))
59
          .registerTypeAdapter(
60
              LocalDateTime.class,
61
              (JsonDeserializer<LocalDateTime>)
62
                  (jsonElement, type, context) ->
63 1 1. lambda$static$3 : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$3 → NO_COVERAGE
                      LocalDateTime.parse(
64
                          jsonElement.getAsJsonPrimitive().getAsString(),
65
                          DateTimeFormatter.ISO_LOCAL_DATE_TIME))
66
          .create();
67
68
  /**
69
   * this method will be called with the method has annotation @SingletonCall if there are multiple
70
   * request calls at the same time, only first request can be processed other request must wait for
71
   * data from first request. Firstly, those request will check Redis cache, If having data (value
72
   * != LOCKED) return intermediately Or else, first request will call database and process, then
73
   * save data to redis cache Other requests will recall redis after each `callAfterMilis`, until
74
   * having data in redis This appoach will save CPU of database server in case expensive queries
75
   */
76
  @Around("@annotation(singletonCall)")
77
  public Object aroundAdvice(ProceedingJoinPoint joinPoint, SingletonCall singletonCall)
78
      throws Throwable {
79
80
    CodeSignature methodSignature = (CodeSignature) joinPoint.getSignature();
81
    methodSignature.getName();
82
    String[] sigParamNames = methodSignature.getParameterNames();
83
    Object[] sigParamValues = joinPoint.getArgs();
84
    String cacheKey = generateCacheKey(methodSignature.getName(), sigParamNames, sigParamValues);
85
86
    var opValuesRedis = redisTemplate.opsForValue();
87
    try {
88
      Object cacheResult = redisTemplate.opsForValue().get(cacheKey);
89 1 1. aroundAdvice : negated conditional → NO_COVERAGE
      if (cacheResult == null) {
90 1 1. aroundAdvice : removed call to org/springframework/data/redis/core/ValueOperations::set → NO_COVERAGE
        opValuesRedis.set(cacheKey, LOCKED);
91
        Object data = joinPoint.proceed();
92 1 1. aroundAdvice : removed call to org/springframework/data/redis/core/ValueOperations::set → NO_COVERAGE
        opValuesRedis.set(
93
            cacheKey, GSON.toJson(data), singletonCall.expireAfterSeconds(), TimeUnit.SECONDS);
94 1 1. aroundAdvice : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::aroundAdvice → NO_COVERAGE
        return data;
95
      } else {
96 1 1. aroundAdvice : negated conditional → NO_COVERAGE
        if (LOCKED.equals(cacheResult.toString())) {
97
          do {
98 1 1. aroundAdvice : removed call to java/lang/Thread::sleep → NO_COVERAGE
            Thread.sleep(singletonCall.callAfterMilis());
99
            cacheResult = redisTemplate.opsForValue().get(cacheKey);
100 1 1. aroundAdvice : negated conditional → NO_COVERAGE
            if (cacheResult == null) {
101
              return null;
102
            }
103 1 1. aroundAdvice : negated conditional → NO_COVERAGE
          } while (LOCKED.equals(cacheResult.toString()));
104
        } else {
105 1 1. aroundAdvice : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::aroundAdvice → NO_COVERAGE
          return GSON.fromJson(cacheResult.toString(), singletonCall.typeToken().getType().get());
106
        }
107 1 1. aroundAdvice : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::aroundAdvice → NO_COVERAGE
        return GSON.fromJson(cacheResult.toString(), singletonCall.typeToken().getType().get());
108
      }
109
    } catch (Exception e) {
110
      redisTemplate.delete(cacheKey);
111
      throw e;
112
    } finally {
113
      // do nothing
114
    }
115
  }
116
117
  private String generateCacheKey(
118
      String methodName, String[] sigParamNames, Object[] sigParamValues) {
119
    StringBuilder str = new StringBuilder();
120 2 1. generateCacheKey : negated conditional → NO_COVERAGE
2. generateCacheKey : changed conditional boundary → NO_COVERAGE
    for (int i = 0; i < sigParamNames.length; i++) {
121
      str.append(sigParamNames[i]).append(":").append(sigParamValues[i]);
122 3 1. generateCacheKey : changed conditional boundary → NO_COVERAGE
2. generateCacheKey : Replaced integer subtraction with addition → NO_COVERAGE
3. generateCacheKey : negated conditional → NO_COVERAGE
      if (i < sigParamNames.length - 1) str.append("_");
123
    }
124 1 1. generateCacheKey : replaced return value with "" for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::generateCacheKey → NO_COVERAGE
    return network + "-" + PREFIX_KEY + methodName + ":" + str.toString().hashCode();
125
  }
126
}

Mutations

46

1.1
Location : lambda$static$0
Killed by : none
replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$0 → NO_COVERAGE

51

1.1
Location : lambda$static$1
Killed by : none
replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$1 → NO_COVERAGE

56

1.1
Location : lambda$static$2
Killed by : none
replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$2 → NO_COVERAGE

63

1.1
Location : lambda$static$3
Killed by : none
replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$3 → NO_COVERAGE

89

1.1
Location : aroundAdvice
Killed by : none
negated conditional → NO_COVERAGE

90

1.1
Location : aroundAdvice
Killed by : none
removed call to org/springframework/data/redis/core/ValueOperations::set → NO_COVERAGE

92

1.1
Location : aroundAdvice
Killed by : none
removed call to org/springframework/data/redis/core/ValueOperations::set → NO_COVERAGE

94

1.1
Location : aroundAdvice
Killed by : none
replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::aroundAdvice → NO_COVERAGE

96

1.1
Location : aroundAdvice
Killed by : none
negated conditional → NO_COVERAGE

98

1.1
Location : aroundAdvice
Killed by : none
removed call to java/lang/Thread::sleep → NO_COVERAGE

100

1.1
Location : aroundAdvice
Killed by : none
negated conditional → NO_COVERAGE

103

1.1
Location : aroundAdvice
Killed by : none
negated conditional → NO_COVERAGE

105

1.1
Location : aroundAdvice
Killed by : none
replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::aroundAdvice → NO_COVERAGE

107

1.1
Location : aroundAdvice
Killed by : none
replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::aroundAdvice → NO_COVERAGE

120

1.1
Location : generateCacheKey
Killed by : none
negated conditional → NO_COVERAGE

2.2
Location : generateCacheKey
Killed by : none
changed conditional boundary → NO_COVERAGE

122

1.1
Location : generateCacheKey
Killed by : none
changed conditional boundary → NO_COVERAGE

2.2
Location : generateCacheKey
Killed by : none
Replaced integer subtraction with addition → NO_COVERAGE

3.3
Location : generateCacheKey
Killed by : none
negated conditional → NO_COVERAGE

124

1.1
Location : generateCacheKey
Killed by : none
replaced return value with "" for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::generateCacheKey → NO_COVERAGE

Active mutators

Tests examined


Report generated by PIT 1.14.2