Package com.anthavio.cache

Source Code of com.anthavio.cache.SpyMemcache

package com.anthavio.cache;

import java.io.IOException;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import net.spy.memcached.ConnectionFactory;
import net.spy.memcached.MemcachedClient;
import net.spy.memcached.internal.CheckedOperationTimeoutException;

import org.apache.commons.codec.digest.DigestUtils;

import com.anthavio.httl.util.Cutils;

/**
* SpyMemcached implementation
*
* TODO constructor is too lenghty - introduce Builder
*
* @author martin.vanek
*
*/
public class SpyMemcache<V extends Serializable> extends CacheBase<V> {

  public static final int MINUTE = 60; //seconds
  public static final int HOUR = 60 * MINUTE;//seconds
  public static final int DAY = 24 * HOUR;//seconds
  public static final int MONTH = 30 * DAY; //Memcached maximum value as interval. Greater value is considered as epoch time (seconds after 1.1.1970T00:00:00)

  private static final int MaxKeyLength = 250; //Memcached limit for key

  private static final String VERSION_IN = "1"; //Must be String "1" - number cannot be incremented - Memcached is ASSHOLE

  private final MemcachedClient client;

  private final boolean clientCreated;

  private final boolean namespaceVersioning;

  private long operationTimeout = 5000; //Memcached call timeout in milliseconds

  public SpyMemcache(String name, ConnectionFactory connectionFactory, List<InetSocketAddress> addrs,
      boolean namespaceVersioning) throws IOException {
    super(name);
    if (Cutils.isBlank(name)) {
      throw new IllegalArgumentException("SpyRequestCache name must not be blank");
    }
    this.client = new MemcachedClient(connectionFactory, addrs);
    this.clientCreated = true;
    this.operationTimeout = connectionFactory.getOperationTimeout();
    this.namespaceVersioning = namespaceVersioning;
  }

  public SpyMemcache(String name, MemcachedClient client, long operationTimeout, TimeUnit unitOfTimeout) {
    this(name, client, operationTimeout, unitOfTimeout, false);
  }

  public SpyMemcache(String name, MemcachedClient client) {
    this(name, client, 5, TimeUnit.SECONDS, false);
  }

  public SpyMemcache(String name, MemcachedClient client, boolean namespaceVersioning) {
    this(name, client, 5, TimeUnit.SECONDS, namespaceVersioning);
  }

  public SpyMemcache(String name, MemcachedClient client, long operationTimeout, TimeUnit unitOfTimeout,
      boolean namespaceVersioning) {
    super(name);
    if (Cutils.isBlank(name)) {
      throw new IllegalArgumentException("SpyRequestCache name must not be blank");
    }
    this.client = client;
    this.clientCreated = false;
    this.operationTimeout = unitOfTimeout.toMillis(operationTimeout);
    this.namespaceVersioning = namespaceVersioning;
  }

  /**
   * @return Spy Memcached client
   */
  public MemcachedClient getClient() {
    return client;
  }

  /**
   * @return Memcached timeout for operation
   */
  public long getOperationTimeout() {
    return operationTimeout;
  }

  public void setOperationTimeout(long timeout, TimeUnit unit) {
    this.operationTimeout = unit.toMillis(timeout);
    if (this.operationTimeout < 1000) {
      throw new IllegalArgumentException("Operation timeout " + operationTimeout + " must be >= 1 second");
    }
  }

  @Override
  protected CacheEntry<V> doGet(String cacheKey) throws Exception {
    if (cacheKey.length() > MaxKeyLength) {
      throw new IllegalArgumentException("Key length exceded maximum " + MaxKeyLength);
    }
    Future<Object> future = client.asyncGet(cacheKey);
    try {
      return (CacheEntry<V>) future.get(operationTimeout, TimeUnit.MILLISECONDS);
    } catch (CheckedOperationTimeoutException cotx) {
      logger.warn("GET operation timeout: " + operationTimeout + " millis, Key: " + cacheKey);
      return null;
    }

  }

  @Override
  protected Boolean doSet(String cacheKey, CacheEntry<V> entry) throws Exception {
    if (cacheKey.length() > MaxKeyLength) {
      throw new IllegalArgumentException("Key length exceded maximum " + MaxKeyLength);
    }
    int ttlMillis = (int) entry.getHardTtl();
    Future<Boolean> future = client.set(cacheKey, ttlMillis, entry);
    try {
      return future.get(operationTimeout, TimeUnit.MILLISECONDS);
    } catch (CheckedOperationTimeoutException cotx) {
      logger.warn("SET operation timeout: " + operationTimeout + " millis, Key: " + cacheKey);
      return false;
    }
  }

  @Override
  protected Boolean doRemove(String cacheKey) throws Exception {
    if (cacheKey.length() > MaxKeyLength) {
      throw new IllegalArgumentException("Key length exceded maximum " + MaxKeyLength);
    }
    Future<Boolean> future = client.delete(cacheKey);
    try {
      return future.get(operationTimeout, TimeUnit.MILLISECONDS);
    } catch (CheckedOperationTimeoutException cotx) {
      logger.warn("DELETE operation timeout: " + operationTimeout + " millis, Key: " + cacheKey);
      return null;
    }
  }

  @Override
  public String getCacheKey(String userKey) {
    String namespace = getNamespace();
    //use MD5 hash if the key is too long, but keep the namespace
    if (namespace.length() + userKey.length() > MaxKeyLength) {
      return namespace + ":" + DigestUtils.md5Hex(userKey);
    } else {
      return namespace + ":" + userKey;
    }
  }

  /**
   * Changing namespace by incrementing it's version results in (virtally) removing all entries means 
   * because namespace is part of key - all keys became invalid
   */
  @Override
  public void removeAll() throws IllegalStateException {
    if (namespaceVersioning) {
      String nsVersionKey = getNsVersionKey();
      long incr = client.incr(nsVersionKey, 1);
      if (incr == -1) {//nsVersion entry does not not exist -> create it
        try {
          Future<Boolean> future = client.set(nsVersionKey, DAY, VERSION_IN);
          future.get(operationTimeout, TimeUnit.MILLISECONDS);
        } catch (Exception x) {
          logger.warn("Failed to removeAll", x);
        }
      }
      logger.debug("Namespace version set to " + getName() + ":" + incr);
    } else {
      throw new IllegalStateException("Cannot remove all. Namespace versioning is not enabled.");
    }
  }

  @Override
  public void close() {
    super.close();
    //if client instance was given to us (in constructor), we are NOT in charge of shutting it down
    if (clientCreated) {
      client.shutdown();
    }
  }

  /**
   * @return Key for namespace version of this cache
   */
  private String getNsVersionKey() {
    return "namespace-version:" + getName();
  }

  /**
   * Namespace is used as prefix for every key to avoid key clashes with other caches
   */
  public String getNamespace() {
    if (namespaceVersioning) {
      return getName() + ":" + getNsVersion();
    } else {
      return getName();
    }
  }

  private String getNsVersion() {
    //TODO cache nsVersion localy for X seconds - use read/write lock on last checked timestamp
    String nsVersionKey = getNsVersionKey();
    try {
      Future<Object> gfuture = client.asyncGet(nsVersionKey);
      String nsVersion = (String) gfuture.get(operationTimeout, TimeUnit.MILLISECONDS);
      int icnt = 0;
      while (nsVersion == null && ++icnt < 5) {
        Future<Boolean> afuture = client.add(nsVersionKey, DAY, VERSION_IN);
        boolean added = afuture.get(operationTimeout, TimeUnit.MILLISECONDS);
        if (!added) { //somebody else was faster...so get what's there
          gfuture = client.asyncGet(nsVersionKey);
          nsVersion = (String) gfuture.get(operationTimeout, TimeUnit.MILLISECONDS);
        }
      }

      if (nsVersion == null) {
        throw new IllegalStateException("Failed to obtain namespace version for " + nsVersionKey + " in " + icnt
            + " attempts");
      }
      return nsVersion;
    } catch (Exception x) {
      throw new IllegalStateException("Failed to obtain namespace version for " + nsVersionKey, x);
    }
  }

  //default Transcoder is SerializingTranscoder
  /*
  private static class CacheEntryTranscoder implements Transcoder<CacheEntry<CachedResponse>> {

    @Override
    public boolean asyncDecode(CachedData d) {
      return false;
    }

    @Override
    public CachedData encode(CacheEntry<CachedResponse> o) {
      return null;
    }

    @Override
    public CacheEntry<CachedResponse> decode(CachedData d) {
      return null;
    }

    @Override
    public int getMaxSize() {
      return 0;
    }
  }
  */

TOP

Related Classes of com.anthavio.cache.SpyMemcache

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.