com.nesscomputing.cache.guava.GuavaCacheAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.nesscomputing.cache.guava.GuavaCacheAdapter.java

Source

/**
 * Copyright (C) 2012 Ness Computing, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.nesscomputing.cache.guava;

import static com.google.common.base.Predicates.in;
import static com.google.common.base.Predicates.not;

import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;

import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.inject.TypeLiteral;

import org.joda.time.DateTime;
import org.joda.time.Duration;

import com.nesscomputing.cache.NamespacedCache;
import com.nesscomputing.logging.Log;

/**
 * Provides a Guava Cache implementation backed by a namespaced NessCache.
 * @see GuavaCacheModuleBuilder
 */
class GuavaCacheAdapter<K, V> implements LoadingCache<K, V> {
    private static final String NULL_KEY = "null key";
    private static final Log LOG = Log.findLog();

    @SuppressWarnings("rawtypes")
    private static final CacheLoader NO_LOADER = new CacheLoader<Object, Object>() {
        @Override
        public Object load(Object key) throws Exception {
            throw new UnsupportedOperationException("This cache was not configured with a loader");
        }
    };

    private final NamespacedCache cache;
    private final TypeLiteral<K> kClass;
    private final Function<? super K, String> keySerializer;
    private final Function<? super V, byte[]> valueSerializer;
    private final Function<byte[], ? extends V> valueDeserializer;
    private final CacheLoader<? super K, ? extends V> loader;
    private final Duration expiry;
    private final Duration expiryJitter;

    private final AtomicBoolean bulkLoadFailed = new AtomicBoolean();

    @SuppressWarnings("unchecked")
    GuavaCacheAdapter(NamespacedCache cache, TypeLiteral<K> kClass, Function<? super K, String> keySerializer,
            Function<? super V, byte[]> valueSerializer, Function<byte[], ? extends V> valueDeserializer,
            CacheLoader<? super K, ? extends V> loader, Duration expiry, Duration expiryJitter) {
        this.cache = cache;
        this.kClass = kClass;
        this.keySerializer = ExceptionWrappingFunction.of(keySerializer);
        this.valueSerializer = ExceptionWrappingFunction.of(valueSerializer);
        this.valueDeserializer = ExceptionWrappingFunction.of(valueDeserializer);
        this.loader = Objects.firstNonNull(loader, NO_LOADER);
        this.expiry = expiry;
        this.expiryJitter = expiryJitter;
    }

    @Override
    public V getIfPresent(Object key) {
        return getAllPresent(Collections.singleton(key)).get(key);
    }

    @Override
    public V get(final K key) throws ExecutionException {
        Preconditions.checkArgument(key != null, NULL_KEY);
        return get(key, new Callable<V>() {
            @Override
            public V call() throws Exception {
                return loader.load(key);
            }
        });
    }

    @Override
    public V get(K key, Callable<? extends V> valueLoader) throws ExecutionException {
        Preconditions.checkArgument(key != null, NULL_KEY);
        V value = getIfPresent(key);
        if (value != null) {
            return value;
        }

        try {
            value = valueLoader.call();
        } catch (Exception e) {
            throw new ExecutionException(e);
        }

        put(key, value);
        return value;
    }

    @Override
    public ImmutableMap<K, V> getAllPresent(Iterable<?> keys) {
        @SuppressWarnings("unchecked")
        Iterable<? extends K> extendedKeys = ImmutableSet.copyOf((Iterable<? extends K>) keys);
        Map<String, ? extends K> keyStrings = Maps.uniqueIndex(extendedKeys, keySerializer);
        Map<String, byte[]> response = cache.get(keyStrings.keySet());

        Builder<K, V> result = ImmutableMap.builder();

        for (Entry<String, byte[]> e : response.entrySet()) {
            K key = keyStrings.get(e.getKey());
            try {
                result.put(key, valueDeserializer.apply(e.getValue()));
            } catch (Exception exc) {
                invalidate(key);
                throw Throwables.propagate(exc);
            }
        }

        return result.build();
    }

    @SuppressWarnings("unchecked") // Safe because the resulting Map is immutable and we only widen the key / narrow the value
    @Override
    public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
        ImmutableMap<K, V> partialResult = getAllPresent(keys);

        Set<? extends K> remaining = ImmutableSet.copyOf(Iterables.filter(keys, not(in(partialResult.keySet()))));

        if (remaining.isEmpty()) {
            return partialResult;
        }

        Map<K, V> loaded = null;
        try {
            if (!bulkLoadFailed.get()) {
                loaded = (Map<K, V>) loader.loadAll(remaining);
            }
        } catch (UnsupportedOperationException e) {
            if (bulkLoadFailed.compareAndSet(false, true)) {
                LOG.warn(e,
                        "Cache loader %s does not support bulk loads, disabling. Could get a nice performance benefit here!",
                        loader);
            }
        } catch (Exception e) {
            LOG.error(e, "Exception from cache loader during getAll");
            return partialResult;
        }

        if (loaded == null) {
            loaded = Maps.newHashMap();
            for (K key : remaining) {
                try {
                    loaded.put(key, loader.load(key));
                } catch (Exception e) {
                    LOG.error(e, "Exception from cache loader during getAll");
                }
            }
        }

        for (Entry<K, V> e : loaded.entrySet()) {
            put(e.getKey(), e.getValue());
        }

        if (!loaded.keySet().containsAll(remaining)) {
            throw new IncompleteCacheLoadException(
                    String.format("loader %s did not return keys %s for request of %s", loader,
                            Sets.difference(remaining, loaded.keySet()), remaining));
        }

        return ImmutableMap.<K, V>builder().putAll(partialResult).putAll(loaded).build();
    }

    @Override
    public void refresh(K key) {
        Preconditions.checkArgument(key != null, NULL_KEY);
        try {
            put(key, loader.load(key));
        } catch (Exception e) {
            LOG.error(e, "Exception from cache loader during refresh");
        }
    }

    @Override
    public void put(K key, V value) {
        Preconditions.checkArgument(key != null, NULL_KEY);
        cache.set(keySerializer.apply(key), valueSerializer.apply(value), getExpiry());
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
            put(entry.getKey(), entry.getValue());
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public void invalidate(Object key) {
        Preconditions.checkArgument(key != null, NULL_KEY);
        // Best we can do... if your key type is Foo<T> and you pass a Foo<V>, you're on your own :-/
        if (kClass.getRawType().isAssignableFrom(key.getClass())) {
            cache.clear(keySerializer.apply((K) key));
        }
    }

    @Override
    public void invalidateAll(Iterable<?> keys) {
        for (Object key : keys) {
            Preconditions.checkArgument(key != null, NULL_KEY);
            invalidate(key);
        }
    }

    @Override
    public void invalidateAll() {
        throw new UnsupportedOperationException();
    }

    @Override
    public long size() {
        throw new UnsupportedOperationException();
    }

    @Override
    public CacheStats stats() {
        throw new UnsupportedOperationException();
    }

    @Override
    public ConcurrentMap<K, V> asMap() {
        throw new UnsupportedOperationException();
    }

    @Override
    public void cleanUp() {
    }

    @Override
    public V getUnchecked(K key) {
        try {
            return get(key);
        } catch (ExecutionException e) {
            throw Throwables.propagate(e);
        }
    }

    @Override
    public V apply(K key) {
        return getUnchecked(key);
    }

    private DateTime getExpiry() {
        if (expiry == null) {
            return DateTime.now().plusYears(10);
        }

        DateTime result = DateTime.now().plus(expiry);

        if (expiryJitter != null) {
            result = result.plus((long) (expiryJitter.getMillis() * (Math.random() * 2.0 - 1.0)));
        }

        return result;
    }

    private static class ExceptionWrappingFunction<A, B> implements Function<A, B> {
        private final Function<A, B> func;

        ExceptionWrappingFunction(Function<A, B> func) {
            this.func = func;
        }

        static <A, B> ExceptionWrappingFunction<A, B> of(Function<A, B> func) {
            return new ExceptionWrappingFunction<A, B>(func);
        }

        @Override
        public B apply(A input) {
            try {
                return func.apply(input);
            } catch (Exception e) {
                throw new UncheckedExecutionException(e);
            }
        }
    }
}