com.google.walkaround.util.server.appengine.MemcacheTable.java Source code

Java tutorial

Introduction

Here is the source code for com.google.walkaround.util.server.appengine.MemcacheTable.java

Source

/*
 * Copyright 2011 Google Inc. All Rights Reserved.
 * 
 * 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.google.walkaround.util.server.appengine;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.appengine.api.memcache.Expiration;
import com.google.appengine.api.memcache.InvalidValueException;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheService.SetPolicy;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.taskqueue.DeferredTask;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.common.base.Objects;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.walkaround.util.server.RetryHelper.PermanentFailure;
import com.google.walkaround.util.server.RetryHelper.RetryableFailure;
import com.google.walkaround.util.server.appengine.CheckedDatastore.CheckedTransaction;

import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;

/**
 * A wrapper around App Engine's MemcacheService that provides stronger typing and prepends a
 * user-defined prefix to all keys to make it easier to avoid collisions.
 * 
 * @author ohler@google.com (Christian Ohler)
 * 
 * @param <K> key type
 * @param <V> value type
 */
public class MemcacheTable<K extends Serializable, V extends Serializable> {

    public interface Factory {
        <K extends Serializable, V extends Serializable> MemcacheTable<K, V> create(String tag);
    }

    // Guice says "Factory cannot be used as a key; It is not fully specified."
    // Looks like we can't use FactoryModuleBuilder and have to write our own
    // implementation.
    public static class FactoryImpl implements Factory {
        private final MemcacheService service;
        private final Queue deletionQueue;

        @Inject
        public FactoryImpl(MemcacheService service, @MemcacheDeletionQueue Queue deletionQueue) {
            this.service = service;
            this.deletionQueue = deletionQueue;
        }

        @Override
        public <K extends Serializable, V extends Serializable> MemcacheTable<K, V> create(String tag) {
            return new MemcacheTable<K, V>(service, deletionQueue, tag);
        }
    }

    public static class IdentifiableValue<V> {
        private final MemcacheService.IdentifiableValue inner;

        IdentifiableValue(MemcacheService.IdentifiableValue inner) {
            this.inner = checkNotNull(inner, "Null inner");
        }

        @SuppressWarnings("unchecked")
        public V getValue() {
            return (V) inner.getValue();
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + "(" + inner.getValue() + ")";
        }
    }

    private static class PutNullTask implements DeferredTask {
        private static final long serialVersionUID = 483212086178559149L;

        private final TaggedKey<?> key;

        PutNullTask(TaggedKey<?> key) {
            this.key = checkNotNull(key, "Null key");
        }

        @Override
        public void run() {
            log.info("Deferred overwrite for memcache key " + key);
            // We have to put null rather than deleting since what we do here must
            // interfere with a concurrent getIdentifiable/putIfUntouched sequence,
            // and MemcacheService's Javadoc does not specify that putIfUntouched will
            // abort if the value was absent during the lookup and delete has been
            // called between the lookup and putIfUntouched.
            MemcacheServiceFactory.getMemcacheService().put(key, null);
        }
    }

    private static class TaggedKey<K extends Serializable> implements Serializable {
        private static final long serialVersionUID = 267550222157163337L;

        private final String tag;
        @Nullable
        private final K key;

        public TaggedKey(String tag, @Nullable K key) {
            this.tag = checkNotNull(tag, "Null tag");
            this.key = key;
        }

        @Override
        public final boolean equals(Object o) {
            if (o == this) {
                return true;
            }
            if (!(o instanceof TaggedKey)) {
                return false;
            }
            TaggedKey<?> other = (TaggedKey<?>) o;
            return Objects.equal(tag, other.tag) && Objects.equal(key, other.key);
        }

        @Nullable
        public K getKey() {
            return key;
        }

        @SuppressWarnings("unused")
        // Unused at the moment but no reason not to expose.
        public String getTag() {
            return tag;
        }

        @Override
        public final int hashCode() {
            return Objects.hashCode(tag, key);
        }

        @Override
        public String toString() {
            return "TaggedKey(" + tag + ", " + key + ")";
        }
    }

    private static final Logger log = Logger.getLogger(MemcacheTable.class.getName());
    private final MemcacheService service;
    private final String tag;

    private final Queue deletionQueue;

    /**
     * @param tag a unique tag that distinguishes this table from other tables. Since memcache
     *          persists through application reloads, you have to explicitly clear the cache first if
     *          you ever want to re-use a tag for different data.
     */
    @Inject
    public MemcacheTable(MemcacheService service, @MemcacheDeletionQueue Queue deletionQueue,
            @Assisted String tag) {
        this.service = service;
        this.deletionQueue = deletionQueue;
        this.tag = tag;
    }

    public void delete(@Nullable K key) {
        TaggedKey<K> taggedKey = tagKey(key);
        log.info("cache delete " + taggedKey);
        service.delete(taggedKey);
    }

    public void enqueuePutNull(CheckedTransaction tx, @Nullable K key) throws RetryableFailure, PermanentFailure {
        tx.enqueueTask(deletionQueue, TaskOptions.Builder.withPayload(new PutNullTask(tagKey(key))));
    }

    @Nullable
    public V get(@Nullable K key) {
        TaggedKey<K> taggedKey = tagKey(key);
        Object rawValue;
        try {
            rawValue = service.get(taggedKey);
        } catch (InvalidValueException e) {
            // Probably a deserialization error (incompatible serialVersionUID or similar).
            log.log(Level.WARNING, "Error getting object from memcache, key: " + key, e);
            return null;
        }
        if (rawValue == null) {
            log.info("cache miss " + taggedKey);
            return null;
        } else {
            log.info("cache hit " + taggedKey + " = " + rawValue);
            // TODO(ohler): check actual type
            return castRawValue(rawValue);
        }
    }

    public Map<K, V> getAll(Set<K> keys) {
        Set<TaggedKey<K>> taggedKeys = Sets.newHashSetWithExpectedSize(keys.size());
        for (K key : keys) {
            taggedKeys.add(tagKey(key));
        }
        Map<TaggedKey<K>, Object> rawMappings;
        try {
            rawMappings = service.getAll(taggedKeys);
        } catch (InvalidValueException e) {
            // Probably a deserialization error (incompatible serialVersionUID or similar).
            log.log(Level.WARNING, "Error getting objects from memcache, keys: " + keys, e);
            return Collections.emptyMap();
        }

        Map<K, V> mappings = Maps.newHashMapWithExpectedSize(rawMappings.size());
        for (Map.Entry<TaggedKey<K>, Object> entry : rawMappings.entrySet()) {
            mappings.put(entry.getKey().getKey(), castRawValue(entry.getValue()));
        }

        log.info("Found " + mappings.size() + " of " + keys.size() + " objects in memcache: " + mappings);

        return mappings;
    }

    @Nullable
    public IdentifiableValue<V> getIdentifiable(@Nullable K key) {
        TaggedKey<K> taggedKey = tagKey(key);
        MemcacheService.IdentifiableValue rawValue;
        try {
            rawValue = service.getIdentifiable(taggedKey);
        } catch (InvalidValueException e) {
            // Probably a deserialization error (incompatible serialVersionUID or similar).
            log.log(Level.WARNING, "Error getting object from memcache, key: " + key, e);
            return null;
        }
        if (rawValue == null) {
            log.info("cache miss " + taggedKey);
            return null;
        } else {
            log.info("cache hit " + taggedKey + " = " + rawValue);
            return new IdentifiableValue<V>(rawValue);
        }
    }

    public void put(@Nullable K key, @Nullable V value) {
        put(key, value, null);
    }

    public void put(@Nullable K key, @Nullable V value, @Nullable Expiration expires) {
        put(key, value, expires, SetPolicy.SET_ALWAYS);
    }

    /**
     * @return true if a new entry was created, false if not because of the policy.
     */
    public boolean put(@Nullable K key, @Nullable V value, @Nullable Expiration expires, SetPolicy policy) {
        TaggedKey<K> taggedKey = tagKey(key);
        String expiresString = expires == null ? null : "" + expires.getMillisecondsValue();
        log.info("cache put " + taggedKey + " = " + value + ", " + expiresString + ", " + policy);
        return service.put(taggedKey, value, expires, policy);
    }

    /**
     * @return the set of keys for which new entries were created (some may not have been created
     *         because of the policy).
     */
    public Set<K> putAll(Map<K, V> mappings, @Nullable Expiration expires, SetPolicy policy) {
        Map<TaggedKey<K>, V> rawMappings = Maps.newHashMapWithExpectedSize(mappings.size());
        for (Map.Entry<K, V> entry : mappings.entrySet()) {
            rawMappings.put(tagKey(entry.getKey()), entry.getValue());
        }
        Set<TaggedKey<K>> rawResult = service.putAll(rawMappings, expires, policy);
        Set<K> result = Sets.newHashSetWithExpectedSize(rawResult.size());
        for (TaggedKey<K> key : rawResult) {
            result.add(key.getKey());
        }
        return result;
    }

    /**
     * NOTE: This differs from {@link MemcacheService} in that it allows oldValue to be null, in which
     * case it will put unconditionally.
     */
    public boolean putIfUntouched(@Nullable K key, IdentifiableValue<V> oldValue, @Nullable V newValue) {
        return putIfUntouched(key, oldValue, newValue, null);
    }

    /**
     * NOTE: This differs from {@link MemcacheService} in that it allows oldValue to be null, in which
     * case it will put unconditionally.
     */
    public boolean putIfUntouched(@Nullable K key, IdentifiableValue<V> oldValue, @Nullable V newValue,
            @Nullable Expiration expires) {
        TaggedKey<K> taggedKey = tagKey(key);
        String expiresString = expires == null ? null : "" + expires.getMillisecondsValue();
        boolean result;
        if (oldValue == null) {
            service.put(taggedKey, newValue, expires);
            result = true;
        } else {
            result = service.putIfUntouched(taggedKey, oldValue.inner, newValue, expires);
        }
        log.info("cache putIfUntouched " + taggedKey + " = " + newValue + ", " + expiresString + ": " + result);
        return result;
    }

    // Cast is safe under the assumption that tag is not re-used for a different
    // type
    @SuppressWarnings("unchecked")
    @Nullable
    private V castRawValue(Object rawValue) {
        return (V) rawValue;
    }

    private TaggedKey<K> tagKey(@Nullable K key) {
        return new TaggedKey<K>(tag, key);
    }

    // TODO(ohler): Add more methods here to match MemcacheService.
}