|
1 | 1 | package comasky.service; |
2 | 2 |
|
3 | | -import com.github.benmanes.caffeine.cache.Cache; |
4 | | -import com.github.benmanes.caffeine.cache.Caffeine; |
5 | 3 | import comasky.config.DashboardConfig; |
6 | 4 | import comasky.rpcClass.dto.GlobalResponse; |
7 | 5 | import io.smallrye.mutiny.Uni; |
8 | 6 | import jakarta.enterprise.context.ApplicationScoped; |
9 | 7 | import jakarta.inject.Inject; |
10 | 8 |
|
11 | | -import java.time.Duration; |
12 | 9 | import java.util.concurrent.CompletableFuture; |
| 10 | +import java.util.concurrent.ConcurrentHashMap; |
| 11 | +import java.util.concurrent.locks.ReentrantReadWriteLock; |
13 | 12 | import java.util.function.Supplier; |
14 | 13 |
|
15 | 14 | /** |
16 | | - * Provides a configured, high-performance cache for RPC data. |
17 | | - * |
18 | | - * Stores results as {@link CompletableFuture} so callers can easily consume the cached |
19 | | - * response asynchronously without requiring Caffeine's AsyncCache implementation. |
| 15 | + * Provides a native-image compatible cache for RPC data. |
| 16 | + * |
| 17 | + * Uses a simple concurrent map with TTL instead of Caffeine (which generates |
| 18 | + * dynamic classes incompatible with GraalVM native image). |
20 | 19 | */ |
21 | 20 | @ApplicationScoped |
22 | 21 | public class CacheProvider { |
23 | 22 |
|
24 | 23 | private static final String RPC_DATA_KEY = "rpc-data"; |
25 | 24 | private static final long MIN_CACHE_DURATION_MS = 100L; |
26 | 25 | private static final long MILLIS_PER_SECOND = 1000L; |
27 | | - |
28 | 26 |
|
29 | | - private final Cache<String, CompletableFuture<GlobalResponse>> cache; |
| 27 | + private final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>(); |
| 28 | + private final long cacheDurationMs; |
| 29 | + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); |
30 | 30 |
|
31 | 31 | @Inject |
32 | 32 | public CacheProvider(DashboardConfig config) { |
33 | 33 | // Calculate cache duration: polling interval minus the configured buffer |
34 | 34 | long pollingIntervalMs = config.polling().seconds() * MILLIS_PER_SECOND; |
35 | 35 | long bufferMs = config.cache().validityBufferMs(); |
36 | | - long cacheDurationMs = Math.max(MIN_CACHE_DURATION_MS, pollingIntervalMs - bufferMs); |
37 | | - |
38 | | - // Optimized Caffeine cache configuration |
39 | | - this.cache = Caffeine.newBuilder() |
40 | | - // Automatic expiration after write |
41 | | - .expireAfterWrite(Duration.ofMillis(cacheDurationMs)) |
42 | | - // Limit maximum size to prevent unbounded growth |
43 | | - .maximumSize(config.cache().maxItems()) |
44 | | - // Record cache statistics for monitoring |
45 | | - .recordStats() |
46 | | - // Build a synchronous cache (native-image friendly) |
47 | | - .build(); |
| 36 | + this.cacheDurationMs = Math.max(MIN_CACHE_DURATION_MS, pollingIntervalMs - bufferMs); |
48 | 37 | } |
49 | 38 |
|
50 | 39 | /** |
51 | | - * Retrieves data from the cache. If the data is not present, it will be fetched |
52 | | - * using the provided data supplier, populated into the cache, and then returned. |
| 40 | + * Retrieves data from the cache. If the data is not present or expired, |
| 41 | + * it will be fetched using the provided data supplier. |
53 | 42 | * |
54 | 43 | * @param dataSupplier A supplier providing a Uni<GlobalResponse> to fetch fresh data. |
55 | 44 | * @return A Uni<GlobalResponse> containing either cached or fresh data. |
56 | 45 | */ |
57 | 46 | public Uni<GlobalResponse> getCachedData(Supplier<Uni<GlobalResponse>> dataSupplier) { |
58 | | - // Get or compute from cache - efficient non-blocking operation |
59 | | - CompletableFuture<GlobalResponse> future = cache.get(RPC_DATA_KEY, key -> |
60 | | - dataSupplier.get().subscribeAsCompletionStage() |
61 | | - ); |
62 | | - return Uni.createFrom().completionStage(future); |
| 47 | + lock.readLock().lock(); |
| 48 | + try { |
| 49 | + CacheEntry entry = cache.get(RPC_DATA_KEY); |
| 50 | + if (entry != null && !entry.isExpired()) { |
| 51 | + // Return cached future |
| 52 | + return Uni.createFrom().completionStage(entry.future); |
| 53 | + } |
| 54 | + } finally { |
| 55 | + lock.readLock().unlock(); |
| 56 | + } |
| 57 | + |
| 58 | + // Cache miss or expired - fetch new data |
| 59 | + lock.writeLock().lock(); |
| 60 | + try { |
| 61 | + // Double-check after acquiring write lock |
| 62 | + CacheEntry entry = cache.get(RPC_DATA_KEY); |
| 63 | + if (entry != null && !entry.isExpired()) { |
| 64 | + return Uni.createFrom().completionStage(entry.future); |
| 65 | + } |
| 66 | + |
| 67 | + // Compute and cache the result |
| 68 | + CompletableFuture<GlobalResponse> future = dataSupplier.get() |
| 69 | + .subscribeAsCompletionStage(); |
| 70 | + cache.put(RPC_DATA_KEY, new CacheEntry(future)); |
| 71 | + return Uni.createFrom().completionStage(future); |
| 72 | + } finally { |
| 73 | + lock.writeLock().unlock(); |
| 74 | + } |
63 | 75 | } |
64 | 76 |
|
65 | 77 | /** |
66 | 78 | * Invalidates all entries in the cache. |
67 | | - * Useful for testing or forcing a refresh. |
68 | 79 | */ |
69 | 80 | public void invalidateAll() { |
70 | | - cache.invalidateAll(); |
| 81 | + lock.writeLock().lock(); |
| 82 | + try { |
| 83 | + cache.clear(); |
| 84 | + } finally { |
| 85 | + lock.writeLock().unlock(); |
| 86 | + } |
71 | 87 | } |
72 | 88 |
|
73 | 89 | /** |
74 | 90 | * Returns the estimated number of entries in the cache. |
75 | 91 | */ |
76 | 92 | public long estimatedSize() { |
77 | | - return cache.estimatedSize(); |
| 93 | + lock.readLock().lock(); |
| 94 | + try { |
| 95 | + return cache.size(); |
| 96 | + } finally { |
| 97 | + lock.readLock().unlock(); |
| 98 | + } |
78 | 99 | } |
79 | 100 |
|
80 | 101 | /** |
81 | 102 | * Manually invalidates the RPC data cache entry. |
82 | 103 | */ |
83 | 104 | public void invalidateRpcData() { |
84 | | - cache.invalidate(RPC_DATA_KEY); |
| 105 | + lock.writeLock().lock(); |
| 106 | + try { |
| 107 | + cache.remove(RPC_DATA_KEY); |
| 108 | + } finally { |
| 109 | + lock.writeLock().unlock(); |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + /** |
| 114 | + * Internal cache entry wrapper with expiration timestamp. |
| 115 | + */ |
| 116 | + private class CacheEntry { |
| 117 | + final CompletableFuture<GlobalResponse> future; |
| 118 | + final long expirationTime; |
| 119 | + |
| 120 | + CacheEntry(CompletableFuture<GlobalResponse> future) { |
| 121 | + this.future = future; |
| 122 | + this.expirationTime = System.currentTimeMillis() + cacheDurationMs; |
| 123 | + } |
| 124 | + |
| 125 | + boolean isExpired() { |
| 126 | + return System.currentTimeMillis() > expirationTime; |
| 127 | + } |
85 | 128 | } |
86 | 129 | } |
0 commit comments