Java tutorial
/** * Copyright (c) 2008 Greg Whalin * All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the BSD license * * This library is distributed in the hope that it will be * useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. * * You should have received a copy of the BSD License along with this * library. * * @author Greg Whalin <greg@meetup.com> */ package com.meetup.memcached; import java.util.*; import java.util.zip.*; import java.nio.*; import java.nio.channels.*; import java.io.*; import java.net.URLEncoder; import org.apache.commons.lang.CharEncoding; import org.apache.log4j.Logger; /** * This is a Memcached client for the Java platform available from * <a href="http:/www.danga.com/memcached/">http://www.danga.com/memcached/</a>. * <br/> * Supports setting, adding, replacing, deleting compressed/uncompressed and<br/> * serialized (can be stored as string if object is native class) objects to memcached.<br/> * <br/> * Now pulls SockIO objects from SockIOPool, which is a connection pool. The server failover<br/> * has also been moved into the SockIOPool class.<br/> * This pool needs to be initialized prior to the client working. See javadocs from SockIOPool.<br/> * <br/> * Some examples of use follow.<br/> * <h3>To create cache client object and set params:</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * * // compression is enabled by default * mc.setCompressEnable(true); * * // set compression threshhold to 4 KB (default: 15 KB) * mc.setCompressThreshold(4096); * * // turn on storing primitive types as a string representation * // Should not do this in most cases. * mc.setPrimitiveAsString(true); * </pre> * <h3>To store an object:</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String key = "cacheKey1"; * Object value = SomeClass.getObject(); * mc.set(key, value); * </pre> * <h3>To store an object using a custom server hashCode:</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String key = "cacheKey1"; * Object value = SomeClass.getObject(); * Integer hash = Integer.valueOf(45); * mc.set(key, value, hash); * </pre> * The set method shown above will always set the object in the cache.<br/> * The add and replace methods do the same, but with a slight difference.<br/> * <ul> * <li>add -- will store the object only if the server does not have an entry for this key</li> * <li>replace -- will store the object only if the server already has an entry for this key</li> * </ul> * <h3>To delete a cache entry:</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String key = "cacheKey1"; * mc.delete(key); * </pre> * <h3>To delete a cache entry using a custom hash code:</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String key = "cacheKey1"; * Integer hash = Integer.valueOf(45); * mc.delete(key, hashCode); * </pre> * <h3>To store a counter and then increment or decrement that counter:</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String key = "counterKey"; * mc.storeCounter(key, Integer.valueOf(100)); * System.out.println("counter after adding 1: " mc.incr(key)); * System.out.println("counter after adding 5: " mc.incr(key, 5)); * System.out.println("counter after subtracting 4: " mc.decr(key, 4)); * System.out.println("counter after subtracting 1: " mc.decr(key)); * </pre> * <h3>To store a counter and then increment or decrement that counter with custom hash:</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String key = "counterKey"; * Integer hash = Integer.valueOf(45); * mc.storeCounter(key, Integer.valueOf(100), hash); * System.out.println("counter after adding 1: " mc.incr(key, 1, hash)); * System.out.println("counter after adding 5: " mc.incr(key, 5, hash)); * System.out.println("counter after subtracting 4: " mc.decr(key, 4, hash)); * System.out.println("counter after subtracting 1: " mc.decr(key, 1, hash)); * </pre> * <h3>To retrieve an object from the cache:</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String key = "key"; * Object value = mc.get(key); * </pre> * <h3>To retrieve an object from the cache with custom hash:</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String key = "key"; * Integer hash = Integer.valueOf(45); * Object value = mc.get(key, hash); * </pre> * <h3>To retrieve an multiple objects from the cache</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String[] keys = { "key", "key1", "key2" }; * Map<Object> values = mc.getMulti(keys); * </pre> * <h3>To retrieve an multiple objects from the cache with custom hashing</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * String[] keys = { "key", "key1", "key2" }; * Integer[] hashes = { Integer.valueOf(45), Integer.valueOf(32), Integer.valueOf(44) }; * Map<Object> values = mc.getMulti(keys, hashes); * </pre> * <h3>To flush all items in server(s)</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * mc.flushAll(); * </pre> * <h3>To get stats from server(s)</h3> * <pre> * MemcachedClient mc = new MemcachedClient(); * Map stats = mc.stats(); * </pre> * * @author greg whalin <greg@meetup.com> * @author Richard 'toast' Russo <russor@msoe.edu> * @author Kevin Burton <burton@peerfear.org> * @author Robert Watts <robert@wattsit.co.uk> * @author Vin Chawla <vin@tivo.com> * @version 1.5 */ public class MemcachedClient { // logger private static Logger log = Logger.getLogger(MemcachedClient.class.getName()); // return codes private static final String VALUE = "VALUE"; // start of value line from server private static final String STATS = "STAT"; // start of stats line from server private static final String ITEM = "ITEM"; // start of item line from server private static final String DELETED = "DELETED"; // successful deletion private static final String NOTFOUND = "NOT_FOUND"; // record not found for delete or incr/decr private static final String STORED = "STORED"; // successful store of data private static final String NOTSTORED = "NOT_STORED"; // data not stored private static final String OK = "OK"; // success private static final String END = "END"; // end of data from server private static final String ERROR = "ERROR"; // invalid command name from client private static final String CLIENT_ERROR = "CLIENT_ERROR"; // client error in input line - invalid protocol private static final String SERVER_ERROR = "SERVER_ERROR"; // server error private static final byte[] B_END = "END\r\n".getBytes(); private static final byte[] B_NOTFOUND = "NOT_FOUND\r\n".getBytes(); private static final byte[] B_DELETED = "DELETED\r\r".getBytes(); private static final byte[] B_STORED = "STORED\r\r".getBytes(); // default compression threshold private static final int COMPRESS_THRESH = 30720; // values for cache flags public static final int MARKER_BYTE = 1; public static final int MARKER_BOOLEAN = 8192; public static final int MARKER_INTEGER = 4; public static final int MARKER_LONG = 16384; public static final int MARKER_CHARACTER = 16; public static final int MARKER_STRING = 32; public static final int MARKER_STRINGBUFFER = 64; public static final int MARKER_FLOAT = 128; public static final int MARKER_SHORT = 256; public static final int MARKER_DOUBLE = 512; public static final int MARKER_DATE = 1024; public static final int MARKER_STRINGBUILDER = 2048; public static final int MARKER_BYTEARR = 4096; public static final int F_COMPRESSED = 2; public static final int F_SERIALIZED = 8; // flags private boolean sanitizeKeys; private boolean primitiveAsString; private boolean compressEnable; private long compressThreshold; private String defaultEncoding; // pool instance private SockIOPool pool; // which pool to use private String poolName; // optional passed in classloader private ClassLoader classLoader; // optional error handler private ErrorHandler errorHandler; /** * Creates a new instance of MemCachedClient. */ public MemcachedClient() { init(); } /** * Creates a new instance of MemCachedClient * accepting a passed in pool name. * * @param poolName name of SockIOPool */ public MemcachedClient(String poolName) { this.poolName = poolName; init(); } /** * Creates a new instance of MemCacheClient but * acceptes a passed in ClassLoader. * * @param classLoader ClassLoader object. */ public MemcachedClient(ClassLoader classLoader) { this.classLoader = classLoader; init(); } /** * Creates a new instance of MemCacheClient but * acceptes a passed in ClassLoader and a passed * in ErrorHandler. * * @param classLoader ClassLoader object. * @param errorHandler ErrorHandler object. */ public MemcachedClient(ClassLoader classLoader, ErrorHandler errorHandler) { this.classLoader = classLoader; this.errorHandler = errorHandler; init(); } /** * Creates a new instance of MemCacheClient but * acceptes a passed in ClassLoader, ErrorHandler, * and SockIOPool name. * * @param classLoader ClassLoader object. * @param errorHandler ErrorHandler object. * @param poolName SockIOPool name */ public MemcachedClient(ClassLoader classLoader, ErrorHandler errorHandler, String poolName) { this.classLoader = classLoader; this.errorHandler = errorHandler; this.poolName = poolName; init(); } /** * Initializes client object to defaults. * * This enables compression and sets compression threshhold to 15 KB. */ private void init() { this.sanitizeKeys = true; this.primitiveAsString = false; this.compressEnable = true; this.compressThreshold = COMPRESS_THRESH; this.defaultEncoding = CharEncoding.UTF_8; this.poolName = (this.poolName == null) ? "default" : this.poolName; // get a pool instance to work with for the life of this instance this.pool = SockIOPool.getInstance(poolName); } /** * Sets an optional ClassLoader to be used for * serialization. * * @param classLoader */ public void setClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } /** * Sets an optional ErrorHandler. * * @param errorHandler */ public void setErrorHandler(ErrorHandler errorHandler) { this.errorHandler = errorHandler; } /** * Enables/disables sanitizing keys by URLEncoding. * * @param sanitizeKeys if true, then URLEncode all keys */ public void setSanitizeKeys(boolean sanitizeKeys) { this.sanitizeKeys = sanitizeKeys; } /** * Enables storing primitive types as their String values. * * @param primitiveAsString if true, then store all primitives as their string value. */ public void setPrimitiveAsString(boolean primitiveAsString) { this.primitiveAsString = primitiveAsString; } /** * Sets default String encoding when storing primitives as Strings. * Default is UTF-8. * * @param defaultEncoding */ public void setDefaultEncoding(String defaultEncoding) { this.defaultEncoding = defaultEncoding; } /** * Enable storing compressed data, provided it meets the threshold requirements. * * If enabled, data will be stored in compressed form if it is<br/> * longer than the threshold length set with setCompressThreshold(int)<br/> *<br/> * The default is that compression is enabled.<br/> *<br/> * Even if compression is disabled, compressed data will be automatically<br/> * decompressed. * * @param compressEnable <CODE>true</CODE> to enable compression, <CODE>false</CODE> to disable compression */ public void setCompressEnable(boolean compressEnable) { this.compressEnable = compressEnable; } /** * Sets the required length for data to be considered for compression. * * If the length of the data to be stored is not equal or larger than this value, it will * not be compressed. * * This defaults to 15 KB. * * @param compressThreshold required length of data to consider compression */ public void setCompressThreshold(long compressThreshold) { this.compressThreshold = compressThreshold; } /** * Checks to see if key exists in cache. * * @param key the key to look for * @return true if key found in cache, false if not (or if cache is down) */ public boolean keyExists(String key) { return (this.get(key, null, true) != null); } /** * Deletes an object from cache given cache key. * * @param key the key to be removed * @return <code>true</code>, if the data was deleted successfully */ public boolean delete(String key) { return delete(key, null, null); } /** * Deletes an object from cache given cache key and expiration date. * * @param key the key to be removed * @param expiry when to expire the record. * @return <code>true</code>, if the data was deleted successfully */ public boolean delete(String key, Date expiry) { return delete(key, null, expiry); } /** * Deletes an object from cache given cache key, a delete time, and an optional hashcode. * * The item is immediately made non retrievable.<br/> * Keep in mind {@link #add(String, Object) add} and {@link #replace(String, Object) replace}<br/> * will fail when used with the same key will fail, until the server reaches the<br/> * specified time. However, {@link #set(String, Object) set} will succeed,<br/> * and the new value will not be deleted. * * @param key the key to be removed * @param hashCode if not null, then the int hashcode to use * @param expiry when to expire the record. * @return <code>true</code>, if the data was deleted successfully */ public boolean delete(String key, Integer hashCode, Date expiry) { if (key == null) { log.error("null value for key passed to delete()"); return false; } try { key = sanitizeKey(key); } catch (UnsupportedEncodingException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnDelete(this, e, key); log.error("failed to sanitize your key!", e); return false; } // get SockIO obj from hash or from key SockIOPool.SockIO sock = pool.getSock(key, hashCode); // return false if unable to get SockIO obj if (sock == null) { if (errorHandler != null) errorHandler.handleErrorOnDelete(this, new IOException("no socket to server available"), key); return false; } // build command StringBuilder command = new StringBuilder("delete ").append(key); if (expiry != null) command.append(" " + expiry.getTime() / 1000); command.append("\r\n"); try { sock.write(command.toString().getBytes()); sock.flush(); // if we get appropriate response back, then we return true String line = sock.readLine(); if (DELETED.equals(line)) { if (log.isInfoEnabled()) log.info("++++ deletion of key: " + key + " from cache was a success"); // return sock to pool and bail here sock.close(); sock = null; return true; } else if (NOTFOUND.equals(line)) { if (log.isInfoEnabled()) log.info("++++ deletion of key: " + key + " from cache failed as the key was not found"); } else { log.error("++++ error deleting key: " + key); log.error("++++ server response: " + line); } } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnDelete(this, e, key); // exception thrown log.error("++++ exception thrown while writing bytes to server on delete"); log.error(e.getMessage(), e); try { sock.trueClose(); } catch (IOException ioe) { log.error("++++ failed to close socket : " + sock.toString()); } sock = null; } if (sock != null) { sock.close(); sock = null; } return false; } /** * Stores data on the server; only the key and the value are specified. * * @param key key to store data under * @param value value to store * @return true, if the data was successfully stored */ public boolean set(String key, Object value) { return set("set", key, value, null, null, primitiveAsString); } /** * Stores data on the server; only the key and the value are specified. * * @param key key to store data under * @param value value to store * @param hashCode if not null, then the int hashcode to use * @return true, if the data was successfully stored */ public boolean set(String key, Object value, Integer hashCode) { return set("set", key, value, null, hashCode, primitiveAsString); } /** * Stores data on the server; the key, value, and an expiration time are specified. * * @param key key to store data under * @param value value to store * @param expiry when to expire the record * @return true, if the data was successfully stored */ public boolean set(String key, Object value, Date expiry) { return set("set", key, value, expiry, null, primitiveAsString); } /** * Stores data on the server; the key, value, and an expiration time are specified. * * @param key key to store data under * @param value value to store * @param expiry when to expire the record * @param hashCode if not null, then the int hashcode to use * @return true, if the data was successfully stored */ public boolean set(String key, Object value, Date expiry, Integer hashCode) { return set("set", key, value, expiry, hashCode, primitiveAsString); } /** * Adds data to the server; only the key and the value are specified. * * @param key key to store data under * @param value value to store * @return true, if the data was successfully stored */ public boolean add(String key, Object value) { return set("add", key, value, null, null, primitiveAsString); } /** * Adds data to the server; the key, value, and an optional hashcode are passed in. * * @param key key to store data under * @param value value to store * @param hashCode if not null, then the int hashcode to use * @return true, if the data was successfully stored */ public boolean add(String key, Object value, Integer hashCode) { return set("add", key, value, null, hashCode, primitiveAsString); } /** * Adds data to the server; the key, value, and an expiration time are specified. * * @param key key to store data under * @param value value to store * @param expiry when to expire the record * @return true, if the data was successfully stored */ public boolean add(String key, Object value, Date expiry) { return set("add", key, value, expiry, null, primitiveAsString); } /** * Adds data to the server; the key, value, and an expiration time are specified. * * @param key key to store data under * @param value value to store * @param expiry when to expire the record * @param hashCode if not null, then the int hashcode to use * @return true, if the data was successfully stored */ public boolean add(String key, Object value, Date expiry, Integer hashCode) { return set("add", key, value, expiry, hashCode, primitiveAsString); } /** * Updates data on the server; only the key and the value are specified. * * @param key key to store data under * @param value value to store * @return true, if the data was successfully stored */ public boolean replace(String key, Object value) { return set("replace", key, value, null, null, primitiveAsString); } /** * Updates data on the server; only the key and the value and an optional hash are specified. * * @param key key to store data under * @param value value to store * @param hashCode if not null, then the int hashcode to use * @return true, if the data was successfully stored */ public boolean replace(String key, Object value, Integer hashCode) { return set("replace", key, value, null, hashCode, primitiveAsString); } /** * Updates data on the server; the key, value, and an expiration time are specified. * * @param key key to store data under * @param value value to store * @param expiry when to expire the record * @return true, if the data was successfully stored */ public boolean replace(String key, Object value, Date expiry) { return set("replace", key, value, expiry, null, primitiveAsString); } /** * Updates data on the server; the key, value, and an expiration time are specified. * * @param key key to store data under * @param value value to store * @param expiry when to expire the record * @param hashCode if not null, then the int hashcode to use * @return true, if the data was successfully stored */ public boolean replace(String key, Object value, Date expiry, Integer hashCode) { return set("replace", key, value, expiry, hashCode, primitiveAsString); } /** * Stores data to cache. * * If data does not already exist for this key on the server, or if the key is being<br/> * deleted, the specified value will not be stored.<br/> * The server will automatically delete the value when the expiration time has been reached.<br/> * <br/> * If compression is enabled, and the data is longer than the compression threshold<br/> * the data will be stored in compressed form.<br/> * <br/> * As of the current release, all objects stored will use java serialization. * * @param cmdname action to take (set, add, replace) * @param key key to store cache under * @param value object to cache * @param expiry expiration * @param hashCode if not null, then the int hashcode to use * @param asString store this object as a string? * @return true/false indicating success */ private boolean set(String cmdname, String key, Object value, Date expiry, Integer hashCode, boolean asString) { if (cmdname == null || cmdname.trim().equals("") || key == null) { log.error("key is null or cmd is null/empty for set()"); return false; } try { key = sanitizeKey(key); } catch (UnsupportedEncodingException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnSet(this, e, key); log.error("failed to sanitize your key!", e); return false; } if (value == null) { log.warn("trying to store a null value to cache: " + key); return false; } // get SockIO obj SockIOPool.SockIO sock = pool.getSock(key, hashCode); if (sock == null) { if (errorHandler != null) errorHandler.handleErrorOnSet(this, new IOException("no socket to server available"), key); return false; } if (expiry == null) expiry = new Date(0); // store flags int flags = 0; // byte array to hold data byte[] val; if (NativeHandler.isHandled(value)) { if (asString) { // useful for sharing data between java and non-java // and also for storing ints for the increment method try { if (log.isInfoEnabled()) log.info("++++ storing data as a string for key: " + key + " for class: " + value.getClass().getName()); val = value.toString().getBytes(defaultEncoding); } catch (UnsupportedEncodingException ue) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnSet(this, ue, key); log.error("invalid encoding type used: " + defaultEncoding, ue); sock.close(); sock = null; return false; } } else { try { if (log.isInfoEnabled()) log.info("Storing with native handler..."); flags |= NativeHandler.getMarkerFlag(value); val = NativeHandler.encode(value); } catch (Exception e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnSet(this, e, key); log.error("Failed to native handle obj", e); sock.close(); sock = null; return false; } } } else { // always serialize for non-primitive types try { if (log.isInfoEnabled()) log.info("++++ serializing for key: " + key + " for class: " + value.getClass().getName()); ByteArrayOutputStream bos = new ByteArrayOutputStream(); (new ObjectOutputStream(bos)).writeObject(value); val = bos.toByteArray(); flags |= F_SERIALIZED; } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnSet(this, e, key); // if we fail to serialize, then // we bail log.error("failed to serialize obj", e); log.error(value.toString()); // return socket to pool and bail sock.close(); sock = null; return false; } } // now try to compress if we want to // and if the length is over the threshold if (compressEnable && val.length > compressThreshold) { try { if (log.isInfoEnabled()) { log.info("++++ trying to compress data"); log.info("++++ size prior to compression: " + val.length); } ByteArrayOutputStream bos = new ByteArrayOutputStream(val.length); GZIPOutputStream gos = new GZIPOutputStream(bos); gos.write(val, 0, val.length); gos.finish(); // store it and set compression flag val = bos.toByteArray(); flags |= F_COMPRESSED; if (log.isInfoEnabled()) log.info("++++ compression succeeded, size after: " + val.length); } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnSet(this, e, key); log.error("IOException while compressing stream: " + e.getMessage()); log.error("storing data uncompressed"); } } // now write the data to the cache server try { String cmd = String.format("%s %s %d %d %d\r\n", cmdname, key, flags, (expiry.getTime() / 1000), val.length); sock.write(cmd.getBytes()); sock.write(val); sock.write("\r\n".getBytes()); sock.flush(); // get result code String line = sock.readLine(); if (log.isInfoEnabled()) log.info("++++ memcache cmd (result code): " + cmd + " (" + line + ")"); if (STORED.equals(line)) { if (log.isInfoEnabled()) log.info("++++ data successfully stored for key: " + key); sock.close(); sock = null; return true; } else if (NOTSTORED.equals(line)) { if (log.isInfoEnabled()) log.info("++++ data not stored in cache for key: " + key); } else { log.error("++++ error storing data in cache for key: " + key + " -- length: " + val.length); log.error("++++ server response: " + line); } } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnSet(this, e, key); // exception thrown log.error("++++ exception thrown while writing bytes to server on set"); log.error(e.getMessage(), e); try { sock.trueClose(); } catch (IOException ioe) { log.error("++++ failed to close socket : " + sock.toString()); } sock = null; } if (sock != null) { sock.close(); sock = null; } return false; } /** * Store a counter to memcached given a key * * @param key cache key * @param counter number to store * @return true/false indicating success */ public boolean storeCounter(String key, long counter) { return set("set", key, Long.valueOf(counter), null, null, true); } /** * Store a counter to memcached given a key * * @param key cache key * @param counter number to store * @return true/false indicating success */ public boolean storeCounter(String key, Long counter) { return set("set", key, counter, null, null, true); } /** * Store a counter to memcached given a key * * @param key cache key * @param counter number to store * @param hashCode if not null, then the int hashcode to use * @return true/false indicating success */ public boolean storeCounter(String key, Long counter, Integer hashCode) { return set("set", key, counter, null, hashCode, true); } /** * Returns value in counter at given key as long. * * @param key cache ket * @return counter value or -1 if not found */ public long getCounter(String key) { return getCounter(key, null); } /** * Returns value in counter at given key as long. * * @param key cache ket * @param hashCode if not null, then the int hashcode to use * @return counter value or -1 if not found */ public long getCounter(String key, Integer hashCode) { if (key == null) { log.error("null key for getCounter()"); return -1; } long counter = -1; try { counter = Long.parseLong((String) get(key, hashCode, true)); } catch (Exception ex) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, ex, key); // not found or error getting out if (log.isInfoEnabled()) log.info(String.format("Failed to parse Long value for key: %s", key)); } return counter; } /** * Thread safe way to initialize and increment a counter. * * @param key key where the data is stored * @return value of incrementer */ public long addOrIncr(String key) { return addOrIncr(key, 0, null); } /** * Thread safe way to initialize and increment a counter. * * @param key key where the data is stored * @param inc value to set or increment by * @return value of incrementer */ public long addOrIncr(String key, long inc) { return addOrIncr(key, inc, null); } /** * Thread safe way to initialize and increment a counter. * * @param key key where the data is stored * @param inc value to set or increment by * @param hashCode if not null, then the int hashcode to use * @return value of incrementer */ public long addOrIncr(String key, long inc, Integer hashCode) { boolean ret = set("add", key, Long.valueOf(inc), null, hashCode, true); if (ret) { return inc; } else { return incrdecr("incr", key, inc, hashCode); } } /** * Thread safe way to initialize and decrement a counter. * * @param key key where the data is stored * @return value of incrementer */ public long addOrDecr(String key) { return addOrDecr(key, 0, null); } /** * Thread safe way to initialize and decrement a counter. * * @param key key where the data is stored * @param inc value to set or increment by * @return value of incrementer */ public long addOrDecr(String key, long inc) { return addOrDecr(key, inc, null); } /** * Thread safe way to initialize and decrement a counter. * * @param key key where the data is stored * @param inc value to set or increment by * @param hashCode if not null, then the int hashcode to use * @return value of incrementer */ public long addOrDecr(String key, long inc, Integer hashCode) { boolean ret = set("add", key, Long.valueOf(inc), null, hashCode, true); if (ret) { return inc; } else { return incrdecr("decr", key, inc, hashCode); } } /** * Increment the value at the specified key by 1, and then return it. * * @param key key where the data is stored * @return -1, if the key is not found, the value after incrementing otherwise */ public long incr(String key) { return incrdecr("incr", key, 1, null); } /** * Increment the value at the specified key by passed in val. * * @param key key where the data is stored * @param inc how much to increment by * @return -1, if the key is not found, the value after incrementing otherwise */ public long incr(String key, long inc) { return incrdecr("incr", key, inc, null); } /** * Increment the value at the specified key by the specified increment, and then return it. * * @param key key where the data is stored * @param inc how much to increment by * @param hashCode if not null, then the int hashcode to use * @return -1, if the key is not found, the value after incrementing otherwise */ public long incr(String key, long inc, Integer hashCode) { return incrdecr("incr", key, inc, hashCode); } /** * Decrement the value at the specified key by 1, and then return it. * * @param key key where the data is stored * @return -1, if the key is not found, the value after incrementing otherwise */ public long decr(String key) { return incrdecr("decr", key, 1, null); } /** * Decrement the value at the specified key by passed in value, and then return it. * * @param key key where the data is stored * @param inc how much to increment by * @return -1, if the key is not found, the value after incrementing otherwise */ public long decr(String key, long inc) { return incrdecr("decr", key, inc, null); } /** * Decrement the value at the specified key by the specified increment, and then return it. * * @param key key where the data is stored * @param inc how much to increment by * @param hashCode if not null, then the int hashcode to use * @return -1, if the key is not found, the value after incrementing otherwise */ public long decr(String key, long inc, Integer hashCode) { return incrdecr("decr", key, inc, hashCode); } /** * Increments/decrements the value at the specified key by inc. * * Note that the server uses a 32-bit unsigned integer, and checks for<br/> * underflow. In the event of underflow, the result will be zero. Because<br/> * Java lacks unsigned types, the value is returned as a 64-bit integer.<br/> * The server will only decrement a value if it already exists;<br/> * if a value is not found, -1 will be returned. * * @param cmdname increment/decrement * @param key cache key * @param inc amount to incr or decr * @param hashCode if not null, then the int hashcode to use * @return new value or -1 if not exist */ private long incrdecr(String cmdname, String key, long inc, Integer hashCode) { if (key == null) { log.error("null key for incrdecr()"); return -1; } try { key = sanitizeKey(key); } catch (UnsupportedEncodingException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error("failed to sanitize your key!", e); return -1; } // get SockIO obj for given cache key SockIOPool.SockIO sock = pool.getSock(key, hashCode); if (sock == null) { if (errorHandler != null) errorHandler.handleErrorOnSet(this, new IOException("no socket to server available"), key); return -1; } try { String cmd = String.format("%s %s %d\r\n", cmdname, key, inc); if (log.isDebugEnabled()) log.debug("++++ memcache incr/decr command: " + cmd); sock.write(cmd.getBytes()); sock.flush(); // get result back String line = sock.readLine(); if (line.matches("\\d+")) { // return sock to pool and return result sock.close(); try { return Long.parseLong(line); } catch (Exception ex) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, ex, key); log.error(String.format("Failed to parse Long value for key: %s", key)); } } else if (NOTFOUND.equals(line)) { if (log.isInfoEnabled()) log.info("++++ key not found to incr/decr for key: " + key); } else { log.error("++++ error incr/decr key: " + key); log.error("++++ server response: " + line); } } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); // exception thrown log.error("++++ exception thrown while writing bytes to server on incr/decr"); log.error(e.getMessage(), e); try { sock.trueClose(); } catch (IOException ioe) { log.error("++++ failed to close socket : " + sock.toString()); } sock = null; } if (sock != null) { sock.close(); sock = null; } return -1; } /** * Retrieve a key from the server, using a specific hash. * * If the data was compressed or serialized when compressed, it will automatically<br/> * be decompressed or serialized, as appropriate. (Inclusive or)<br/> *<br/> * Non-serialized data will be returned as a string, so explicit conversion to<br/> * numeric types will be necessary, if desired<br/> * * @param key key where data is stored * @return the object that was previously stored, or null if it was not previously stored */ public Object get(String key) { return get(key, null, false); } /** * Retrieve a key from the server, using a specific hash. * * If the data was compressed or serialized when compressed, it will automatically<br/> * be decompressed or serialized, as appropriate. (Inclusive or)<br/> *<br/> * Non-serialized data will be returned as a string, so explicit conversion to<br/> * numeric types will be necessary, if desired<br/> * * @param key key where data is stored * @param hashCode if not null, then the int hashcode to use * @return the object that was previously stored, or null if it was not previously stored */ public Object get(String key, Integer hashCode) { return get(key, hashCode, false); } /** * Retrieve a key from the server, using a specific hash. * * If the data was compressed or serialized when compressed, it will automatically<br/> * be decompressed or serialized, as appropriate. (Inclusive or)<br/> *<br/> * Non-serialized data will be returned as a string, so explicit conversion to<br/> * numeric types will be necessary, if desired<br/> * * @param key key where data is stored * @param hashCode if not null, then the int hashcode to use * @param asString if true, then return string val * @return the object that was previously stored, or null if it was not previously stored */ public Object get(String key, Integer hashCode, boolean asString) { if (key == null) { log.error("key is null for get()"); return null; } try { key = sanitizeKey(key); } catch (UnsupportedEncodingException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error("failed to sanitize your key!", e); return null; } // get SockIO obj using cache key SockIOPool.SockIO sock = pool.getSock(key, hashCode); if (sock == null) { if (errorHandler != null) errorHandler.handleErrorOnGet(this, new IOException("no socket to server available"), key); return null; } try { String cmd = "get " + key + "\r\n"; if (log.isDebugEnabled()) log.debug("++++ memcache get command: " + cmd); sock.write(cmd.getBytes()); sock.flush(); // ready object Object o = null; while (true) { String line = sock.readLine(); if (log.isDebugEnabled()) log.debug("++++ line: " + line); if (line.startsWith(VALUE)) { String[] info = line.split(" "); int flag = Integer.parseInt(info[2]); int length = Integer.parseInt(info[3]); if (log.isDebugEnabled()) { log.debug("++++ key: " + key); log.debug("++++ flags: " + flag); log.debug("++++ length: " + length); } // read obj into buffer byte[] buf = new byte[length]; sock.read(buf); sock.clearEOL(); if ((flag & F_COMPRESSED) == F_COMPRESSED) { try { // read the input stream, and write to a byte array output stream since // we have to read into a byte array, but we don't know how large it // will need to be, and we don't want to resize it a bunch GZIPInputStream gzi = new GZIPInputStream(new ByteArrayInputStream(buf)); ByteArrayOutputStream bos = new ByteArrayOutputStream(buf.length); int count; byte[] tmp = new byte[2048]; while ((count = gzi.read(tmp)) != -1) { bos.write(tmp, 0, count); } // store uncompressed back to buffer buf = bos.toByteArray(); gzi.close(); } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error("++++ IOException thrown while trying to uncompress input stream for key: " + key); log.error(e.getMessage(), e); throw new NestedIOException( "++++ IOException thrown while trying to uncompress input stream for key: " + key, e); } } // we can only take out serialized objects if ((flag & F_SERIALIZED) != F_SERIALIZED) { if (primitiveAsString || asString) { // pulling out string value if (log.isInfoEnabled()) log.info("++++ retrieving object and stuffing into a string."); o = new String(buf, defaultEncoding); } else { // decoding object try { o = NativeHandler.decode(buf, flag); } catch (Exception e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error("++++ Exception thrown while trying to deserialize for key: " + key, e); throw new NestedIOException(e); } } } else { // deserialize if the data is serialized ContextObjectInputStream ois = new ContextObjectInputStream(new ByteArrayInputStream(buf), classLoader); try { o = ois.readObject(); if (log.isInfoEnabled()) log.info("++++ deserializing " + o.getClass()); } catch (ClassNotFoundException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error("++++ ClassNotFoundException thrown while trying to deserialize for key: " + key, e); throw new NestedIOException("+++ failed while trying to deserialize for key: " + key, e); } finally { ois.close(); } } } else if (END.equals(line)) { if (log.isDebugEnabled()) log.debug("++++ finished reading from cache server"); break; } } sock.close(); sock = null; return o; } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); // exception thrown log.error("++++ exception thrown while trying to get object from cache for key: " + key); log.error(e.getMessage(), e); try { sock.trueClose(); } catch (IOException ioe) { log.error("++++ failed to close socket : " + sock.toString()); } sock = null; } if (sock != null) sock.close(); return null; } /** * Retrieve multiple objects from the memcache. * * This is recommended over repeated calls to {@link #get(String) get()}, since it<br/> * is more efficient.<br/> * * @param keys String array of keys to retrieve * @return Object array ordered in same order as key array containing results */ public Object[] getMultiArray(String[] keys) { return getMultiArray(keys, null, false); } /** * Retrieve multiple objects from the memcache. * * This is recommended over repeated calls to {@link #get(String) get()}, since it<br/> * is more efficient.<br/> * * @param keys String array of keys to retrieve * @param hashCodes if not null, then the Integer array of hashCodes * @return Object array ordered in same order as key array containing results */ public Object[] getMultiArray(String[] keys, Integer[] hashCodes) { return getMultiArray(keys, hashCodes, false); } /** * Retrieve multiple objects from the memcache. * * This is recommended over repeated calls to {@link #get(String) get()}, since it<br/> * is more efficient.<br/> * * @param keys String array of keys to retrieve * @param hashCodes if not null, then the Integer array of hashCodes * @param asString if true, retrieve string vals * @return Object array ordered in same order as key array containing results */ public Object[] getMultiArray(String[] keys, Integer[] hashCodes, boolean asString) { Map<String, Object> data = getMulti(keys, hashCodes, asString); if (data == null) return null; Object[] res = new Object[keys.length]; for (int i = 0; i < keys.length; i++) { res[i] = data.get(keys[i]); } return res; } /** * Retrieve multiple objects from the memcache. * * This is recommended over repeated calls to {@link #get(String) get()}, since it<br/> * is more efficient.<br/> * * @param keys String array of keys to retrieve * @return a hashmap with entries for each key is found by the server, * keys that are not found are not entered into the hashmap, but attempting to * retrieve them from the hashmap gives you null. */ public Map<String, Object> getMulti(String[] keys) { return getMulti(keys, null, false); } /** * Retrieve multiple keys from the memcache. * * This is recommended over repeated calls to {@link #get(String) get()}, since it<br/> * is more efficient.<br/> * * @param keys keys to retrieve * @param hashCodes if not null, then the Integer array of hashCodes * @return a hashmap with entries for each key is found by the server, * keys that are not found are not entered into the hashmap, but attempting to * retrieve them from the hashmap gives you null. */ public Map<String, Object> getMulti(String[] keys, Integer[] hashCodes) { return getMulti(keys, hashCodes, false); } /** * Retrieve multiple keys from the memcache. * * This is recommended over repeated calls to {@link #get(String) get()}, since it<br/> * is more efficient.<br/> * * @param keys keys to retrieve * @param hashCodes if not null, then the Integer array of hashCodes * @param asString if true then retrieve using String val * @return a hashmap with entries for each key is found by the server, * keys that are not found are not entered into the hashmap, but attempting to * retrieve them from the hashmap gives you null. */ public Map<String, Object> getMulti(String[] keys, Integer[] hashCodes, boolean asString) { if (keys == null || keys.length == 0) { log.error("missing keys for getMulti()"); return null; } Map<String, StringBuilder> cmdMap = new HashMap<String, StringBuilder>(); for (int i = 0; i < keys.length; ++i) { String key = keys[i]; if (key == null) { log.error("null key, so skipping"); continue; } Integer hash = null; if (hashCodes != null && hashCodes.length > i) hash = hashCodes[i]; String cleanKey = key; try { cleanKey = sanitizeKey(key); } catch (UnsupportedEncodingException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error("failed to sanitize your key!", e); continue; } // get SockIO obj from cache key SockIOPool.SockIO sock = pool.getSock(cleanKey, hash); if (sock == null) { if (errorHandler != null) errorHandler.handleErrorOnGet(this, new IOException("no socket to server available"), key); continue; } // store in map and list if not already if (!cmdMap.containsKey(sock.getHost())) cmdMap.put(sock.getHost(), new StringBuilder("get")); cmdMap.get(sock.getHost()).append(" " + cleanKey); // return to pool sock.close(); } if (log.isInfoEnabled()) log.info("multi get socket count : " + cmdMap.size()); // now query memcache Map<String, Object> ret = new HashMap<String, Object>(keys.length); // now use new NIO implementation (new NIOLoader(this)).doMulti(asString, cmdMap, keys, ret); // fix the return array in case we had to rewrite any of the keys for (String key : keys) { String cleanKey = key; try { cleanKey = sanitizeKey(key); } catch (UnsupportedEncodingException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error("failed to sanitize your key!", e); continue; } if (!key.equals(cleanKey) && ret.containsKey(cleanKey)) { ret.put(key, ret.get(cleanKey)); ret.remove(cleanKey); } // backfill missing keys w/ null value if (!ret.containsKey(key)) ret.put(key, null); } if (log.isDebugEnabled()) log.debug("++++ memcache: got back " + ret.size() + " results"); return ret; } /** * This method loads the data from cache into a Map. * * Pass a SockIO object which is ready to receive data and a HashMap<br/> * to store the results. * * @param sock socket waiting to pass back data * @param hm hashmap to store data into * @param asString if true, and if we are using NativehHandler, return string val * @throws IOException if io exception happens while reading from socket */ private void loadMulti(LineInputStream input, Map<String, Object> hm, boolean asString) throws IOException { while (true) { String line = input.readLine(); if (log.isDebugEnabled()) log.debug("++++ line: " + line); if (line.startsWith(VALUE)) { String[] info = line.split(" "); String key = info[1]; int flag = Integer.parseInt(info[2]); int length = Integer.parseInt(info[3]); if (log.isDebugEnabled()) { log.debug("++++ key: " + key); log.debug("++++ flags: " + flag); log.debug("++++ length: " + length); } // read obj into buffer byte[] buf = new byte[length]; input.read(buf); input.clearEOL(); // ready object Object o; // check for compression if ((flag & F_COMPRESSED) == F_COMPRESSED) { try { // read the input stream, and write to a byte array output stream since // we have to read into a byte array, but we don't know how large it // will need to be, and we don't want to resize it a bunch GZIPInputStream gzi = new GZIPInputStream(new ByteArrayInputStream(buf)); ByteArrayOutputStream bos = new ByteArrayOutputStream(buf.length); int count; byte[] tmp = new byte[2048]; while ((count = gzi.read(tmp)) != -1) { bos.write(tmp, 0, count); } // store uncompressed back to buffer buf = bos.toByteArray(); gzi.close(); } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error( "++++ IOException thrown while trying to uncompress input stream for key: " + key); log.error(e.getMessage(), e); throw new NestedIOException( "++++ IOException thrown while trying to uncompress input stream for key: " + key, e); } } // we can only take out serialized objects if ((flag & F_SERIALIZED) != F_SERIALIZED) { if (primitiveAsString || asString) { // pulling out string value if (log.isInfoEnabled()) log.info("++++ retrieving object and stuffing into a string."); o = new String(buf, defaultEncoding); } else { // decoding object try { o = NativeHandler.decode(buf, flag); } catch (Exception e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error("++++ Exception thrown while trying to deserialize for key: " + key, e); throw new NestedIOException(e); } } } else { // deserialize if the data is serialized ContextObjectInputStream ois = new ContextObjectInputStream(new ByteArrayInputStream(buf), classLoader); try { o = ois.readObject(); if (log.isInfoEnabled()) log.info("++++ deserializing " + o.getClass()); } catch (ClassNotFoundException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(this, e, key); log.error("++++ ClassNotFoundException thrown while trying to deserialize for key: " + key, e); throw new NestedIOException("+++ failed while trying to deserialize for key: " + key, e); } finally { ois.close(); } } // store the object into the cache hm.put(key, o); } else if (END.equals(line)) { if (log.isDebugEnabled()) log.debug("++++ finished reading from cache server"); break; } } } private String sanitizeKey(String key) throws UnsupportedEncodingException { return (sanitizeKeys) ? URLEncoder.encode(key, CharEncoding.UTF_8) : key; } /** * Invalidates the entire cache. * * Will return true only if succeeds in clearing all servers. * * @return success true/false */ public boolean flushAll() { return flushAll(null); } /** * Invalidates the entire cache. * * Will return true only if succeeds in clearing all servers. * If pass in null, then will try to flush all servers. * * @param servers optional array of host(s) to flush (host:port) * @return success true/false */ public boolean flushAll(String[] servers) { // get SockIOPool instance // return false if unable to get SockIO obj if (pool == null) { log.error("++++ unable to get SockIOPool instance"); return false; } // get all servers and iterate over them servers = (servers == null) ? pool.getServers() : servers; // if no servers, then return early if (servers == null || servers.length <= 0) { log.error("++++ no servers to flush"); return false; } boolean success = true; for (int i = 0; i < servers.length; i++) { SockIOPool.SockIO sock = pool.getConnection(servers[i]); if (sock == null) { log.error("++++ unable to get connection to : " + servers[i]); success = false; if (errorHandler != null) errorHandler.handleErrorOnFlush(this, new IOException("no socket to server available")); continue; } // build command String command = "flush_all\r\n"; try { sock.write(command.getBytes()); sock.flush(); // if we get appropriate response back, then we return true String line = sock.readLine(); success = (OK.equals(line)) ? success && true : false; } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnFlush(this, e); // exception thrown log.error("++++ exception thrown while writing bytes to server on flushAll"); log.error(e.getMessage(), e); try { sock.trueClose(); } catch (IOException ioe) { log.error("++++ failed to close socket : " + sock.toString()); } success = false; sock = null; } if (sock != null) { sock.close(); sock = null; } } return success; } /** * Retrieves stats for all servers. * * Returns a map keyed on the servername. * The value is another map which contains stats * with stat name as key and value as value. * * @return Stats map */ public Map stats() { return stats(null); } /** * Retrieves stats for passed in servers (or all servers). * * Returns a map keyed on the servername. * The value is another map which contains stats * with stat name as key and value as value. * * @param servers string array of servers to retrieve stats from, or all if this is null * @return Stats map */ public Map stats(String[] servers) { return stats(servers, "stats\r\n", STATS); } /** * Retrieves stats items for all servers. * * Returns a map keyed on the servername. * The value is another map which contains item stats * with itemname:number:field as key and value as value. * * @return Stats map */ public Map statsItems() { return statsItems(null); } /** * Retrieves stats for passed in servers (or all servers). * * Returns a map keyed on the servername. * The value is another map which contains item stats * with itemname:number:field as key and value as value. * * @param servers string array of servers to retrieve stats from, or all if this is null * @return Stats map */ public Map statsItems(String[] servers) { return stats(servers, "stats items\r\n", STATS); } /** * Retrieves stats items for all servers. * * Returns a map keyed on the servername. * The value is another map which contains slabs stats * with slabnumber:field as key and value as value. * * @return Stats map */ public Map statsSlabs() { return statsSlabs(null); } /** * Retrieves stats for passed in servers (or all servers). * * Returns a map keyed on the servername. * The value is another map which contains slabs stats * with slabnumber:field as key and value as value. * * @param servers string array of servers to retrieve stats from, or all if this is null * @return Stats map */ public Map statsSlabs(String[] servers) { return stats(servers, "stats slabs\r\n", STATS); } /** * Retrieves items cachedump for all servers. * * Returns a map keyed on the servername. * The value is another map which contains cachedump stats * with the cachekey as key and byte size and unix timestamp as value. * * @param slabNumber the item number of the cache dump * @return Stats map */ public Map statsCacheDump(int slabNumber, int limit) { return statsCacheDump(null, slabNumber, limit); } /** * Retrieves stats for passed in servers (or all servers). * * Returns a map keyed on the servername. * The value is another map which contains cachedump stats * with the cachekey as key and byte size and unix timestamp as value. * * @param servers string array of servers to retrieve stats from, or all if this is null * @param slabNumber the item number of the cache dump * @return Stats map */ public Map statsCacheDump(String[] servers, int slabNumber, int limit) { return stats(servers, String.format("stats cachedump %d %d\r\n", slabNumber, limit), ITEM); } private Map stats(String[] servers, String command, String lineStart) { if (command == null || command.trim().equals("")) { log.error("++++ invalid / missing command for stats()"); return null; } // get all servers and iterate over them servers = (servers == null) ? pool.getServers() : servers; // if no servers, then return early if (servers == null || servers.length <= 0) { log.error("++++ no servers to check stats"); return null; } // array of stats Maps Map<String, Map> statsMaps = new HashMap<String, Map>(); for (int i = 0; i < servers.length; i++) { SockIOPool.SockIO sock = pool.getConnection(servers[i]); if (sock == null) { log.error("++++ unable to get connection to : " + servers[i]); if (errorHandler != null) errorHandler.handleErrorOnStats(this, new IOException("no socket to server available")); continue; } // build command try { sock.write(command.getBytes()); sock.flush(); // map to hold key value pairs Map<String, String> stats = new HashMap<String, String>(); // loop over results while (true) { String line = sock.readLine(); if (log.isDebugEnabled()) log.debug("++++ line: " + line); if (line.startsWith(lineStart)) { String[] info = line.split(" ", 3); String key = info[1]; String value = info[2]; if (log.isDebugEnabled()) { log.debug("++++ key : " + key); log.debug("++++ value: " + value); } stats.put(key, value); } else if (END.equals(line)) { // finish when we get end from server if (log.isDebugEnabled()) log.debug("++++ finished reading from cache server"); break; } else if (line.startsWith(ERROR) || line.startsWith(CLIENT_ERROR) || line.startsWith(SERVER_ERROR)) { log.error("++++ failed to query stats"); log.error("++++ server response: " + line); break; } statsMaps.put(servers[i], stats); } } catch (IOException e) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnStats(this, e); // exception thrown log.error("++++ exception thrown while writing bytes to server on stats"); log.error(e.getMessage(), e); try { sock.trueClose(); } catch (IOException ioe) { log.error("++++ failed to close socket : " + sock.toString()); } sock = null; } if (sock != null) { sock.close(); sock = null; } } return statsMaps; } protected final class NIOLoader { protected Selector selector; protected int numConns = 0; protected MemcachedClient mc; protected Connection[] conns; public NIOLoader(MemcachedClient mc) { this.mc = mc; } private final class Connection { public List<ByteBuffer> incoming = new ArrayList<ByteBuffer>(); public ByteBuffer outgoing; public SockIOPool.SockIO sock; public SocketChannel channel; private boolean isDone = false; public Connection(SockIOPool.SockIO sock, StringBuilder request) throws IOException { if (log.isDebugEnabled()) log.debug("setting up connection to " + sock.getHost()); this.sock = sock; outgoing = ByteBuffer.wrap(request.append("\r\n").toString().getBytes()); channel = sock.getChannel(); if (channel == null) throw new IOException("dead connection to: " + sock.getHost()); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_WRITE, this); } public void close() { try { if (isDone) { // turn off non-blocking IO and return to pool if (log.isDebugEnabled()) log.debug("++++ gracefully closing connection to " + sock.getHost()); channel.configureBlocking(true); sock.close(); return; } } catch (IOException e) { log.warn("++++ memcache: unexpected error closing normally"); } try { if (log.isDebugEnabled()) log.debug("forcefully closing connection to " + sock.getHost()); channel.close(); sock.trueClose(); } catch (IOException ignoreMe) { } } public boolean isDone() { // if we know we're done, just say so if (isDone) return true; // else find out the hard way int strPos = B_END.length - 1; int bi = incoming.size() - 1; while (bi >= 0 && strPos >= 0) { ByteBuffer buf = incoming.get(bi); int pos = buf.position() - 1; while (pos >= 0 && strPos >= 0) { if (buf.get(pos--) != B_END[strPos--]) return false; } bi--; } isDone = strPos < 0; return isDone; } public ByteBuffer getBuffer() { int last = incoming.size() - 1; if (last >= 0 && incoming.get(last).hasRemaining()) { return incoming.get(last); } else { ByteBuffer newBuf = ByteBuffer.allocate(8192); incoming.add(newBuf); return newBuf; } } public String toString() { return "Connection to " + sock.getHost() + " with " + incoming.size() + " bufs; done is " + isDone; } } public void doMulti(boolean asString, Map<String, StringBuilder> sockKeys, String[] keys, Map<String, Object> ret) { long timeRemaining = 0; try { selector = Selector.open(); // get the sockets, flip them to non-blocking, and set up data // structures conns = new Connection[sockKeys.keySet().size()]; numConns = 0; for (Iterator<String> i = sockKeys.keySet().iterator(); i.hasNext();) { // get SockIO obj from hostname String host = i.next(); SockIOPool.SockIO sock = pool.getConnection(host); if (sock == null) { if (errorHandler != null) errorHandler.handleErrorOnGet(this.mc, new IOException("no socket to server available"), keys); return; } conns[numConns++] = new Connection(sock, sockKeys.get(host)); } // the main select loop; ends when // 1) we've received data from all the servers, or // 2) we time out long startTime = System.currentTimeMillis(); long timeout = pool.getMaxBusy(); timeRemaining = timeout; while (numConns > 0 && timeRemaining > 0) { int n = selector.select(Math.min(timeout, 5000)); if (n > 0) { // we've got some activity; handle it Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); handleKey(key); } } else { // timeout likely... better check // TODO: This seems like a problem area that we need to figure out how to handle. log.error("selector timed out waiting for activity"); } timeRemaining = timeout - (System.currentTimeMillis() - startTime); } } catch (IOException e) { // errors can happen just about anywhere above, from // connection setup to any of the mechanics handleError(e, keys); return; } finally { if (log.isDebugEnabled()) log.debug("Disconnecting; numConns=" + numConns + " timeRemaining=" + timeRemaining); // run through our conns and either return them to the pool // or forcibly close them try { selector.close(); } catch (IOException ignoreMe) { } for (Connection c : conns) { if (c != null) c.close(); } } // Done! Build the list of results and return them. If we get // here by a timeout, then some of the connections are probably // not done. But we'll return what we've got... for (Connection c : conns) { try { if (c.incoming.size() > 0 && c.isDone()) loadMulti(new ByteBufArrayInputStream(c.incoming), ret, asString); } catch (Exception e) { // shouldn't happen; we have all the data already log.warn("Caught the aforementioned exception on " + c); } } } private void handleError(Throwable e, String[] keys) { // if we have an errorHandler, use its hook if (errorHandler != null) errorHandler.handleErrorOnGet(MemcachedClient.this, e, keys); // exception thrown log.error("++++ exception thrown while getting from cache on getMulti"); log.error(e.getMessage()); } private void handleKey(SelectionKey key) throws IOException { if (log.isDebugEnabled()) log.debug("handling selector op " + key.readyOps() + " for key " + key); if (key.isReadable()) readResponse(key); else if (key.isWritable()) writeRequest(key); } public void writeRequest(SelectionKey key) throws IOException { ByteBuffer buf = ((Connection) key.attachment()).outgoing; SocketChannel sc = (SocketChannel) key.channel(); if (buf.hasRemaining()) { if (log.isDebugEnabled()) log.debug("writing " + buf.remaining() + "B to " + ((SocketChannel) key.channel()).socket().getInetAddress()); sc.write(buf); } if (!buf.hasRemaining()) { if (log.isDebugEnabled()) log.debug("switching to read mode for server " + ((SocketChannel) key.channel()).socket().getInetAddress()); key.interestOps(SelectionKey.OP_READ); } } public void readResponse(SelectionKey key) throws IOException { Connection conn = (Connection) key.attachment(); ByteBuffer buf = conn.getBuffer(); int count = conn.channel.read(buf); if (count > 0) { if (log.isDebugEnabled()) log.debug("read " + count + " from " + conn.channel.socket().getInetAddress()); if (conn.isDone()) { if (log.isDebugEnabled()) log.debug("connection done to " + conn.channel.socket().getInetAddress()); key.cancel(); numConns--; return; } } } } }