diff --git a/server/src/main/java/com/onixbyte/ahsarahguide/config/CacheConfig.java b/server/src/main/java/com/onixbyte/ahsarahguide/config/CacheConfig.java index 1b43650..5fdb3c4 100644 --- a/server/src/main/java/com/onixbyte/ahsarahguide/config/CacheConfig.java +++ b/server/src/main/java/com/onixbyte/ahsarahguide/config/CacheConfig.java @@ -1,9 +1,11 @@ package com.onixbyte.ahsarahguide.config; import com.onixbyte.ahsarahguide.shared.JacksonRedisSerialiser; +import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -39,21 +41,24 @@ import java.time.Duration; public class CacheConfig { /** - * Creates a custom Redis cache manager with JSON serialisation support. + * Creates the application's primary cache manager, backed by Redis. *

- * This method configures a {@link RedisCacheManager} that uses string serialisation for cache - * keys and {@link GenericJackson2JsonRedisSerializer} for cache values. This setup ensures that - * complex objects can be stored and retrieved from Redis cache whilst maintaining readability - * and compatibility with JSON-based systems. + * This manager is marked {@link Primary}, so it is the one selected for any + * {@code @Cacheable} or {@code @CacheEvict} annotation that does not name a manager + * explicitly, as well as for any {@link CacheManager} that is injected by type. + *

+ * Cache entries are keyed using string serialisation and values are stored as JSON via + * {@link JacksonRedisSerialiser}. Every entry expires after two hours, making this manager + * suitable for data that is expensive to compute but not required to survive for long. * * @param connectionFactory the Redis connection factory used to establish connections - * @return a configured {@link RedisCacheManager} with custom serialisation settings + * @return a fully configured {@link CacheManager} with a two-hour default entry TTL + * @see #longTermCacheManager(RedisConnectionFactory) * @see RedisCacheManager - * @see GenericJackson2JsonRedisSerializer - * @see RedisSerializationContext */ + @Primary @Bean - public RedisCacheManager cacheManager( + public CacheManager defaultCacheManager( RedisConnectionFactory connectionFactory ) { var _keySerializer = RedisSerializer.string(); @@ -63,7 +68,40 @@ public class CacheConfig { .fromSerializer(_keySerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(JacksonRedisSerialiser.INSTANCE)) - .entryTtl(Duration.ofMinutes(90L)); + .entryTtl(Duration.ofHours(2L)); + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(connectionFactory) + .cacheDefaults(cacheConfiguration) + .build(); + } + + /** + * Creates a secondary cache manager for data that must be retained for longer. + *

+ * This manager is not {@link Primary}; it must be requested by name wherever it is needed, + * as in {@code @Cacheable(cacheManager = "longTermCacheManager")}. + *

+ * Serialisation is configured identically to {@link #defaultCacheManager}, but entries + * expire after one day rather than two hours. + * + * @param connectionFactory the Redis connection factory used to establish connections + * @return a fully configured {@link CacheManager} with a one-day default entry TTL + * @see #defaultCacheManager(RedisConnectionFactory) + * @see RedisCacheManager + */ + @Bean + public CacheManager longTermCacheManager( + RedisConnectionFactory connectionFactory + ) { + var _keySerializer = RedisSerializer.string(); + + var cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair + .fromSerializer(_keySerializer)) + .serializeValuesWith(RedisSerializationContext.SerializationPair + .fromSerializer(JacksonRedisSerialiser.INSTANCE)) + .entryTtl(Duration.ofDays(1L)); return RedisCacheManager.RedisCacheManagerBuilder .fromConnectionFactory(connectionFactory) diff --git a/server/src/main/java/com/onixbyte/ahsarahguide/manager/DailyPasswordManager.java b/server/src/main/java/com/onixbyte/ahsarahguide/manager/DailyPasswordManager.java index 6686fbb..f622458 100644 --- a/server/src/main/java/com/onixbyte/ahsarahguide/manager/DailyPasswordManager.java +++ b/server/src/main/java/com/onixbyte/ahsarahguide/manager/DailyPasswordManager.java @@ -4,20 +4,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.onixbyte.ahsarahguide.domain.dto.DailyPasswordResponse; -import com.onixbyte.ahsarahguide.exeption.InternalServerErrorException; import com.onixbyte.ahsarahguide.shared.JacksonModules; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.cache.annotation.Cacheable; import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import org.springframework.web.server.ResponseStatusException; -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.Objects; /** @@ -28,15 +23,13 @@ import java.util.Objects; @Component public class DailyPasswordManager { - private static final String CACHE_KEY_PREFIX = "daily-password:"; + private static final String CACHE_NAME = "daily-password"; private final RestClient restClient; - private final RedisTemplate redisTemplate; @Autowired public DailyPasswordManager( - RestClient.Builder restClientBuilder, - RedisTemplate redisTemplate + RestClient.Builder restClientBuilder ) { var snakeCaseMapper = new ObjectMapper(); snakeCaseMapper.setPropertyNamingStrategy( @@ -54,25 +47,22 @@ public class DailyPasswordManager { new MappingJackson2HttpMessageConverter(snakeCaseMapper)); }) .build(); - this.redisTemplate = redisTemplate; } /** * Retrieves the daily password from cache or generates a new one. * @return the daily password response */ + @Cacheable( + cacheNames = CACHE_NAME, + key = "T(java.time.LocalDate).now().toString()", + sync = true, + unless = "#result == null", + cacheManager = "longTermCacheManager" + ) public DailyPasswordResponse getDailyPassword() { - var key = CACHE_KEY_PREFIX + LocalDate.now(); - - // Attempt to fetch the cached response - var cached = redisTemplate.opsForValue().get(key); - if (cached != null) { - return (DailyPasswordResponse) cached; - } - DailyPasswordResponse response; try { - // Fetch from the upstream remote service response = restClient.get() .uri((uriBuilder) -> uriBuilder .path("/sjzmm") @@ -90,14 +80,6 @@ public class DailyPasswordManager { throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "No daily password data available."); } - // Calculate the precise duration until midnight to optimise Redis memory usage - var now = LocalDateTime.now(); - var midnight = now.toLocalDate().atStartOfDay().plusDays(1); - var durationUntilMidnight = Duration.between(now, midnight); - - // Save to Redis with the calculated TTL - redisTemplate.opsForValue().set(key, response, durationUntilMidnight); - return response; } }