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.
Guava Caches store values in RAM.
Cache entries are manually added using get(Object, Callable) or put(Object, Object) , and are stored in the cache until either evicted or manually invalidated. Implementations of this interface are expected to be thread-safe, and can be safely accessed by multiple concurrent threads.
A CacheMap is a Map that supports caching. This interface will be eventually replaced by the javax. cache.
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.
}
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);
}
}
...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With