nikhil
nikhil

Reputation: 9385

How do I store a Map in a Guava Cache

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

Answers (4)

Pravin Bansal
Pravin Bansal

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

Vadzim
Vadzim

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

David
David

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

harshtuna
harshtuna

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

Related Questions