Skip to content

Commit 904897b

Browse files
committed
Fix
1 parent 7867dac commit 904897b

File tree

1 file changed

+75
-32
lines changed

1 file changed

+75
-32
lines changed
Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,129 @@
11
package comasky.service;
22

3-
import com.github.benmanes.caffeine.cache.Cache;
4-
import com.github.benmanes.caffeine.cache.Caffeine;
53
import comasky.config.DashboardConfig;
64
import comasky.rpcClass.dto.GlobalResponse;
75
import io.smallrye.mutiny.Uni;
86
import jakarta.enterprise.context.ApplicationScoped;
97
import jakarta.inject.Inject;
108

11-
import java.time.Duration;
129
import java.util.concurrent.CompletableFuture;
10+
import java.util.concurrent.ConcurrentHashMap;
11+
import java.util.concurrent.locks.ReentrantReadWriteLock;
1312
import java.util.function.Supplier;
1413

1514
/**
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).
2019
*/
2120
@ApplicationScoped
2221
public class CacheProvider {
2322

2423
private static final String RPC_DATA_KEY = "rpc-data";
2524
private static final long MIN_CACHE_DURATION_MS = 100L;
2625
private static final long MILLIS_PER_SECOND = 1000L;
27-
2826

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();
3030

3131
@Inject
3232
public CacheProvider(DashboardConfig config) {
3333
// Calculate cache duration: polling interval minus the configured buffer
3434
long pollingIntervalMs = config.polling().seconds() * MILLIS_PER_SECOND;
3535
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);
4837
}
4938

5039
/**
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.
5342
*
5443
* @param dataSupplier A supplier providing a Uni<GlobalResponse> to fetch fresh data.
5544
* @return A Uni<GlobalResponse> containing either cached or fresh data.
5645
*/
5746
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+
}
6375
}
6476

6577
/**
6678
* Invalidates all entries in the cache.
67-
* Useful for testing or forcing a refresh.
6879
*/
6980
public void invalidateAll() {
70-
cache.invalidateAll();
81+
lock.writeLock().lock();
82+
try {
83+
cache.clear();
84+
} finally {
85+
lock.writeLock().unlock();
86+
}
7187
}
7288

7389
/**
7490
* Returns the estimated number of entries in the cache.
7591
*/
7692
public long estimatedSize() {
77-
return cache.estimatedSize();
93+
lock.readLock().lock();
94+
try {
95+
return cache.size();
96+
} finally {
97+
lock.readLock().unlock();
98+
}
7899
}
79100

80101
/**
81102
* Manually invalidates the RPC data cache entry.
82103
*/
83104
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+
}
85128
}
86129
}

0 commit comments

Comments
 (0)