Reputation: 9385
I had a Map<Range<Double>, String>
that checks where a particular Double
value (score) is mapped to a String
(level). The end users want to be able to dynamically change this mapping, in the long term we would like for there to be a web based GUI
where they control this but for the short term they're happy for a file to be in S3
and to editing that whenever a change is needed. I don't want to hit S3
for each request and want to cache this as it doesn't change too frequently(Once a week or so). I don't want to have to make a code change and bounce my service either.
Here is what I have come up with -
public class Mapper() {
private LoadingCache<Score, String> scoreToLevelCache;
public Mapper() {
scoreToLevelCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<Score, String>() {
public String load(Score score) {
Map<Range<Double>, String> scoreToLevelMap = readMappingFromS3(); //readMappingFromS3 omitted for brevity
for(Range<Double> key : scoreToLevelMap.keySet()) {
if(key.contains(score.getCount())) { return scoreToLevelMap.get(key); }
}
throw new IllegalArgumentException("The score couldn't be mapped to a level. Either the score passed in was incorrect or the mapping is incorrect");
}
});
}
public String getContentLevelForScore(Score Score) {
try {
return scoreToLevelCache.get(Score);
} catch (ExecutionException e) { throw new InternalServerException(e); }
}
}
The obvious problem with this approach is in the load
method when I do
Map<Range<Double>, String> scoreToLevelMap = readMappingFromS3();
For each key I'm loading the entire map over and over. This isn't a performance issue but it could become one when the size increases, in any case this is not an efficient approach.
I think that keeping this entire map in the cache would be better, but I'm not sure how to do that here. Can anyone help with this or suggest a more elegant way of achieving this.
Upvotes: 8
Views: 7301
Reputation: 4681
so far i couldn't find a way where you could dynamically store map value cached by guava, looks all values needs to be loaded once during initialization of cached map. while you requires to have a map loaded over time rather a solution is to use "PassiveExpiringMap" from org.apache.commons.collections4.map library
private static Map<String, String> cachedMap = new PassiveExpiringMap<>(30, TimeUnit.SECONDS);
Which cache keys for given time as they got added to the map.
Upvotes: 0
Reputation: 26180
Here is a solution that reads entire map at once and keeps refreshing it asynchronouzly as needed via asyncReloading.
It would return old value during refresh without blocking multiple reader threads like Suppliers.memoizeWithExpiration
does.
private static final Object DUMMY_KEY = new Object();
private static final LoadingCache<Object, Map<Range<Double>, String>> scoreToLevelCache =
CacheBuilder.newBuilder()
.maximumSize(1)
.refreshAfterWrite(10, TimeUnit.MINUTES)
.build(CacheLoader.asyncReloading(
new CacheLoader<Object, Map<Range<Double>, String>>() {
public Map<Range<Double>, String> load(Object key) {
return readMappingFromS3();
}
},
Executors.newSingleThreadExecutor()));
public String getContentLevelForScore(Score score) {
try {
Map<Range<Double>, String> scoreMap = scoreToLevelCache.get(DUMMY_KEY);
// traverse scoreMap ranges
return level;
} catch (ExecutionException e) {
Throwables.throwIfUnchecked(e);
throw new IllegalStateException(e);
}
}
Also consider replacing Map<Range<Double>, String>
with RangeMap<Double, String>
to perform effective ranged lookups.
Upvotes: 1
Reputation: 3113
Guava has a different mechanism for "a cache that only ever contains one value"; it's called Suppliers.memoizeWithExpiration
.
private Supplier<Map<Range<Double>, String> cachedMap =
Suppliers.memoizeWithExpiration(
new Supplier<Map<Range<Double>, String>() {
public Map<Range<Double>, String> get() {
return readMappingFromS3();
}
}, 10, TimeUnit.MINUTES);
public String getContentLevelForScore(Score score) {
Map<Range<Double>, String> scoreMap = cachedMap.get();
// etc.
}
Upvotes: 9
Reputation: 757
Do not mix caching and business logic. Unless your score mapping is huge AND you can load individual pieces, e.g. using readMappingFromS3(Double d) - simply cache the whole map.
public static final String MAGIC_WORD = "oh please please give me my data!!!";
private final LoadingCache<String, Map<Range<Double>, String>> scoreToLevelCache;
public Mapper() {
scoreToLevelCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, Map<Range<Double>, String>>() {
public Map<Range<Double>, String> load(String score) {
return readMappingFromS3(); //readMappingFromS3 omitted for brevity
}
});
}
public Map<Range<Double>, String> getScoreMap() {
try {
return scoreToLevelCache.get(MAGIC_WORD);
} catch (ExecutionException e) {
throw new InternalServerException(e);
}
}
Fetch level name like this
public String findLevel(final Double score) {
final Map<Range<Double>, String> scoreMap = getScoreMap();
for (final Range<Double> key : scoreMap.keySet()) {
if (key.contains(score)) {
return scoreMap.get(key);
}
}
...
}
Upvotes: 2