ModernSearchCache.java
package com.university.bookstore.search;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Function;
import java.util.logging.Logger;
import com.university.bookstore.model.Material;
/**
* Modern high-performance cache implementation with advanced features.
* Features:
* - Time-based and size-based eviction
* - Async loading with CompletableFuture
* - Statistics tracking
* - Warm-up and refresh capabilities
* - Thread-safe operations
*
* @author Navid Mohaghegh
* @version 4.0
* @since 2024-09-15
*/
public class ModernSearchCache implements AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(ModernSearchCache.class.getName());
/**
* Cache entry with metadata.
*/
private record CacheEntry(
List<Material> value,
Instant createdAt,
Instant lastAccessedAt,
AtomicLong accessCount
) {
/**
* Creates a new cache entry.
*/
public static CacheEntry of(List<Material> value) {
return new CacheEntry(
Collections.unmodifiableList(new ArrayList<>(value)),
Instant.now(),
Instant.now(),
new AtomicLong(1)
);
}
/**
* Records an access to this entry.
*/
public CacheEntry recordAccess() {
accessCount.incrementAndGet();
return new CacheEntry(value, createdAt, Instant.now(), accessCount);
}
/**
* Checks if the entry is expired.
*/
public boolean isExpired(Duration ttl) {
return Duration.between(createdAt, Instant.now()).compareTo(ttl) > 0;
}
/**
* Checks if the entry is stale (not accessed recently).
*/
public boolean isStale(Duration idleTime) {
return Duration.between(lastAccessedAt, Instant.now()).compareTo(idleTime) > 0;
}
}
/**
* Cache statistics record.
*
* @param hitCount number of cache hits
* @param missCount number of cache misses
* @param evictionCount number of cache evictions
* @param loadCount number of cache loads
* @param hitRate cache hit rate (0.0 to 1.0)
* @param averageLoadTime average time to load data
* @param size current cache size
* @param totalAccessCount total number of cache accesses
*/
public record CacheStats(
long hitCount,
long missCount,
long evictionCount,
long loadCount,
double hitRate,
double averageLoadTime,
int size,
long totalAccessCount
) {
/**
* Creates a summary string.
*/
public String getSummary() {
return String.format(
"""
Cache Statistics:
- Hit Rate: %.2f%%
- Total Hits: %d
- Total Misses: %d
- Evictions: %d
- Cache Size: %d
- Average Load Time: %.2f ms
- Total Accesses: %d
""",
hitRate * 100, hitCount, missCount, evictionCount,
size, averageLoadTime, totalAccessCount
);
}
}
private final Map<String, CacheEntry> cache;
private final ExecutorService loadingExecutor;
private final ScheduledExecutorService maintenanceExecutor;
private final int maxSize;
private final Duration ttl;
private final Duration idleTime;
// Statistics
private final LongAdder hitCount = new LongAdder();
private final LongAdder missCount = new LongAdder();
private final LongAdder evictionCount = new LongAdder();
private final LongAdder loadCount = new LongAdder();
private final LongAdder totalLoadTime = new LongAdder();
private volatile boolean closed = false;
/**
* Creates a new modern cache with default settings.
*/
public ModernSearchCache() {
this(1000, Duration.ofMinutes(10), Duration.ofMinutes(5));
}
/**
* Creates a new modern cache with custom settings.
*
* @param maxSize maximum number of entries
* @param ttl time to live for entries
* @param idleTime maximum idle time before eviction
*/
public ModernSearchCache(int maxSize, Duration ttl, Duration idleTime) {
this.maxSize = maxSize;
this.ttl = ttl;
this.idleTime = idleTime;
this.cache = new ConcurrentHashMap<>();
// Use virtual threads if available, otherwise use cached thread pool
this.loadingExecutor = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r, "Cache-Loader");
t.setDaemon(true);
return t;
});
this.maintenanceExecutor = Executors.newScheduledThreadPool(1, r -> {
Thread t = new Thread(r, "Cache-Maintenance");
t.setDaemon(true);
return t;
});
// Schedule periodic maintenance
scheduleMaintenance();
}
private void scheduleMaintenance() {
// Periodic cleanup of expired and stale entries
maintenanceExecutor.scheduleWithFixedDelay(
this::performMaintenance,
1, 1, TimeUnit.MINUTES
);
// Periodic statistics logging
maintenanceExecutor.scheduleWithFixedDelay(
() -> LOGGER.info(getStats().getSummary()),
5, 5, TimeUnit.MINUTES
);
}
private void performMaintenance() {
if (closed) return;
int removed = 0;
Iterator<Map.Entry<String, CacheEntry>> iterator = cache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, CacheEntry> entry = iterator.next();
CacheEntry cacheEntry = entry.getValue();
if (cacheEntry.isExpired(ttl) || cacheEntry.isStale(idleTime)) {
iterator.remove();
evictionCount.increment();
removed++;
}
}
if (removed > 0) {
LOGGER.fine("Maintenance: Removed " + removed + " expired/stale entries");
}
// Size-based eviction if needed
if (cache.size() > maxSize) {
performSizeBasedEviction();
}
}
private void performSizeBasedEviction() {
int toRemove = cache.size() - maxSize;
if (toRemove <= 0) return;
// Remove least recently accessed entries
List<Map.Entry<String, CacheEntry>> entries = new ArrayList<>(cache.entrySet());
entries.sort(Comparator.comparing(e -> e.getValue().lastAccessedAt));
for (int i = 0; i < toRemove && i < entries.size(); i++) {
cache.remove(entries.get(i).getKey());
evictionCount.increment();
}
LOGGER.fine("Size-based eviction: Removed " + toRemove + " entries");
}
/**
* Gets a value from the cache or loads it if not present.
*
* @param key the cache key
* @param loader function to load the value if not cached
* @return the cached or loaded value
*/
public List<Material> get(String key, Function<String, List<Material>> loader) {
Objects.requireNonNull(key, "Key cannot be null");
Objects.requireNonNull(loader, "Loader cannot be null");
ensureNotClosed();
CacheEntry entry = cache.get(key);
if (entry != null && !entry.isExpired(ttl)) {
// Cache hit
hitCount.increment();
cache.put(key, entry.recordAccess());
return new ArrayList<>(entry.value);
}
// Cache miss
missCount.increment();
// Load synchronously
long startTime = System.currentTimeMillis();
List<Material> value = loader.apply(key);
long loadTime = System.currentTimeMillis() - startTime;
loadCount.increment();
totalLoadTime.add(loadTime);
if (value != null && !value.isEmpty()) {
put(key, value);
}
return value != null ? new ArrayList<>(value) : new ArrayList<>();
}
/**
* Gets a value from the cache or loads it asynchronously.
*
* @param key the cache key
* @param asyncLoader async function to load the value
* @return CompletableFuture with the result
*/
public CompletableFuture<List<Material>> getAsync(
String key,
Function<String, CompletableFuture<List<Material>>> asyncLoader) {
Objects.requireNonNull(key, "Key cannot be null");
Objects.requireNonNull(asyncLoader, "Async loader cannot be null");
ensureNotClosed();
CacheEntry entry = cache.get(key);
if (entry != null && !entry.isExpired(ttl)) {
// Cache hit
hitCount.increment();
cache.put(key, entry.recordAccess());
return CompletableFuture.completedFuture(new ArrayList<>(entry.value));
}
// Cache miss - load asynchronously
missCount.increment();
long startTime = System.currentTimeMillis();
return asyncLoader.apply(key)
.thenApply(value -> {
long loadTime = System.currentTimeMillis() - startTime;
loadCount.increment();
totalLoadTime.add(loadTime);
if (value != null && !value.isEmpty()) {
put(key, value);
}
return value != null ? new ArrayList<>(value) : new ArrayList<>();
});
}
/**
* Puts a value in the cache.
*
* @param key the cache key
* @param value the value to cache
*/
public void put(String key, List<Material> value) {
Objects.requireNonNull(key, "Key cannot be null");
Objects.requireNonNull(value, "Value cannot be null");
ensureNotClosed();
// Check size limit
if (cache.size() >= maxSize && !cache.containsKey(key)) {
performSizeBasedEviction();
}
cache.put(key, CacheEntry.of(value));
}
/**
* Invalidates a cache entry.
*
* @param key the key to invalidate
* @return true if an entry was removed
*/
public boolean invalidate(String key) {
ensureNotClosed();
CacheEntry removed = cache.remove(key);
if (removed != null) {
evictionCount.increment();
}
return removed != null;
}
/**
* Invalidates all cache entries.
*/
public void invalidateAll() {
ensureNotClosed();
int size = cache.size();
cache.clear();
evictionCount.add(size);
LOGGER.info("Cache cleared: " + size + " entries invalidated");
}
/**
* Invalidates entries matching a predicate.
*
* @param predicate the predicate to test keys
* @return number of entries invalidated
*/
public int invalidateIf(java.util.function.Predicate<String> predicate) {
ensureNotClosed();
int removed = 0;
Iterator<String> iterator = cache.keySet().iterator();
while (iterator.hasNext()) {
if (predicate.test(iterator.next())) {
iterator.remove();
evictionCount.increment();
removed++;
}
}
return removed;
}
/**
* Refreshes a cache entry asynchronously.
*
* @param key the key to refresh
* @param loader the loader function
* @return CompletableFuture with the refreshed value
*/
public CompletableFuture<List<Material>> refresh(
String key,
Function<String, List<Material>> loader) {
return CompletableFuture.supplyAsync(() -> {
invalidate(key);
return get(key, loader);
}, loadingExecutor);
}
/**
* Warms up the cache with predefined keys.
*
* @param keys keys to warm up
* @param loader the loader function
* @return CompletableFuture that completes when warm-up is done
*/
public CompletableFuture<Void> warmUp(
Collection<String> keys,
Function<String, List<Material>> loader) {
List<CompletableFuture<Void>> futures = keys.stream()
.map(key -> CompletableFuture.runAsync(
() -> get(key, loader),
loadingExecutor
))
.toList();
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}
/**
* Gets the current cache size.
*
* @return number of cached entries
*/
public int size() {
return cache.size();
}
/**
* Checks if the cache is empty.
*
* @return true if no entries are cached
*/
public boolean isEmpty() {
return cache.isEmpty();
}
/**
* Checks if a key is cached.
*
* @param key the key to check
* @return true if the key is cached and not expired
*/
public boolean containsKey(String key) {
CacheEntry entry = cache.get(key);
return entry != null && !entry.isExpired(ttl);
}
/**
* Gets cache statistics.
*
* @return current cache statistics
*/
public CacheStats getStats() {
long hits = hitCount.sum();
long misses = missCount.sum();
long total = hits + misses;
double hitRate = total > 0 ? (double) hits / total : 0.0;
double avgLoadTime = loadCount.sum() > 0
? (double) totalLoadTime.sum() / loadCount.sum()
: 0.0;
return new CacheStats(
hits,
misses,
evictionCount.sum(),
loadCount.sum(),
hitRate,
avgLoadTime,
cache.size(),
total
);
}
/**
* Resets cache statistics.
*/
public void resetStats() {
hitCount.reset();
missCount.reset();
evictionCount.reset();
loadCount.reset();
totalLoadTime.reset();
LOGGER.info("Cache statistics reset");
}
private void ensureNotClosed() {
if (closed) {
throw new IllegalStateException("Cache has been closed");
}
}
@Override
public void close() {
if (!closed) {
closed = true;
// Log final statistics
LOGGER.info("Closing cache. Final stats: " + getStats().getSummary());
// Clear cache
cache.clear();
// Shutdown executors
loadingExecutor.shutdown();
maintenanceExecutor.shutdown();
try {
if (!loadingExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
loadingExecutor.shutdownNow();
}
if (!maintenanceExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
maintenanceExecutor.shutdownNow();
}
} catch (InterruptedException e) {
loadingExecutor.shutdownNow();
maintenanceExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}